From d928df7ab2f5e82b75bf8d8d423bef76c208e8df Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Mon, 17 May 2021 23:42:33 +0100
Subject: [PATCH 01/25] Discord: Start bot, add !start and pin validity check
The bot should be created by the admin and added to a discord server
mutual to the intended new user(s). On !start in the server,
communication is moved to DMs. Currently !start works, and validity of a
given PIN is checked although nothing it done with this yet.
---
config.go | 7 +-
config/config-base.json | 65 ++++++++++++++++++
discord.go | 146 ++++++++++++++++++++++++++++++++++++++++
go.mod | 1 +
go.sum | 5 ++
main.go | 14 ++++
storage.go | 42 ++++++++----
telegram.go | 24 ++++---
8 files changed, 279 insertions(+), 25 deletions(-)
create mode 100644 discord.go
diff --git a/config.go b/config.go
index 9c60efc..9e18bba 100644
--- a/config.go
+++ b/config.go
@@ -14,6 +14,7 @@ import (
var emailEnabled = false
var messagesEnabled = false
var telegramEnabled = false
+var discordEnabled = false
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
val := app.config.Section(sect).Key(key).MustString("")
@@ -42,7 +43,7 @@ func (app *appContext) loadConfig() error {
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
}
}
- for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users"} {
+ for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users"} {
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
}
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
@@ -87,15 +88,17 @@ func (app *appContext) loadConfig() error {
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
+ discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false)
if !messagesEnabled {
emailEnabled = false
telegramEnabled = false
+ discordEnabled = false
} else if app.config.Section("email").Key("method").MustString("") == "" {
emailEnabled = false
} else {
emailEnabled = true
}
- if !emailEnabled && !telegramEnabled {
+ if !emailEnabled && !telegramEnabled && !discordEnabled {
messagesEnabled = false
}
diff --git a/config/config-base.json b/config/config-base.json
index 12f6861..5756d1c 100644
--- a/config/config-base.json
+++ b/config/config-base.json
@@ -546,6 +546,62 @@
}
}
},
+ "discord": {
+ "order": [],
+ "meta": {
+ "name": "Discord",
+ "description": "Settings for Discord invites/signup/notifications"
+ },
+ "settings": {
+ "enabled": {
+ "name": "Enabled",
+ "required": false,
+ "requires_restart": true,
+ "type": "bool",
+ "value": false,
+ "description": "Enable signup verification through Discord and the sending of notifications through it.\nSee the jfa-go wiki for setting up a bot."
+ },
+ "required": {
+ "name": "Require on sign-up",
+ "required": false,
+ "required_restart": true,
+ "depends_true": "enabled",
+ "type": "bool",
+ "value": false,
+ "description": "Require Discord connection on sign-up."
+ },
+ "token": {
+ "name": "API Token",
+ "required": false,
+ "requires_restart": true,
+ "depends_true": "enabled",
+ "type": "text",
+ "value": "",
+ "description": "Discord Bot API Token."
+ },
+ "start_command": {
+ "name": "Start command",
+ "required": false,
+ "requires_restart": true,
+ "depends_true": "enabled",
+ "type": "text",
+ "value": "!start",
+ "description": "Command to start the user verification process."
+ },
+ "language": {
+ "name": "Language",
+ "required": false,
+ "requires_restart": false,
+ "depends_true": "enabled",
+ "type": "select",
+ "options": [
+ ["en-us", "English (US)"]
+ ],
+ "value": "en-us",
+ "description": "Default Discord message language. Visit weblate if you'd like to translate."
+ }
+ }
+ },
"telegram": {
"order": [],
"meta": {
@@ -565,6 +621,7 @@
"name": "Require on sign-up",
"required": false,
"required_restart": true,
+ "depends_true": "enabled",
"type": "bool",
"value": false,
"description": "Require telegram connection on sign-up."
@@ -1140,6 +1197,14 @@
"type": "text",
"value": "",
"description": "Stores telegram user IDs and language preferences."
+ },
+ "discord_users": {
+ "name": "Discord users",
+ "required": false,
+ "requires_restart": false,
+ "type": "text",
+ "value": "",
+ "description": "Stores discord user IDs and language preferences."
}
}
}
diff --git a/discord.go b/discord.go
new file mode 100644
index 0000000..7d6843a
--- /dev/null
+++ b/discord.go
@@ -0,0 +1,146 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ dg "github.com/bwmarrin/discordgo"
+)
+
+type DiscordToken struct {
+ Token string
+ ChannelID string
+ UserID string
+ Username string
+}
+
+type DiscordDaemon struct {
+ Stopped bool
+ ShutdownChannel chan string
+ bot *dg.Session
+ username string
+ tokens map[string]DiscordToken // map of user IDs to tokens.
+ verifiedTokens []DiscordToken
+ languages map[string]string // Store of languages for user channelIDs. Added to on first interaction, and loaded from app.storage.discord on start.
+ app *appContext
+}
+
+func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
+ token := app.config.Section("discord").Key("token").String()
+ if token == "" {
+ return nil, fmt.Errorf("token was blank")
+ }
+ bot, err := dg.New("Bot " + token)
+ if err != nil {
+ return nil, err
+ }
+ dd := &DiscordDaemon{
+ Stopped: false,
+ ShutdownChannel: make(chan string),
+ bot: bot,
+ tokens: map[string]DiscordToken{},
+ verifiedTokens: []DiscordToken{},
+ languages: map[string]string{},
+ app: app,
+ }
+ for _, user := range app.storage.discord {
+ if user.Lang != "" {
+ dd.languages[user.ChannelID] = user.Lang
+ }
+ }
+ return dd, nil
+}
+
+func (d *DiscordDaemon) NewAuthToken(channelID, userID, username string) DiscordToken {
+ pin := genAuthToken()
+ token := DiscordToken{
+ Token: pin,
+ ChannelID: channelID,
+ UserID: userID,
+ Username: username,
+ }
+ return token
+}
+
+func (d *DiscordDaemon) run() {
+ d.bot.AddHandler(d.messageHandler)
+ d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages
+ if err := d.bot.Open(); err != nil {
+ d.app.err.Printf("Discord: Failed to start daemon: %v", err)
+ return
+ }
+ d.username = d.bot.State.User.Username
+ defer d.bot.Close()
+ <-d.ShutdownChannel
+ d.ShutdownChannel <- "Down"
+ return
+}
+
+func (d *DiscordDaemon) Shutdown() {
+ d.Stopped = true
+ d.ShutdownChannel <- "Down"
+ <-d.ShutdownChannel
+ close(d.ShutdownChannel)
+}
+
+func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
+ if m.Author.ID == s.State.User.ID {
+ return
+ }
+ sects := strings.Split(m.Content, " ")
+ if len(sects) == 0 {
+ return
+ }
+ lang := d.app.storage.lang.chosenTelegramLang
+ if storedLang, ok := d.languages[m.Author.ID]; ok {
+ lang = storedLang
+ }
+ switch msg := sects[0]; msg {
+ case d.app.config.Section("telegram").Key("start_command").MustString("!start"):
+ d.commandStart(s, m, lang)
+ default:
+ d.commandPIN(s, m, lang)
+ }
+}
+
+func (d *DiscordDaemon) commandStart(s *dg.Session, m *dg.MessageCreate, lang string) {
+ channel, err := s.UserChannelCreate(m.Author.ID)
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", m.Author.Username, err)
+ return
+ }
+ token := d.NewAuthToken(channel.ID, m.Author.ID, m.Author.Username)
+ d.tokens[m.Author.ID] = token
+ content := d.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
+ content += d.app.storage.lang.Telegram[lang].Strings.get("languageMessage")
+ _, err = s.ChannelMessageSend(channel.ID, content)
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
+ return
+ }
+}
+
+func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, lang string) {
+ token, ok := d.tokens[m.Author.ID]
+ if !ok || token.Token != m.Content {
+ _, err := s.ChannelMessageSendReply(
+ m.ChannelID,
+ d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
+ m.Reference(),
+ )
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
+ }
+ return
+ }
+ _, err := s.ChannelMessageSendReply(
+ m.ChannelID,
+ d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
+ m.Reference(),
+ )
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
+ }
+ d.verifiedTokens = append(d.verifiedTokens, token)
+ delete(d.tokens, m.Author.ID)
+}
diff --git a/go.mod b/go.mod
index 2fe4e0e..1ca27fc 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ replace github.com/hrfee/jfa-go/ombi => ./ombi
replace github.com/hrfee/jfa-go/logger => ./logger
require (
+ github.com/bwmarrin/discordgo v0.23.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a // indirect
diff --git a/go.sum b/go.sum
index 610d893..8c9ae47 100644
--- a/go.sum
+++ b/go.sum
@@ -11,6 +11,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
+github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
@@ -148,6 +150,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hrfee/mediabrowser v0.3.3 h1:7E05uiol8hh2ytKn3WVLrUIvHAyifYEIy3Y5qtuNh8I=
github.com/hrfee/mediabrowser v0.3.3/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/itchyny/timefmt-go v0.1.2 h1:q0Xa4P5it6K6D7ISsbLAMwx1PnWlixDcJL6/sFs93Hs=
@@ -265,6 +269,7 @@ github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead h1:jeP6FgaSLNTMP+Yri3qjlACywQLye+huGLmNGhBzm6k=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
diff --git a/main.go b/main.go
index e233a30..a16d561 100644
--- a/main.go
+++ b/main.go
@@ -96,6 +96,7 @@ type appContext struct {
validator Validator
email *Emailer
telegram *TelegramDaemon
+ discord *DiscordDaemon
info, debug, err logger.Logger
host string
port int
@@ -341,6 +342,10 @@ func start(asDaemon, firstCall bool) {
if err := app.storage.loadTelegramUsers(); err != nil {
app.err.Printf("Failed to load Telegram users: %v", err)
}
+ app.storage.discord_path = app.config.Section("files").Key("discord_users").String()
+ if err := app.storage.loadDiscordUsers(); err != nil {
+ app.err.Printf("Failed to load Discord users: %v", err)
+ }
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
app.storage.loadProfiles()
@@ -567,6 +572,15 @@ func start(asDaemon, firstCall bool) {
defer app.telegram.Shutdown()
}
}
+ if discordEnabled {
+ app.discord, err = newDiscordDaemon(app)
+ if err != nil {
+ app.err.Printf("Failed to authenticate with Discord: %v", err)
+ } else {
+ go app.discord.run()
+ defer app.discord.Shutdown()
+ }
+ }
} else {
debugMode = false
address = "0.0.0.0:8056"
diff --git a/storage.go b/storage.go
index 0fe7333..add1a9a 100644
--- a/storage.go
+++ b/storage.go
@@ -15,19 +15,20 @@ import (
)
type Storage struct {
- timePattern string
- invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path string
- users map[string]time.Time
- invites Invites
- profiles map[string]Profile
- defaultProfile string
- emails, displayprefs, ombi_template map[string]interface{}
- telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
- customEmails customEmails
- policy mediabrowser.Policy
- configuration mediabrowser.Configuration
- lang Lang
- invitesLock, usersLock sync.Mutex
+ timePattern string
+ invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path string
+ users map[string]time.Time
+ invites Invites
+ profiles map[string]Profile
+ defaultProfile string
+ emails, displayprefs, ombi_template map[string]interface{}
+ telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
+ discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
+ customEmails customEmails
+ policy mediabrowser.Policy
+ configuration mediabrowser.Configuration
+ lang Lang
+ invitesLock, usersLock sync.Mutex
}
type TelegramUser struct {
@@ -37,6 +38,13 @@ type TelegramUser struct {
Contact bool // Whether to contact through telegram or not
}
+type DiscordUser struct {
+ ChannelID string
+ Username string
+ Lang string
+ Contact bool
+}
+
type customEmails struct {
UserCreated customEmail `json:"userCreated"`
InviteExpiry customEmail `json:"inviteExpiry"`
@@ -765,6 +773,14 @@ func (st *Storage) storeTelegramUsers() error {
return storeJSON(st.telegram_path, st.telegram)
}
+func (st *Storage) loadDiscordUsers() error {
+ return loadJSON(st.discord_path, &st.discord)
+}
+
+func (st *Storage) storeDiscordUsers() error {
+ return storeJSON(st.discord_path, st.discord)
+}
+
func (st *Storage) loadCustomEmails() error {
return loadJSON(st.customEmails_path, &st.customEmails)
}
diff --git a/telegram.go b/telegram.go
index 7897413..1d8acc9 100644
--- a/telegram.go
+++ b/telegram.go
@@ -9,7 +9,7 @@ import (
tg "github.com/go-telegram-bot-api/telegram-bot-api"
)
-type VerifiedToken struct {
+type TelegramVerifiedToken struct {
Token string
ChatID int64
Username string
@@ -21,7 +21,7 @@ type TelegramDaemon struct {
bot *tg.BotAPI
username string
tokens []string
- verifiedTokens []VerifiedToken
+ verifiedTokens []TelegramVerifiedToken
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
link string
app *appContext
@@ -37,12 +37,11 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
return nil, err
}
td := &TelegramDaemon{
- Stopped: false,
ShutdownChannel: make(chan string),
bot: bot,
username: bot.Self.UserName,
tokens: []string{},
- verifiedTokens: []VerifiedToken{},
+ verifiedTokens: []TelegramVerifiedToken{},
languages: map[int64]string{},
link: "https://t.me/" + bot.Self.UserName,
app: app,
@@ -55,10 +54,7 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
return td, nil
}
-var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
-
-// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
-func (t *TelegramDaemon) NewAuthToken() string {
+func genAuthToken() string {
rand.Seed(time.Now().UnixNano())
pin := make([]rune, 8)
for i := range pin {
@@ -68,10 +64,18 @@ func (t *TelegramDaemon) NewAuthToken() string {
pin[i] = runes[rand.Intn(len(runes))]
}
}
- t.tokens = append(t.tokens, string(pin))
return string(pin)
}
+var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+
+// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
+func (t *TelegramDaemon) NewAuthToken() string {
+ pin := genAuthToken()
+ t.tokens = append(t.tokens, pin)
+ return pin
+}
+
func (t *TelegramDaemon) run() {
t.app.info.Println("Starting Telegram bot daemon")
u := tg.NewUpdate(0)
@@ -222,7 +226,7 @@ func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string)
if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
- t.verifiedTokens = append(t.verifiedTokens, VerifiedToken{
+ t.verifiedTokens = append(t.verifiedTokens, TelegramVerifiedToken{
Token: upd.Message.Text,
ChatID: upd.Message.Chat.ID,
Username: upd.Message.Chat.UserName,
From 22bba922f95724492ccfe004bae609c846c9538a Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Tue, 18 May 2021 18:41:42 +0100
Subject: [PATCH 02/25] Discord: Add !lang command
---
discord.go | 45 ++++++++++++++++++++++++++++++++++------
lang/telegram/en-us.json | 2 +-
storage.go | 1 +
telegram.go | 9 ++++----
4 files changed, 45 insertions(+), 12 deletions(-)
diff --git a/discord.go b/discord.go
index 7d6843a..e91eca5 100644
--- a/discord.go
+++ b/discord.go
@@ -21,7 +21,7 @@ type DiscordDaemon struct {
username string
tokens map[string]DiscordToken // map of user IDs to tokens.
verifiedTokens []DiscordToken
- languages map[string]string // Store of languages for user channelIDs. Added to on first interaction, and loaded from app.storage.discord on start.
+ languages map[string]string // Store of languages for user IDs. Added to on first interaction, and loaded from app.storage.discord on start.
app *appContext
}
@@ -45,7 +45,7 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
}
for _, user := range app.storage.discord {
if user.Lang != "" {
- dd.languages[user.ChannelID] = user.Lang
+ dd.languages[user.ID] = user.Lang
}
}
return dd, nil
@@ -98,8 +98,10 @@ func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
switch msg := sects[0]; msg {
case d.app.config.Section("telegram").Key("start_command").MustString("!start"):
d.commandStart(s, m, lang)
+ case "!lang":
+ d.commandLang(s, m, sects, lang)
default:
- d.commandPIN(s, m, lang)
+ d.commandPIN(s, m, sects, lang)
}
}
@@ -112,7 +114,7 @@ func (d *DiscordDaemon) commandStart(s *dg.Session, m *dg.MessageCreate, lang st
token := d.NewAuthToken(channel.ID, m.Author.ID, m.Author.Username)
d.tokens[m.Author.ID] = token
content := d.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
- content += d.app.storage.lang.Telegram[lang].Strings.get("languageMessage")
+ content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"})
_, err = s.ChannelMessageSend(channel.ID, content)
if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
@@ -120,9 +122,40 @@ func (d *DiscordDaemon) commandStart(s *dg.Session, m *dg.MessageCreate, lang st
}
}
-func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, lang string) {
+func (d *DiscordDaemon) commandLang(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
+ if len(sects) == 1 {
+ list := "!lang \n"
+ for code := range d.app.storage.lang.Telegram {
+ list += fmt.Sprintf("%s: %s\n", code, d.app.storage.lang.Telegram[code].Meta.Name)
+ }
+ _, err := s.ChannelMessageSendReply(
+ m.ChannelID,
+ list,
+ m.Reference(),
+ )
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
+ }
+ return
+ }
+ if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
+ d.languages[m.Author.ID] = sects[1]
+ for jfID, user := range d.app.storage.discord {
+ if user.ID == m.Author.ID {
+ user.Lang = sects[1]
+ d.app.storage.discord[jfID] = user
+ if err := d.app.storage.storeDiscordUsers(); err != nil {
+ d.app.err.Printf("Failed to store Discord users: %v", err)
+ }
+ break
+ }
+ }
+ }
+}
+
+func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
token, ok := d.tokens[m.Author.ID]
- if !ok || token.Token != m.Content {
+ if !ok || token.Token != sects[0] {
_, err := s.ChannelMessageSendReply(
m.ChannelID,
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
diff --git a/lang/telegram/en-us.json b/lang/telegram/en-us.json
index 749587d..642399f 100644
--- a/lang/telegram/en-us.json
+++ b/lang/telegram/en-us.json
@@ -6,6 +6,6 @@
"startMessage": "Hi!\nEnter your Jellyfin PIN code here to verify your account.",
"invalidPIN": "That PIN was invalid, try again.",
"pinSuccess": "Success! You can now return to the sign-up page.",
- "languageMessage": "Note: See available languages with /lang, and set language with /lang ."
+ "languageMessage": "Note: See available languages with {command}, and set language with {command} ."
}
}
diff --git a/storage.go b/storage.go
index add1a9a..61a2e2d 100644
--- a/storage.go
+++ b/storage.go
@@ -40,6 +40,7 @@ type TelegramUser struct {
type DiscordUser struct {
ChannelID string
+ ID string
Username string
Lang string
Contact bool
diff --git a/telegram.go b/telegram.go
index 1d8acc9..00d5ee3 100644
--- a/telegram.go
+++ b/telegram.go
@@ -172,7 +172,7 @@ func (t *TelegramDaemon) Shutdown() {
func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang string) {
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
- content += t.app.storage.lang.Telegram[lang].Strings.get("languageMessage")
+ content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"})
err := t.Reply(upd, content)
if err != nil {
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
@@ -181,9 +181,9 @@ func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang strin
func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string) {
if len(sects) == 1 {
- list := "/lang \n"
+ list := "/lang ``\n"
for code := range t.app.storage.lang.Telegram {
- list += fmt.Sprintf("%s: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
+ list += fmt.Sprintf("`%s`: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
}
err := t.Reply(upd, list)
if err != nil {
@@ -197,8 +197,7 @@ func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string
if user.ChatID == upd.Message.Chat.ID {
user.Lang = sects[1]
t.app.storage.telegram[jfID] = user
- err := t.app.storage.storeTelegramUsers()
- if err != nil {
+ if err := t.app.storage.storeTelegramUsers(); err != nil {
t.app.err.Printf("Failed to store Telegram users: %v", err)
}
break
From 524941da0c2d5b57455ffad2b609a6735c6e0b8f Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Fri, 21 May 2021 21:10:32 +0100
Subject: [PATCH 03/25] fix heading size with sm
---
css/base.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/css/base.css b/css/base.css
index 4b1e040..3b9626d 100644
--- a/css/base.css
+++ b/css/base.css
@@ -172,7 +172,7 @@ div.card:contains(section.banner.footer) {
}
p.sm,
-span.sm {
+span.sm:not(.heading) {
font-size: 0.75rem;
}
From f8f5f35cc1860d8efb04e3c82af9d10310f0f8d4 Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Fri, 21 May 2021 21:35:25 +0100
Subject: [PATCH 04/25] PIN verification, notifications, multiple notif
providers
Discord, Email & Telegram can be enabled, although email is always
enabled right now (will fix). Also apparently markdown hyperlinks don't
work in Discord, eventually will implement something to convert them to
embeds.
---
api.go | 92 +++++++++++++++++++---
config/config-base.json | 9 +++
discord.go | 156 ++++++++++++++++++++++++++----------
email.go | 22 +++++-
html/admin.html | 4 +
html/form-base.html | 3 +
html/form.html | 22 ++++++
lang/common/en-us.json | 2 +
lang/form/en-us.json | 8 +-
models.go | 22 ++++--
router.go | 5 +-
storage.go | 11 +--
ts/form.ts | 55 ++++++++++++-
ts/modules/accounts.ts | 170 +++++++++++++++++++++++++++++++++++-----
ts/typings/d.ts | 1 +
views.go | 19 ++++-
16 files changed, 509 insertions(+), 92 deletions(-)
diff --git a/api.go b/api.go
index 6888868..31d4816 100644
--- a/api.go
+++ b/api.go
@@ -330,6 +330,30 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false
return
}
+ var discordUser DiscordUser
+ discordVerified := false
+ if discordEnabled {
+ if req.DiscordPIN == "" {
+ if app.config.Section("discord").Key("required").MustBool(false) {
+ f = func(gc *gin.Context) {
+ app.debug.Printf("%s: New user failed: Discord verification not completed", req.Code)
+ respond(401, "errorDiscordVerification", gc)
+ }
+ success = false
+ return
+ }
+ } else {
+ discordUser, discordVerified = app.discord.verifiedTokens[req.DiscordPIN]
+ if !discordVerified {
+ f = func(gc *gin.Context) {
+ app.debug.Printf("%s: New user failed: Discord PIN was invalid", req.Code)
+ respond(401, "errorInvalidPIN", gc)
+ }
+ success = false
+ return
+ }
+ }
+ }
telegramTokenIndex := -1
if telegramEnabled {
if req.TelegramPIN == "" {
@@ -479,7 +503,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
app.err.Printf("Failed to store user duration: %v", err)
}
}
-
+ if discordEnabled && discordVerified {
+ discordUser.Contact = req.DiscordContact
+ if app.storage.discord == nil {
+ app.storage.discord = map[string]DiscordUser{}
+ }
+ app.storage.discord[user.ID] = discordUser
+ if err := app.storage.storeDiscordUsers(); err != nil {
+ app.err.Printf("Failed to store Discord users: %v", err)
+ } else {
+ delete(app.discord.verifiedTokens, req.DiscordPIN)
+ }
+ }
if telegramEnabled && telegramTokenIndex != -1 {
tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
tgUser := TelegramUser{
@@ -494,8 +529,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
app.storage.telegram = map[string]TelegramUser{}
}
app.storage.telegram[user.ID] = tgUser
- err := app.storage.storeTelegramUsers()
- if err != nil {
+ if err := app.storage.storeTelegramUsers(); err != nil {
app.err.Printf("Failed to store Telegram users: %v", err)
} else {
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[telegramTokenIndex] = app.telegram.verifiedTokens[telegramTokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
@@ -503,7 +537,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
}
}
- if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 {
+ if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 || discordVerified {
name := app.getAddressOrName(user.ID)
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
@@ -1169,6 +1203,7 @@ func (app *appContext) GetUsers(gc *gin.Context) {
}
if email, ok := app.storage.emails[jfUser.ID]; ok {
user.Email = email.(string)
+ user.NotifyThroughEmail = user.Email != ""
}
expiry, ok := app.storage.users[jfUser.ID]
if ok {
@@ -1178,6 +1213,11 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.Telegram = tgUser.Username
user.NotifyThroughTelegram = tgUser.Contact
}
+ if dc, ok := app.storage.discord[jfUser.ID]; ok {
+ user.Discord = dc.Username + "#" + dc.Discriminator
+ user.DiscordID = dc.ID
+ user.NotifyThroughDiscord = dc.Contact
+ }
resp.UserList[i] = user
i++
}
@@ -2011,7 +2051,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
// @Router /users/telegram/notify [post]
// @Security Bearer
// @tags Other
-func (app *appContext) TelegramSetNotify(gc *gin.Context) {
+func (app *appContext) SetContactMethods(gc *gin.Context) {
var req telegramNotifyDTO
gc.BindJSON(&req)
if req.ID == "" {
@@ -2019,23 +2059,34 @@ func (app *appContext) TelegramSetNotify(gc *gin.Context) {
return
}
if tgUser, ok := app.storage.telegram[req.ID]; ok {
- tgUser.Contact = req.Enabled
+ tgUser.Contact = req.Telegram
app.storage.telegram[req.ID] = tgUser
if err := app.storage.storeTelegramUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Telegram: Failed to store users: %v", err)
return
}
- respondBool(200, true, gc)
msg := ""
- if !req.Enabled {
+ if !req.Telegram {
msg = "not"
}
app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg)
- return
}
- app.err.Printf("Telegram: User \"%s\" does not have a telegram account registered.", req.ID)
- respondBool(400, false, gc)
+ if dcUser, ok := app.storage.discord[req.ID]; ok {
+ dcUser.Contact = req.Discord
+ app.storage.discord[req.ID] = dcUser
+ if err := app.storage.storeDiscordUsers(); err != nil {
+ respondBool(500, false, gc)
+ app.err.Printf("Discord: Failed to store users: %v", err)
+ return
+ }
+ msg := ""
+ if !req.Discord {
+ msg = "not"
+ }
+ app.debug.Printf("Discord: User \"%s\" will %s be notified through Discord.", dcUser.Username, msg)
+ }
+ respondBool(200, true, gc)
}
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
@@ -2092,6 +2143,25 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
respondBool(200, tokenIndex != -1, gc)
}
+// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
+// @Produce json
+// @Success 200 {object} boolResponse
+// @Success 401 {object} boolResponse
+// @Param pin path string true "PIN code to check"
+// @Param invCode path string true "invite Code"
+// @Router /invite/{invCode}/discord/verified/{pin} [get]
+// @tags Other
+func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
+ code := gc.Param("invCode")
+ if _, ok := app.storage.invites[code]; !ok {
+ respondBool(401, false, gc)
+ return
+ }
+ pin := gc.Param("pin")
+ _, ok := app.discord.verifiedTokens[pin]
+ respondBool(200, ok, gc)
+}
+
// @Summary Restarts the program. No response means success.
// @Router /restart [post]
// @Security Bearer
diff --git a/config/config-base.json b/config/config-base.json
index 5756d1c..48fb173 100644
--- a/config/config-base.json
+++ b/config/config-base.json
@@ -588,6 +588,15 @@
"value": "!start",
"description": "Command to start the user verification process."
},
+ "channel": {
+ "name": "Channel to monitor",
+ "required": false,
+ "requires_restart": true,
+ "depens_true": "enabled",
+ "type": "text",
+ "value": "",
+ "description": "Only listen to commands in specified channel. Leave blank to monitor all."
+ },
"language": {
"name": "Language",
"required": false,
diff --git a/discord.go b/discord.go
index e91eca5..f80d216 100644
--- a/discord.go
+++ b/discord.go
@@ -7,22 +7,17 @@ import (
dg "github.com/bwmarrin/discordgo"
)
-type DiscordToken struct {
- Token string
- ChannelID string
- UserID string
- Username string
-}
-
type DiscordDaemon struct {
- Stopped bool
- ShutdownChannel chan string
- bot *dg.Session
- username string
- tokens map[string]DiscordToken // map of user IDs to tokens.
- verifiedTokens []DiscordToken
- languages map[string]string // Store of languages for user IDs. Added to on first interaction, and loaded from app.storage.discord on start.
- app *appContext
+ Stopped bool
+ ShutdownChannel chan string
+ bot *dg.Session
+ username string
+ tokens []string
+ verifiedTokens map[string]DiscordUser // Map of tokens to discord users.
+ channelID, channelName string
+ serverChannelName string
+ users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start.
+ app *appContext
}
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
@@ -38,28 +33,39 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
Stopped: false,
ShutdownChannel: make(chan string),
bot: bot,
- tokens: map[string]DiscordToken{},
- verifiedTokens: []DiscordToken{},
- languages: map[string]string{},
+ tokens: []string{},
+ verifiedTokens: map[string]DiscordUser{},
+ users: map[string]DiscordUser{},
app: app,
}
for _, user := range app.storage.discord {
- if user.Lang != "" {
- dd.languages[user.ID] = user.Lang
- }
+ dd.users[user.ID] = user
}
return dd, nil
}
-func (d *DiscordDaemon) NewAuthToken(channelID, userID, username string) DiscordToken {
+// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
+func (d *DiscordDaemon) NewAuthToken() string {
pin := genAuthToken()
- token := DiscordToken{
- Token: pin,
- ChannelID: channelID,
- UserID: userID,
- Username: username,
+ d.tokens = append(d.tokens, pin)
+ return pin
+}
+
+func (d *DiscordDaemon) NewUnknownUser(channelID, userID, discrim, username string) DiscordUser {
+ user := DiscordUser{
+ ChannelID: channelID,
+ ID: userID,
+ Username: username,
+ Discriminator: discrim,
}
- return token
+ return user
+}
+
+func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string) DiscordUser {
+ if user, ok := d.users[userID]; ok {
+ return user
+ }
+ return d.NewUnknownUser(channelID, userID, discrim, username)
}
func (d *DiscordDaemon) run() {
@@ -70,6 +76,15 @@ func (d *DiscordDaemon) run() {
return
}
d.username = d.bot.State.User.Username
+ // Choose the last guild (server), for now we don't really support multiple anyway
+ guild, err := d.bot.Guild(d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID)
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to get guild: %v", err)
+ }
+ d.serverChannelName = guild.Name
+ if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
+ d.channelName = channel
+ }
defer d.bot.Close()
<-d.ShutdownChannel
d.ShutdownChannel <- "Down"
@@ -84,6 +99,22 @@ func (d *DiscordDaemon) Shutdown() {
}
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
+ if m.GuildID != "" && d.channelName != "" {
+ if d.channelID == "" {
+ channel, err := s.Channel(m.ChannelID)
+ if err != nil {
+ d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
+ d.channelName = ""
+ }
+ if channel.Name == d.channelName {
+ d.channelID = channel.ID
+ }
+ }
+ if d.channelID != m.ChannelID {
+ d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
+ return
+ }
+ }
if m.Author.ID == s.State.User.ID {
return
}
@@ -92,11 +123,13 @@ func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
return
}
lang := d.app.storage.lang.chosenTelegramLang
- if storedLang, ok := d.languages[m.Author.ID]; ok {
- lang = storedLang
+ if user, ok := d.users[m.Author.ID]; ok {
+ if _, ok := d.app.storage.lang.Telegram[user.Lang]; ok {
+ lang = user.Lang
+ }
}
switch msg := sects[0]; msg {
- case d.app.config.Section("telegram").Key("start_command").MustString("!start"):
+ case d.app.config.Section("discord").Key("start_command").MustString("!start"):
d.commandStart(s, m, lang)
case "!lang":
d.commandLang(s, m, sects, lang)
@@ -111,8 +144,8 @@ func (d *DiscordDaemon) commandStart(s *dg.Session, m *dg.MessageCreate, lang st
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", m.Author.Username, err)
return
}
- token := d.NewAuthToken(channel.ID, m.Author.ID, m.Author.Username)
- d.tokens[m.Author.ID] = token
+ user := d.MustGetUser(channel.ID, m.Author.ID, m.Author.Discriminator, m.Author.Username)
+ d.users[m.Author.ID] = user
content := d.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"})
_, err = s.ChannelMessageSend(channel.ID, content)
@@ -139,7 +172,7 @@ func (d *DiscordDaemon) commandLang(s *dg.Session, m *dg.MessageCreate, sects []
return
}
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
- d.languages[m.Author.ID] = sects[1]
+ var user DiscordUser
for jfID, user := range d.app.storage.discord {
if user.ID == m.Author.ID {
user.Lang = sects[1]
@@ -150,30 +183,69 @@ func (d *DiscordDaemon) commandLang(s *dg.Session, m *dg.MessageCreate, sects []
break
}
}
+ d.users[m.Author.ID] = user
}
}
func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
- token, ok := d.tokens[m.Author.ID]
- if !ok || token.Token != sects[0] {
- _, err := s.ChannelMessageSendReply(
+ if _, ok := d.users[m.Author.ID]; ok {
+ channel, err := s.Channel(m.ChannelID)
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to get channel: %v", err)
+ return
+ }
+ if channel.Type != dg.ChannelTypeDM {
+ d.app.debug.Println("Discord: Ignoring message as not a DM")
+ return
+ }
+ } else {
+ d.app.debug.Println("Discord: Ignoring message as user was not found")
+ return
+ }
+ tokenIndex := -1
+ for i, token := range d.tokens {
+ if sects[0] == token {
+ tokenIndex = i
+ break
+ }
+ }
+ if tokenIndex == -1 {
+ _, err := s.ChannelMessageSend(
m.ChannelID,
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
- m.Reference(),
)
if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
}
return
}
- _, err := s.ChannelMessageSendReply(
+ _, err := s.ChannelMessageSend(
m.ChannelID,
d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
- m.Reference(),
)
if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
}
- d.verifiedTokens = append(d.verifiedTokens, token)
- delete(d.tokens, m.Author.ID)
+ d.verifiedTokens[sects[0]] = d.users[m.Author.ID]
+ d.tokens[len(d.tokens)-1], d.tokens[tokenIndex] = d.tokens[tokenIndex], d.tokens[len(d.tokens)-1]
+ d.tokens = d.tokens[:len(d.tokens)-1]
+}
+
+func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
+ for _, id := range channelID {
+ msg := ""
+ if message.Markdown == "" {
+ msg = message.Text
+ } else {
+ msg = message.Markdown
+ }
+ _, err := d.bot.ChannelMessageSend(
+ id,
+ msg,
+ )
+ if err != nil {
+ return err
+ }
+ }
+ return nil
}
diff --git a/email.go b/email.go
index e81d6b5..5f752fd 100644
--- a/email.go
+++ b/email.go
@@ -230,7 +230,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
var keys []string
plaintext := app.config.Section("email").Key("plaintext").MustBool(false)
if plaintext {
- if telegramEnabled {
+ if telegramEnabled || discordEnabled {
keys = []string{"text"}
text, markdown = "", ""
} else {
@@ -238,7 +238,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
text = ""
}
} else {
- if telegramEnabled {
+ if telegramEnabled || discordEnabled {
keys = []string{"html", "text", "markdown"}
} else {
keys = []string{"html", "text"}
@@ -807,8 +807,21 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
var err error
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
err = app.telegram.Send(email, tgChat.ChatID)
- } else if address, ok := app.storage.emails[id]; ok {
+ if err != nil {
+ return err
+ }
+ }
+ if dcChat, ok := app.storage.discord[id]; ok && dcChat.Contact && discordEnabled {
+ err = app.discord.Send(email, dcChat.ChannelID)
+ if err != nil {
+ return err
+ }
+ }
+ if address, ok := app.storage.emails[id]; ok {
err = app.email.send(email, address.(string))
+ if err != nil {
+ return err
+ }
}
if err != nil {
return err
@@ -818,6 +831,9 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
}
func (app *appContext) getAddressOrName(jfID string) string {
+ if dcChat, ok := app.storage.discord[jfID]; ok && dcChat.Contact && discordEnabled {
+ return dcChat.Username + "#" + dcChat.Discriminator
+ }
if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled {
return "@" + tgChat.Username
}
diff --git a/html/admin.html b/html/admin.html
index a5b784f..6286144 100644
--- a/html/admin.html
+++ b/html/admin.html
@@ -7,6 +7,7 @@
window.notificationsEnabled = {{ .notifications }};
window.emailEnabled = {{ .email_enabled }};
window.telegramEnabled = {{ .telegram_enabled }};
+ window.discordEnabled = {{ .discord_enabled }};
window.ombiEnabled = {{ .ombiEnabled }};
window.usernameEnabled = {{ .username }};
window.langFile = JSON.parse({{ .language }});
@@ -525,6 +526,9 @@
{{ if .telegram_enabled }}
Telegram |
{{ end }}
+ {{ if .discord_enabled }}
+ Discord |
+ {{ end }}
{{ .strings.expiry }} |
{{ .strings.lastActiveTime }} |
diff --git a/html/form-base.html b/html/form-base.html
index 8a4a9ee..e144f58 100644
--- a/html/form-base.html
+++ b/html/form-base.html
@@ -17,6 +17,9 @@
window.telegramEnabled = {{ .telegramEnabled }};
window.telegramRequired = {{ .telegramRequired }};
window.telegramPIN = "{{ .telegramPIN }}";
+ window.discordEnabled = {{ .discordEnabled }};
+ window.discordRequired = {{ .discordRequired }};
+ window.discordPIN = "{{ .discordPIN }}";
{{ end }}
diff --git a/html/form.html b/html/form.html
index df3ee14..c391707 100644
--- a/html/form.html
+++ b/html/form.html
@@ -37,6 +37,16 @@
{{ end }}
+ {{ if .discordEnabled }}
+
+
+
{{ .strings.linkDiscord }}
+
{{ .discordSendPINMessage }}
+
{{ .discordPIN }}
+
{{ .strings.success }}
+
+
+ {{ end }}
@@ -69,13 +79,25 @@
{{ if .telegramEnabled }}
{{ .strings.linkTelegram }}
+ {{ end }}
+ {{ if .discordEnabled }}
+ {{ .strings.linkDiscord }}
+ {{ end }}
+ {{ if or (.telegramEnabled) (.discordEnabled) }}
+ {{ if .telegramEnabled }}
+ {{ end }}
+ {{ if .discordEnabled }}
+
+ {{ end }}
{{ end }}
diff --git a/lang/common/en-us.json b/lang/common/en-us.json
index 9d77a89..3e9ef53 100644
--- a/lang/common/en-us.json
+++ b/lang/common/en-us.json
@@ -17,6 +17,8 @@
"linkTelegram": "Link Telegram",
"contactEmail": "Contact through Email",
"contactTelegram": "Contact through Telegram",
+ "linkDiscord": "Link Discord",
+ "contactDiscord": "Contact through Discord",
"theme": "Theme"
}
}
diff --git a/lang/form/en-us.json b/lang/form/en-us.json
index 391d672..6726c0b 100644
--- a/lang/form/en-us.json
+++ b/lang/form/en-us.json
@@ -18,14 +18,16 @@
"confirmationRequired": "Email confirmation required",
"confirmationRequiredMessage": "Please check your email inbox to verify your address.",
"yourAccountIsValidUntil": "Your account will be valid until {date}.",
- "sendPIN": "Send the PIN below to the bot, then come back here to link your account."
+ "sendPIN": "Send the PIN below to the bot, then come back here to link your account.",
+ "sendPINDiscord": "Type {command} in {server_channel} on Discord, then send the PIN below via DM to the bot."
},
"notifications": {
"errorUserExists": "User already exists.",
"errorInvalidCode": "Invalid invite code.",
"errorTelegramVerification": "Telegram verification required.",
- "errorInvalidPIN": "Telegram PIN is invalid.",
- "telegramVerified": "Telegram account verified."
+ "errorDiscordVerification": "Discord verification required.",
+ "errorInvalidPIN": "PIN is invalid.",
+ "verified": "Account verified."
},
"validationStrings": {
"length": {
diff --git a/models.go b/models.go
index 772238b..c79cab4 100644
--- a/models.go
+++ b/models.go
@@ -17,6 +17,8 @@ type newUserDTO struct {
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used)
TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs
+ DiscordPIN string `json:"discord_pin" example:"A1-B2-3C"` // Discord verification PIN (if used)
+ DiscordContact bool `json:"discord_contact"` // Whether or not to use discord for notifications/pwrs
}
type newUserResponse struct {
@@ -125,12 +127,16 @@ type respUser struct {
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
Name string `json:"name" example:"jeff"` // Username of user
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
- LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
- Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
- Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
- Disabled bool `json:"disabled"` // Whether or not the user is disabled.
- Telegram string `json:"telegram"` // Telegram username (if known)
+ NotifyThroughEmail bool `json:"notify_email"`
+ LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
+ Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
+ Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
+ Disabled bool `json:"disabled"` // Whether or not the user is disabled.
+ Telegram string `json:"telegram"` // Telegram username (if known)
NotifyThroughTelegram bool `json:"notify_telegram"`
+ Discord string `json:"discord"` // Discord username (if known)
+ DiscordID string `json:"discord_id"` // Discord user ID for creating links.
+ NotifyThroughDiscord bool `json:"notify_discord"`
}
type getUsersDTO struct {
@@ -250,6 +256,8 @@ type telegramSetDTO struct {
}
type telegramNotifyDTO struct {
- ID string `json:"id"`
- Enabled bool `json:"enabled"`
+ ID string `json:"id"`
+ Email bool `json:"email"`
+ Discord bool `json:"discord"`
+ Telegram bool `json:"telegram"`
}
diff --git a/router.go b/router.go
index 355fb3f..4bad4d8 100644
--- a/router.go
+++ b/router.go
@@ -121,6 +121,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
if telegramEnabled {
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
}
+ if discordEnabled {
+ router.GET(p+"/invite/:invCode/discord/verified/:pin", app.DiscordVerifiedInvite)
+ }
}
if *SWAGGER {
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
@@ -162,7 +165,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/telegram/pin", app.TelegramGetPin)
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
api.POST(p+"/users/telegram", app.TelegramAddUser)
- api.POST(p+"/users/telegram/notify", app.TelegramSetNotify)
+ api.POST(p+"/users/contact", app.SetContactMethods)
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET(p+"/ombi/users", app.OmbiUsers)
diff --git a/storage.go b/storage.go
index 61a2e2d..b0f0ee8 100644
--- a/storage.go
+++ b/storage.go
@@ -39,11 +39,12 @@ type TelegramUser struct {
}
type DiscordUser struct {
- ChannelID string
- ID string
- Username string
- Lang string
- Contact bool
+ ChannelID string
+ ID string
+ Username string
+ Discriminator string
+ Lang string
+ Contact bool
}
type customEmails struct {
diff --git a/ts/form.ts b/ts/form.ts
index 0cd1611..8f76c89 100644
--- a/ts/form.ts
+++ b/ts/form.ts
@@ -8,12 +8,16 @@ interface formWindow extends Window {
invalidPassword: string;
successModal: Modal;
telegramModal: Modal;
+ discordModal: Modal;
confirmationModal: Modal
code: string;
messages: { [key: string]: string };
confirmation: boolean;
telegramRequired: boolean;
telegramPIN: string;
+ discordRequired: boolean;
+ discordPIN: string;
+ discordStartCommand: string;
userExpiryEnabled: boolean;
userExpiryMonths: number;
userExpiryDays: number;
@@ -68,7 +72,7 @@ if (window.telegramEnabled) {
telegramVerified = true;
waiting.classList.add("~positive");
waiting.classList.remove("~info");
- window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]);
+ window.notifications.customPositive("telegramVerified", "", window.messages["verified"]);
setTimeout(window.telegramModal.close, 2000);
telegramButton.classList.add("unfocused");
document.getElementById("contact-via").classList.remove("unfocused");
@@ -84,6 +88,46 @@ if (window.telegramEnabled) {
};
}
+var discordVerified = false;
+if (window.discordEnabled) {
+ window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired);
+ const discordButton = document.getElementById("link-discord") as HTMLSpanElement;
+ discordButton.onclick = () => {
+ const waiting = document.getElementById("discord-waiting") as HTMLSpanElement;
+ toggleLoader(waiting);
+ window.discordModal.show();
+ let modalClosed = false;
+ window.discordModal.onclose = () => {
+ modalClosed = true;
+ toggleLoader(waiting);
+ }
+ const checkVerified = () => _get("/invite/" + window.code + "/discord/verified/" + window.discordPIN, null, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ if (req.status == 401) {
+ window.discordModal.close();
+ window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]);
+ return;
+ } else if (req.status == 200) {
+ if (req.response["success"] as boolean) {
+ discordVerified = true;
+ waiting.classList.add("~positive");
+ waiting.classList.remove("~info");
+ window.notifications.customPositive("discordVerified", "", window.messages["verified"]);
+ setTimeout(window.discordModal.close, 2000);
+ discordButton.classList.add("unfocused");
+ document.getElementById("contact-via").classList.remove("unfocused");
+ const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
+ radio.checked = true;
+ } else if (!modalClosed) {
+ setTimeout(checkVerified, 1500);
+ }
+ }
+ }
+ });
+ checkVerified();
+ };
+}
+
if (window.confirmation) {
window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true);
}
@@ -161,6 +205,8 @@ interface sendDTO {
password: string;
telegram_pin?: string;
telegram_contact?: boolean;
+ discord_pin?: string;
+ discord_contact?: boolean;
}
const create = (event: SubmitEvent) => {
@@ -179,6 +225,13 @@ const create = (event: SubmitEvent) => {
send.telegram_contact = true;
}
}
+ if (discordVerified) {
+ send.discord_pin = window.discordPIN;
+ const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
+ if (radio.checked) {
+ send.discord_contact = true;
+ }
+ }
_post("/newUser", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
let vals = req.response as respDTO;
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index e383b4a..21f19ef 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -7,12 +7,16 @@ interface User {
id: string;
name: string;
email: string | undefined;
+ notify_email: boolean;
last_active: number;
admin: boolean;
disabled: boolean;
expiry: number;
telegram: string;
notify_telegram: boolean;
+ discord: string;
+ notify_discord: boolean;
+ discord_id: string;
}
interface getPinResponse {
@@ -27,11 +31,16 @@ class user implements User {
private _admin: HTMLSpanElement;
private _disabled: HTMLSpanElement;
private _email: HTMLInputElement;
+ private _notifyEmail: boolean;
private _emailAddress: string;
private _emailEditButton: HTMLElement;
private _telegram: HTMLTableDataCellElement;
private _telegramUsername: string;
private _notifyTelegram: boolean;
+ private _discord: HTMLTableDataCellElement;
+ private _discordUsername: string;
+ private _discordID: string;
+ private _notifyDiscord: boolean;
private _expiry: HTMLTableDataCellElement;
private _expiryUnix: number;
private _lastActive: HTMLTableDataCellElement;
@@ -81,6 +90,19 @@ class user implements User {
this._email.textContent = value;
}
}
+
+ get notify_email(): boolean { return this._notifyEmail; }
+ set notify_email(s: boolean) {
+ this._notifyEmail = s;
+ if (window.telegramEnabled && this._telegramUsername != "") {
+ const email = this._telegram.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
+ email.checked = s;
+ }
+ if (window.discordEnabled && this._discordUsername != "") {
+ const email = this._discord.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
+ email.checked = s;
+ }
+ }
get telegram(): string { return this._telegramUsername; }
set telegram(u: string) {
@@ -90,7 +112,7 @@ class user implements User {
this._telegram.innerHTML = `Add`;
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
} else {
- this._telegram.innerHTML = `
+ let innerHTML = `
@${u}
`;
+ this._discord.innerHTML = innerHTML;
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
const button = this._telegram.querySelector("i");
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
- const radios = this._telegram.querySelectorAll("input") as NodeListOf;
- for (let i = 0; i < radios.length; i++) {
- radios[i].onclick = this._setTelegramNotify;
+ const checks = this._telegram.querySelectorAll("input") as NodeListOf;
+ for (let i = 0; i < checks.length; i++) {
+ checks[i].onclick = () => this._setNotifyMethod("telegram");
}
button.onclick = () => {
@@ -134,36 +167,128 @@ class user implements User {
set notify_telegram(s: boolean) {
if (!window.telegramEnabled || !this._telegramUsername) return;
this._notifyTelegram = s;
- const radios = this._telegram.querySelectorAll("input") as NodeListOf;
- radios[0].checked = !s;
- radios[1].checked = s;
+ const telegram = this._telegram.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
+ telegram.checked = s;
+ if (window.discordEnabled && this._discordUsername != "") {
+ const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
+ telegram.checked = s;
+ }
}
- private _setTelegramNotify = () => {
- const radios = this._telegram.querySelectorAll("input") as NodeListOf;
+ private _setNotifyMethod = (mode: string = "telegram") => {
+ let el: HTMLElement;
+ if (mode == "telegram") { el = this._telegram }
+ else if (mode == "discord") { el = this._discord }
+ const email = el.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
let send = {
id: this.id,
- enabled: radios[1].checked
- };
- _post("/users/telegram/notify", send, (req: XMLHttpRequest) => {
+ email: email.checked
+ }
+ if (window.telegramEnabled && this._telegramUsername != "") {
+ const telegram = el.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
+ send["telegram"] = telegram.checked;
+ }
+ if (window.discordEnabled && this._discordUsername != "") {
+ const discord = el.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
+ send["discord"] = discord.checked;
+ }
+ _post("/users/contact", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
- window.notifications.customError("errorSetTelegramNotify", window.lang.notif("errorSaveSettings"));
- radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
+ window.notifications.customError("errorSetNotify", window.lang.notif("errorSaveSettings"));
+ document.dispatchEvent(new CustomEvent("accounts-reload"));
return;
}
}
}, false, (req: XMLHttpRequest) => {
if (req.status == 0) {
window.notifications.connectionError();
- radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
+ document.dispatchEvent(new CustomEvent("accounts-reload"));
return;
} else if (req.status == 401) {
- radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized"));
+ document.dispatchEvent(new CustomEvent("accounts-reload"));
}
});
}
+
+ get discord(): string { return this._discordUsername; }
+ set discord(u: string) {
+ if (!window.discordEnabled) return;
+ this._discordUsername = u;
+ if (u == "") {
+ this._discord.innerHTML = `Add`;
+ // (this._discord.querySelector("span") as HTMLSpanElement).onclick = this._addDiscord;
+ } else {
+ let innerHTML = `
+ @${u}
+
+
+ `;
+ this._discord.innerHTML = innerHTML;
+ // Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
+ const button = this._discord.querySelector("i");
+ const dropdown = this._discord.querySelector("div.dropdown") as HTMLDivElement;
+ const checks = this._discord.querySelectorAll("input") as NodeListOf;
+ for (let i = 0; i < checks.length; i++) {
+ checks[i].onclick = () => this._setNotifyMethod("discord");
+ }
+
+ button.onclick = () => {
+ dropdown.classList.add("selected");
+ document.addEventListener("click", outerClickListener);
+ };
+ const outerClickListener = (event: Event) => {
+ if (!(event.target instanceof HTMLElement && (this._discord.contains(event.target) || button.contains(event.target)))) {
+ dropdown.classList.remove("selected");
+ document.removeEventListener("click", outerClickListener);
+ }
+ };
+ }
+ }
+
+ get discord_id(): string { return this._discordID; }
+ set discord_id(id: string) {
+ this._discordID = id;
+ const link = this._discord.getElementsByClassName("discord-link")[0] as HTMLAnchorElement;
+ link.href = `https://discord.com/users/${id}`;
+ }
+
+ get notify_discord(): boolean { return this._notifyDiscord; }
+ set notify_discord(s: boolean) {
+ if (!window.discordEnabled || !this._discordUsername) return;
+ this._notifyDiscord = s;
+ const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
+ discord.checked = s;
+ if (window.telegramEnabled && this._telegramUsername != "") {
+ const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
+ discord.checked = s;
+ }
+ }
get expiry(): number { return this._expiryUnix; }
set expiry(unix: number) {
@@ -200,6 +325,11 @@ class user implements User {
|
`;
}
+ if (window.discordEnabled) {
+ innerHTML += `
+ |
+ `;
+ }
innerHTML += `
|
|
@@ -213,6 +343,7 @@ class user implements User {
this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement;
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement;
+ this._discord = this._row.querySelector(".accounts-discord") as HTMLTableDataCellElement;
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
this._check.onchange = () => { this.selected = this._check.checked; }
@@ -320,11 +451,14 @@ class user implements User {
this.name = user.name;
this.email = user.email || "";
this.telegram = user.telegram;
+ this.discord = user.discord;
this.last_active = user.last_active;
this.admin = user.admin;
this.disabled = user.disabled;
this.expiry = user.expiry;
this.notify_telegram = user.notify_telegram;
+ this.notify_discord = user.notify_discord;
+ this.notify_email = user.notify_email;
}
asElement = (): HTMLTableRowElement => { return this._row; }
diff --git a/ts/typings/d.ts b/ts/typings/d.ts
index 7e0bf3e..f768063 100644
--- a/ts/typings/d.ts
+++ b/ts/typings/d.ts
@@ -21,6 +21,7 @@ declare interface Window {
notificationsEnabled: boolean;
emailEnabled: boolean;
telegramEnabled: boolean;
+ discordEnabled: boolean;
ombiEnabled: boolean;
usernameEnabled: boolean;
token: string;
diff --git a/views.go b/views.go
index 8841c09..241dd47 100644
--- a/views.go
+++ b/views.go
@@ -1,6 +1,7 @@
package main
import (
+ "html/template"
"io/fs"
"net/http"
"strconv"
@@ -121,6 +122,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"contactMessage": "",
"email_enabled": emailEnabled,
"telegram_enabled": telegramEnabled,
+ "discord_enabled": discordEnabled,
"notifications": notificationsEnabled,
"version": version,
"commit": commit,
@@ -284,13 +286,28 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang,
"telegramEnabled": telegramEnabled,
+ "discordEnabled": discordEnabled,
}
- if data["telegramEnabled"].(bool) {
+ if telegramEnabled {
data["telegramPIN"] = app.telegram.NewAuthToken()
data["telegramUsername"] = app.telegram.username
data["telegramURL"] = app.telegram.link
data["telegramRequired"] = app.config.Section("telegram").Key("required").MustBool(false)
}
+ if discordEnabled {
+ data["discordPIN"] = app.discord.NewAuthToken()
+ data["discordUsername"] = app.discord.username
+ data["discordRequired"] = app.config.Section("discord").Key("required").MustBool(false)
+ data["discordSendPINMessage"] = template.HTML(app.storage.lang.Form[lang].Strings.template("sendPINDiscord", tmpl{
+ "command": `` + app.config.Section("discord").Key("start_command").MustString("!start") + `
`,
+ "server_channel": app.discord.serverChannelName,
+ }))
+ }
+
+ // if discordEnabled {
+ // pin := ""
+ // for _, token := range app.discord.tokens {
+ // if
gcHTML(gc, http.StatusOK, "form-loader.html", data)
}
From 3bf722c5fe27d624313af15aa82c1ef0f1c13f06 Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Fri, 21 May 2021 21:59:50 +0100
Subject: [PATCH 05/25] Discord: send links as embeds
Kind of janky but works. This kind of messes up the layout if you write
links in-line.
---
discord.go | 54 ++++++++++++++++++++++++++++++++++++++++++------------
stripmd.go | 25 ++++++++++++++++++++-----
2 files changed, 62 insertions(+), 17 deletions(-)
diff --git a/discord.go b/discord.go
index f80d216..4f49354 100644
--- a/discord.go
+++ b/discord.go
@@ -232,19 +232,49 @@ func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, sects []s
}
func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
- for _, id := range channelID {
- msg := ""
- if message.Markdown == "" {
- msg = message.Text
- } else {
- msg = message.Markdown
+ msg := ""
+ var embeds []*dg.MessageEmbed
+ if message.Markdown != "" {
+ var links []Link
+ msg, links = StripAltText(message.Markdown, true)
+ embeds = make([]*dg.MessageEmbed, len(links))
+ for i := range links {
+ embeds[i] = &dg.MessageEmbed{
+ URL: links[i].URL,
+ Title: links[i].Alt,
+ Type: dg.EmbedTypeLink,
+ }
}
- _, err := d.bot.ChannelMessageSend(
- id,
- msg,
- )
- if err != nil {
- return err
+ } else {
+ msg = message.Text
+ }
+ for _, id := range channelID {
+ var err error
+ if len(embeds) != 0 {
+ _, err = d.bot.ChannelMessageSendComplex(
+ id,
+ &dg.MessageSend{
+ Content: msg,
+ Embed: embeds[0],
+ },
+ )
+ if err != nil {
+ return err
+ }
+ for i := 1; i < len(embeds); i++ {
+ _, err := d.bot.ChannelMessageSendEmbed(id, embeds[i])
+ if err != nil {
+ return err
+ }
+ }
+ } else {
+ _, err := d.bot.ChannelMessageSend(
+ id,
+ msg,
+ )
+ if err != nil {
+ return err
+ }
}
}
return nil
diff --git a/stripmd.go b/stripmd.go
index 0568067..2a5ea4d 100644
--- a/stripmd.go
+++ b/stripmd.go
@@ -6,18 +6,32 @@ import (
stripmd "github.com/writeas/go-strip-markdown"
)
+type Link struct {
+ Alt, URL string
+}
+
// StripAltText removes Markdown alt text from links and images and replaces them with just the URL.
// Currently uses the deepest alt text when links/images are nested.
-func StripAltText(md string) string {
+// If links = true, links are completely removed, and a list of URLs and their alt text is also returned.
+func StripAltText(md string, links bool) (string, []Link) {
altTextStart := -1 // Start of alt text (between '[' & ']')
URLStart := -1 // Start of url (between '(' & ')')
URLEnd := -1
previousURLEnd := -2
out := ""
+ embeds := []Link{}
for i := range md {
if altTextStart != -1 && URLStart != -1 && md[i] == ')' {
URLEnd = i - 1
- out += md[previousURLEnd+2:altTextStart-1] + md[URLStart:URLEnd+1]
+ out += md[previousURLEnd+2 : altTextStart-1]
+ if links {
+ embeds = append(embeds, Link{
+ URL: md[URLStart : URLEnd+1],
+ Alt: md[altTextStart : URLStart-2],
+ })
+ } else {
+ out += md[URLStart : URLEnd+1]
+ }
previousURLEnd = URLEnd
altTextStart, URLStart, URLEnd = -1, -1, -1
continue
@@ -36,11 +50,12 @@ func StripAltText(md string) string {
out += md[previousURLEnd+2:]
}
if out == "" {
- return md
+ return md, embeds
}
- return out
+ return out, embeds
}
func stripMarkdown(md string) string {
- return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(StripAltText(md)), "
"), "")
+ stripped, _ := StripAltText(md, false)
+ return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(stripped), "
"), "")
}
From a6447165b775a4e7645a83cd79bbcedb51647da2 Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Fri, 21 May 2021 22:46:46 +0100
Subject: [PATCH 06/25] add email notify enable/disable; remove (de)hyphening
hyphen/dehyphen conflicted with new migration for email contact
preference, and it's been a while since this has been an issue so i've
just commented it out for now.
---
api.go | 46 +++++++----
config.go | 25 ++++++
email.go | 6 +-
main.go | 143 +++++++++++++++++-----------------
models.go | 2 +-
router.go | 4 +-
storage.go | 172 +++++++++++++++++++++--------------------
ts/modules/accounts.ts | 6 +-
8 files changed, 229 insertions(+), 175 deletions(-)
diff --git a/api.go b/api.go
index 31d4816..141c951 100644
--- a/api.go
+++ b/api.go
@@ -217,7 +217,7 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
username := jfUser.Name
email := ""
if e, ok := app.storage.emails[jfID]; ok {
- email = e.(string)
+ email = e.Addr
}
for _, ombiUser := range ombiUsers {
ombiAddr := ""
@@ -283,7 +283,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
}
app.jf.CacheExpiry = time.Now()
if emailEnabled {
- app.storage.emails[id] = req.Email
+ app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
app.storage.storeEmails()
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
@@ -478,7 +478,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
}
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
if req.Email != "" {
- app.storage.emails[id] = req.Email
+ app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
app.storage.storeEmails()
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
@@ -908,8 +908,8 @@ func (app *appContext) GetInvites(gc *gin.Context) {
var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
app.storage.loadEmails()
- if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil {
- address = addr.(string)
+ if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
+ address = addr.Addr
}
} else {
address = app.config.Section("ui").Key("email").String()
@@ -1108,14 +1108,14 @@ func (app *appContext) SetNotify(gc *gin.Context) {
}
var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
- var ok bool
- address, ok = app.storage.emails[gc.GetString("jfId")].(string)
+ addr, ok := app.storage.emails[gc.GetString("jfId")]
if !ok {
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
respond(500, "Missing user email", gc)
return
}
+ address = addr.Addr
} else {
address = app.config.Section("ui").Key("email").String()
}
@@ -1202,7 +1202,7 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.LastActive = jfUser.LastActivityDate.Unix()
}
if email, ok := app.storage.emails[jfUser.ID]; ok {
- user.Email = email.(string)
+ user.Email = email.Addr
user.NotifyThroughEmail = user.Email != ""
}
expiry, ok := app.storage.users[jfUser.ID]
@@ -1293,7 +1293,11 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
for _, jfUser := range users {
id := jfUser.ID
if address, ok := req[id]; ok {
- app.storage.emails[id] = address
+ contact := true
+ if oldAddr, ok := app.storage.emails[id]; ok {
+ contact = oldAddr.Contact
+ }
+ app.storage.emails[id] = EmailAddress{Addr: address, Contact: contact}
if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(id)
if code == 200 && err == nil {
@@ -2052,7 +2056,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
// @Security Bearer
// @tags Other
func (app *appContext) SetContactMethods(gc *gin.Context) {
- var req telegramNotifyDTO
+ var req SetContactMethodsDTO
gc.BindJSON(&req)
if req.ID == "" {
respondBool(400, false, gc)
@@ -2068,9 +2072,9 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
}
msg := ""
if !req.Telegram {
- msg = "not"
+ msg = " not"
}
- app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg)
+ app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
}
if dcUser, ok := app.storage.discord[req.ID]; ok {
dcUser.Contact = req.Discord
@@ -2082,9 +2086,23 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
}
msg := ""
if !req.Discord {
- msg = "not"
+ msg = " not"
}
- app.debug.Printf("Discord: User \"%s\" will %s be notified through Discord.", dcUser.Username, msg)
+ app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
+ }
+ if email, ok := app.storage.emails[req.ID]; ok {
+ email.Contact = req.Email
+ app.storage.emails[req.ID] = email
+ if err := app.storage.storeEmails(); err != nil {
+ respondBool(500, false, gc)
+ app.err.Printf("Failed to store emails: %v", err)
+ return
+ }
+ msg := ""
+ if !req.Email {
+ msg = " not"
+ }
+ app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
}
respondBool(200, true, gc)
}
diff --git a/config.go b/config.go
index 9e18bba..d605ef5 100644
--- a/config.go
+++ b/config.go
@@ -171,3 +171,28 @@ func (app *appContext) migrateEmailConfig() {
}
app.loadConfig()
}
+
+func (app *appContext) migrateEmailStorage() error {
+ var emails map[string]interface{}
+ err := loadJSON(app.storage.emails_path, &emails)
+ if err != nil {
+ return err
+ }
+ newEmails := map[string]EmailAddress{}
+ for jfID, addr := range emails {
+ newEmails[jfID] = EmailAddress{
+ Addr: addr.(string),
+ Contact: true,
+ }
+ }
+ err = storeJSON(app.storage.emails_path+".bak", emails)
+ if err != nil {
+ return err
+ }
+ err = storeJSON(app.storage.emails_path, newEmails)
+ if err != nil {
+ return err
+ }
+ app.info.Println("Migrated to new email format. A backup has also been made.")
+ return nil
+}
diff --git a/email.go b/email.go
index 5f752fd..8d4399b 100644
--- a/email.go
+++ b/email.go
@@ -817,8 +817,8 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
return err
}
}
- if address, ok := app.storage.emails[id]; ok {
- err = app.email.send(email, address.(string))
+ if address, ok := app.storage.emails[id]; ok && address.Contact && emailEnabled {
+ err = app.email.send(email, address.Addr)
if err != nil {
return err
}
@@ -838,7 +838,7 @@ func (app *appContext) getAddressOrName(jfID string) string {
return "@" + tgChat.Username
}
if addr, ok := app.storage.emails[jfID]; ok {
- return addr.(string)
+ return addr.Addr
}
return ""
}
diff --git a/main.go b/main.go
index a16d561..b2b99cc 100644
--- a/main.go
+++ b/main.go
@@ -16,7 +16,6 @@ import (
"os/signal"
"path/filepath"
"runtime"
- "strconv"
"strings"
"time"
@@ -321,6 +320,10 @@ func start(asDaemon, firstCall bool) {
app.storage.emails_path = app.config.Section("files").Key("emails").String()
if err := app.storage.loadEmails(); err != nil {
app.err.Printf("Failed to load Emails: %v", err)
+ err := app.migrateEmailStorage()
+ if err != nil {
+ app.err.Printf("Failed to migrate Email storage: %v", err)
+ }
}
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
if err := app.storage.loadPolicy(); err != nil {
@@ -434,76 +437,76 @@ func start(asDaemon, firstCall bool) {
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err)
}
app.info.Printf("Authenticated with %s", server)
- /* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs.
- This checks if the version is equal or higher. */
- checkVersion := func(version string) int {
- numberStrings := strings.Split(version, ".")
- n := 0
- for _, s := range numberStrings {
- num, err := strconv.Atoi(s)
- if err == nil {
- n += num
- }
- }
- return n
- }
- if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") {
- // Get users to check if server uses hyphenated userIDs
- app.jf.GetUsers(false)
+ // /* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs.
+ // This checks if the version is equal or higher. */
+ // checkVersion := func(version string) int {
+ // numberStrings := strings.Split(version, ".")
+ // n := 0
+ // for _, s := range numberStrings {
+ // num, err := strconv.Atoi(s)
+ // if err == nil {
+ // n += num
+ // }
+ // }
+ // return n
+ // }
+ // if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") {
+ // // Get users to check if server uses hyphenated userIDs
+ // app.jf.GetUsers(false)
- noHyphens := true
- for id := range app.storage.emails {
- if strings.Contains(id, "-") {
- noHyphens = false
- break
- }
- }
- if noHyphens == app.jf.Hyphens {
- var newEmails map[string]interface{}
- var newUsers map[string]time.Time
- var status, status2 int
- var err, err2 error
- if app.jf.Hyphens {
- app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
- time.Sleep(time.Second * time.Duration(3))
- newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
- newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
- } else {
- app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
- time.Sleep(time.Second * time.Duration(3))
- newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
- newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
- }
- if status != 200 || err != nil {
- app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
- app.err.Fatalf("Couldn't upgrade emails.json")
- }
- if status2 != 200 || err2 != nil {
- app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
- app.err.Fatalf("Couldn't upgrade users.json")
- }
- emailBakFile := app.storage.emails_path + ".bak"
- usersBakFile := app.storage.users_path + ".bak"
- err = storeJSON(emailBakFile, app.storage.emails)
- err2 = storeJSON(usersBakFile, app.storage.users)
- if err != nil {
- app.err.Fatalf("couldn't store emails.json backup: %v", err)
- }
- if err2 != nil {
- app.err.Fatalf("couldn't store users.json backup: %v", err)
- }
- app.storage.emails = newEmails
- app.storage.users = newUsers
- err = app.storage.storeEmails()
- err2 = app.storage.storeUsers()
- if err != nil {
- app.err.Fatalf("couldn't store emails.json: %v", err)
- }
- if err2 != nil {
- app.err.Fatalf("couldn't store users.json: %v", err)
- }
- }
- }
+ // noHyphens := true
+ // for id := range app.storage.emails {
+ // if strings.Contains(id, "-") {
+ // noHyphens = false
+ // break
+ // }
+ // }
+ // if noHyphens == app.jf.Hyphens {
+ // var newEmails map[string]interface{}
+ // var newUsers map[string]time.Time
+ // var status, status2 int
+ // var err, err2 error
+ // if app.jf.Hyphens {
+ // app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
+ // time.Sleep(time.Second * time.Duration(3))
+ // newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
+ // newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
+ // } else {
+ // app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
+ // time.Sleep(time.Second * time.Duration(3))
+ // newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
+ // newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
+ // }
+ // if status != 200 || err != nil {
+ // app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
+ // app.err.Fatalf("Couldn't upgrade emails.json")
+ // }
+ // if status2 != 200 || err2 != nil {
+ // app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
+ // app.err.Fatalf("Couldn't upgrade users.json")
+ // }
+ // emailBakFile := app.storage.emails_path + ".bak"
+ // usersBakFile := app.storage.users_path + ".bak"
+ // err = storeJSON(emailBakFile, app.storage.emails)
+ // err2 = storeJSON(usersBakFile, app.storage.users)
+ // if err != nil {
+ // app.err.Fatalf("couldn't store emails.json backup: %v", err)
+ // }
+ // if err2 != nil {
+ // app.err.Fatalf("couldn't store users.json backup: %v", err)
+ // }
+ // app.storage.emails = newEmails
+ // app.storage.users = newUsers
+ // err = app.storage.storeEmails()
+ // err2 = app.storage.storeUsers()
+ // if err != nil {
+ // app.err.Fatalf("couldn't store emails.json: %v", err)
+ // }
+ // if err2 != nil {
+ // app.err.Fatalf("couldn't store users.json: %v", err)
+ // }
+ // }
+ // }
// Auth (manual user/pass or jellyfin)
app.jellyfinLogin = true
diff --git a/models.go b/models.go
index c79cab4..d018738 100644
--- a/models.go
+++ b/models.go
@@ -255,7 +255,7 @@ type telegramSetDTO struct {
ID string `json:"id"` // Jellyfin ID of user.
}
-type telegramNotifyDTO struct {
+type SetContactMethodsDTO struct {
ID string `json:"id"`
Email bool `json:"email"`
Discord bool `json:"discord"`
diff --git a/router.go b/router.go
index 4bad4d8..8dcf793 100644
--- a/router.go
+++ b/router.go
@@ -161,10 +161,12 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart)
- if telegramEnabled {
+ if telegramEnabled || discordEnabled {
api.GET(p+"/telegram/pin", app.TelegramGetPin)
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
api.POST(p+"/users/telegram", app.TelegramAddUser)
+ }
+ if discordEnabled || telegramEnabled {
api.POST(p+"/users/contact", app.SetContactMethods)
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
diff --git a/storage.go b/storage.go
index b0f0ee8..a190961 100644
--- a/storage.go
+++ b/storage.go
@@ -21,7 +21,8 @@ type Storage struct {
invites Invites
profiles map[string]Profile
defaultProfile string
- emails, displayprefs, ombi_template map[string]interface{}
+ displayprefs, ombi_template map[string]interface{}
+ emails map[string]EmailAddress
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
customEmails customEmails
@@ -47,6 +48,11 @@ type DiscordUser struct {
Contact bool
}
+type EmailAddress struct {
+ Addr string
+ Contact bool
+}
+
type customEmails struct {
UserCreated customEmail `json:"userCreated"`
InviteExpiry customEmail `json:"inviteExpiry"`
@@ -902,85 +908,85 @@ func storeJSON(path string, obj interface{}) error {
return err
}
-// One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage.
-
-func hyphenate(userID string) string {
- if userID[8] == '-' {
- return userID
- }
- return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
-}
-
-func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
- jfUsers, status, err := app.jf.GetUsers(false)
- if status != 200 || err != nil {
- return nil, status, err
- }
- newEmails := map[string]interface{}{}
- for _, user := range jfUsers {
- unHyphenated := user.ID
- hyphenated := hyphenate(unHyphenated)
- val, ok := old[hyphenated]
- if ok {
- newEmails[unHyphenated] = val
- }
- }
- return newEmails, status, err
-}
-
-func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
- jfUsers, status, err := app.jf.GetUsers(false)
- if status != 200 || err != nil {
- return nil, status, err
- }
- newEmails := map[string]interface{}{}
- for _, user := range jfUsers {
- unstripped := user.ID
- stripped := strings.ReplaceAll(unstripped, "-", "")
- val, ok := old[stripped]
- if ok {
- newEmails[unstripped] = val
- }
- }
- return newEmails, status, err
-}
-
-func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
- return app.hyphenateStorage(old)
-}
-
-func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
- return app.deHyphenateStorage(old)
-}
-
-func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
- asInterface := map[string]interface{}{}
- for k, v := range old {
- asInterface[k] = v
- }
- fixed, status, err := app.hyphenateStorage(asInterface)
- if err != nil {
- return nil, status, err
- }
- out := map[string]time.Time{}
- for k, v := range fixed {
- out[k] = v.(time.Time)
- }
- return out, status, err
-}
-
-func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
- asInterface := map[string]interface{}{}
- for k, v := range old {
- asInterface[k] = v
- }
- fixed, status, err := app.deHyphenateStorage(asInterface)
- if err != nil {
- return nil, status, err
- }
- out := map[string]time.Time{}
- for k, v := range fixed {
- out[k] = v.(time.Time)
- }
- return out, status, err
-}
+// // One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage.
+//
+// func hyphenate(userID string) string {
+// if userID[8] == '-' {
+// return userID
+// }
+// return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
+// }
+//
+// func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
+// jfUsers, status, err := app.jf.GetUsers(false)
+// if status != 200 || err != nil {
+// return nil, status, err
+// }
+// newEmails := map[string]interface{}{}
+// for _, user := range jfUsers {
+// unHyphenated := user.ID
+// hyphenated := hyphenate(unHyphenated)
+// val, ok := old[hyphenated]
+// if ok {
+// newEmails[unHyphenated] = val
+// }
+// }
+// return newEmails, status, err
+// }
+//
+// func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
+// jfUsers, status, err := app.jf.GetUsers(false)
+// if status != 200 || err != nil {
+// return nil, status, err
+// }
+// newEmails := map[string]interface{}{}
+// for _, user := range jfUsers {
+// unstripped := user.ID
+// stripped := strings.ReplaceAll(unstripped, "-", "")
+// val, ok := old[stripped]
+// if ok {
+// newEmails[unstripped] = val
+// }
+// }
+// return newEmails, status, err
+// }
+//
+// func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
+// return app.hyphenateStorage(old)
+// }
+//
+// func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
+// return app.deHyphenateStorage(old)
+// }
+//
+// func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
+// asInterface := map[string]interface{}{}
+// for k, v := range old {
+// asInterface[k] = v
+// }
+// fixed, status, err := app.hyphenateStorage(asInterface)
+// if err != nil {
+// return nil, status, err
+// }
+// out := map[string]time.Time{}
+// for k, v := range fixed {
+// out[k] = v.(time.Time)
+// }
+// return out, status, err
+// }
+//
+// func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
+// asInterface := map[string]interface{}{}
+// for k, v := range old {
+// asInterface[k] = v
+// }
+// fixed, status, err := app.deHyphenateStorage(asInterface)
+// if err != nil {
+// return nil, status, err
+// }
+// out := map[string]time.Time{}
+// for k, v := range fixed {
+// out[k] = v.(time.Time)
+// }
+// return out, status, err
+// }
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index 21f19ef..9b53504 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -228,18 +228,18 @@ class user implements User {
${window.lang.strings("contactThrough")}
`;
if (window.telegramEnabled && this._telegramUsername != "") {
innerHTML += `
`;
From 35d407afef090b0e0db29ef388d48911672887b6 Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Sat, 22 May 2021 15:31:25 +0100
Subject: [PATCH 07/25] Discord: remove @ from username
---
ts/modules/accounts.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index 9b53504..b8473e2 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -221,7 +221,7 @@ class user implements User {
// (this._discord.querySelector("span") as HTMLSpanElement).onclick = this._addDiscord;
} else {
let innerHTML = `
- @${u}
+ ${u}
From 591e3c5ca1dc1467af701c85052976d5cba3f3ab Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Sat, 22 May 2021 15:32:51 +0100
Subject: [PATCH 08/25] Discord: embed images
![alt](image link) is now converted to an image embed.
---
api.go | 2 +-
discord.go | 19 ++-----------------
stripmd.go | 23 +++++++++++++++++------
3 files changed, 20 insertions(+), 24 deletions(-)
diff --git a/api.go b/api.go
index 141c951..f42f715 100644
--- a/api.go
+++ b/api.go
@@ -2048,7 +2048,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
// @Summary Sets whether to notify a user through telegram or not.
// @Produce json
-// @Param telegramNotifyDTO body telegramNotifyDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
+// @Param SetContactMethodDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
// @Success 200 {object} boolResponse
// @Success 400 {object} boolResponse
// @Success 500 {object} boolResponse
diff --git a/discord.go b/discord.go
index 4f49354..4c44931 100644
--- a/discord.go
+++ b/discord.go
@@ -41,6 +41,7 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
for _, user := range app.storage.discord {
dd.users[user.ID] = user
}
+
return dd, nil
}
@@ -91,13 +92,6 @@ func (d *DiscordDaemon) run() {
return
}
-func (d *DiscordDaemon) Shutdown() {
- d.Stopped = true
- d.ShutdownChannel <- "Down"
- <-d.ShutdownChannel
- close(d.ShutdownChannel)
-}
-
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
if m.GuildID != "" && d.channelName != "" {
if d.channelID == "" {
@@ -235,16 +229,7 @@ func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
msg := ""
var embeds []*dg.MessageEmbed
if message.Markdown != "" {
- var links []Link
- msg, links = StripAltText(message.Markdown, true)
- embeds = make([]*dg.MessageEmbed, len(links))
- for i := range links {
- embeds[i] = &dg.MessageEmbed{
- URL: links[i].URL,
- Title: links[i].Alt,
- Type: dg.EmbedTypeLink,
- }
- }
+ msg, embeds = StripAltText(message.Markdown, true)
} else {
msg = message.Text
}
diff --git a/stripmd.go b/stripmd.go
index 2a5ea4d..caed8ea 100644
--- a/stripmd.go
+++ b/stripmd.go
@@ -3,6 +3,7 @@ package main
import (
"strings"
+ dg "github.com/bwmarrin/discordgo"
stripmd "github.com/writeas/go-strip-markdown"
)
@@ -13,22 +14,32 @@ type Link struct {
// StripAltText removes Markdown alt text from links and images and replaces them with just the URL.
// Currently uses the deepest alt text when links/images are nested.
// If links = true, links are completely removed, and a list of URLs and their alt text is also returned.
-func StripAltText(md string, links bool) (string, []Link) {
+func StripAltText(md string, links bool) (string, []*dg.MessageEmbed) {
altTextStart := -1 // Start of alt text (between '[' & ']')
URLStart := -1 // Start of url (between '(' & ')')
URLEnd := -1
previousURLEnd := -2
out := ""
- embeds := []Link{}
+ embeds := []*dg.MessageEmbed{}
for i := range md {
if altTextStart != -1 && URLStart != -1 && md[i] == ')' {
URLEnd = i - 1
out += md[previousURLEnd+2 : altTextStart-1]
if links {
- embeds = append(embeds, Link{
- URL: md[URLStart : URLEnd+1],
- Alt: md[altTextStart : URLStart-2],
- })
+ embed := &dg.MessageEmbed{
+ Type: dg.EmbedTypeLink,
+ Title: md[altTextStart : URLStart-2],
+ }
+ if md[altTextStart-1] == '!' {
+ embed.Title = md[altTextStart+1 : URLStart-2]
+ embed.Type = dg.EmbedTypeImage
+ embed.Image = &dg.MessageEmbedImage{
+ URL: md[URLStart : URLEnd+1],
+ }
+ } else {
+ embed.URL = md[URLStart : URLEnd+1]
+ }
+ embeds = append(embeds, embed)
} else {
out += md[URLStart : URLEnd+1]
}
From 9fac79b1f0c6ba96e277b19c2f57c8268dd21748 Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Sat, 22 May 2021 21:42:15 +0100
Subject: [PATCH 09/25] Discord: Add users via accounts tab
Doesn't require a PIN like Telegram, as we can access a list of guild
users with the GuildMembers intent set. This has to be enabled under
Bot > Priviliged Gateway intents on the developer portal.
---
api.go | 59 ++++++++++++++++++++++++++++++--
css/base.css | 23 +++++++++++++
discord.go | 56 +++++++++++++++++++++++++++++-
html/admin.html | 12 +++++++
lang/admin/en-us.json | 5 ++-
models.go | 15 ++++++++
router.go | 6 ++--
ts/admin.ts | 4 +++
ts/modules/accounts.ts | 78 +++++++++++++++++++++++++++++++++++++++++-
ts/typings/d.ts | 1 +
10 files changed, 252 insertions(+), 7 deletions(-)
diff --git a/api.go b/api.go
index f42f715..b3d0879 100644
--- a/api.go
+++ b/api.go
@@ -2048,7 +2048,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
// @Summary Sets whether to notify a user through telegram or not.
// @Produce json
-// @Param SetContactMethodDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
+// @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
// @Success 200 {object} boolResponse
// @Success 400 {object} boolResponse
// @Success 500 {object} boolResponse
@@ -2164,7 +2164,7 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
// @Produce json
// @Success 200 {object} boolResponse
-// @Success 401 {object} boolResponse
+// @Failure 401 {object} boolResponse
// @Param pin path string true "PIN code to check"
// @Param invCode path string true "invite Code"
// @Router /invite/{invCode}/discord/verified/{pin} [get]
@@ -2180,6 +2180,61 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
respondBool(200, ok, gc)
}
+// @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
+// @Produce json
+// @Success 200 {object} DiscordUsersDTO
+// @Failure 400 {object} boolResponse
+// @Failure 500 {object} boolResponse
+// @Param username path string true "username to search."
+// @Router /users/discord/{username} [get]
+// @tags Other
+func (app *appContext) DiscordGetUsers(gc *gin.Context) {
+ name := gc.Param("username")
+ if name == "" {
+ respondBool(400, false, gc)
+ return
+ }
+ users := app.discord.GetUsers(name)
+ resp := DiscordUsersDTO{Users: make([]DiscordUserDTO, len(users))}
+ for i, u := range users {
+ resp.Users[i] = DiscordUserDTO{
+ Name: u.User.Username + "#" + u.User.Discriminator,
+ ID: u.User.ID,
+ AvatarURL: u.User.AvatarURL("32"),
+ }
+ }
+ gc.JSON(200, resp)
+}
+
+// @Summary Links a Discord account to a Jellyfin account via user IDs. Notifications are turned on by default.
+// @Produce json
+// @Success 200 {object} boolResponse
+// @Failure 400 {object} boolResponse
+// @Failure 500 {object} boolResponse
+// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
+// @Router /users/discord [post]
+// @tags Other
+func (app *appContext) DiscordConnect(gc *gin.Context) {
+ var req DiscordConnectUserDTO
+ gc.BindJSON(&req)
+ if req.JellyfinID == "" || req.DiscordID == "" {
+ respondBool(400, false, gc)
+ return
+ }
+ user, ok := app.discord.NewUser(req.DiscordID)
+ if !ok {
+ respondBool(500, false, gc)
+ return
+ }
+ app.storage.discord[req.JellyfinID] = user
+ if err := app.storage.storeDiscordUsers(); err != nil {
+ app.err.Printf("Failed to store Discord users: %v", err)
+ respondBool(500, false, gc)
+ return
+ }
+ respondBool(200, true, gc)
+}
+
// @Summary Restarts the program. No response means success.
// @Router /restart [post]
// @Security Bearer
diff --git a/css/base.css b/css/base.css
index 3b9626d..c4889b6 100644
--- a/css/base.css
+++ b/css/base.css
@@ -130,6 +130,10 @@ div.card:contains(section.banner.footer) {
text-align: center;
}
+.w-100 {
+ width: 100%;
+}
+
.inline-block {
display: inline-block;
}
@@ -483,3 +487,22 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
max-width: 15rem;
min-width: 10rem;
}
+
+td.img-circle {
+ width: 32px;
+ height: 32px;
+}
+
+span.shield.img-circle {
+ padding: 0.2rem;
+}
+
+img.img-circle {
+ border-radius: 50%;
+ vertical-align: middle;
+}
+
+.table td.sm {
+ padding-top: 0.1rem;
+ padding-bottom: 0.1rem;
+}
diff --git a/discord.go b/discord.go
index 4c44931..3d92ead 100644
--- a/discord.go
+++ b/discord.go
@@ -71,7 +71,7 @@ func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string)
func (d *DiscordDaemon) run() {
d.bot.AddHandler(d.messageHandler)
- d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages
+ d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers
if err := d.bot.Open(); err != nil {
d.app.err.Printf("Discord: Failed to start daemon: %v", err)
return
@@ -92,6 +92,60 @@ func (d *DiscordDaemon) run() {
return
}
+// Returns the user(s) roughly corresponding to the username (if they are in the guild).
+// if no discriminator (#xxxx) is given in the username and there are multiple corresponding users, a list of all matching users is returned.
+func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
+ members, err := d.bot.GuildMembers(
+ d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID,
+ "",
+ 1000,
+ )
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to get members: %v", err)
+ return nil
+ }
+ hasDiscriminator := strings.Contains(username, "#")
+ var users []*dg.Member
+ for _, member := range members {
+ if !hasDiscriminator {
+ userSplit := strings.Split(member.User.Username, "#")
+ if strings.Contains(userSplit[0], username) {
+ users = append(users, member)
+ }
+ } else if strings.Contains(member.User.Username, username) {
+ return nil
+ }
+ }
+ return users
+}
+
+func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
+ u, err := d.bot.User(ID)
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to get user: %v", err)
+ return
+ }
+ user.ID = ID
+ user.Username = u.Username
+ user.Contact = true
+ user.Discriminator = u.Discriminator
+ channel, err := d.bot.UserChannelCreate(ID)
+ if err != nil {
+ d.app.err.Printf("Discord: Failed to create DM channel: %v", err)
+ return
+ }
+ user.ChannelID = channel.ID
+ ok = true
+ return
+}
+
+func (d *DiscordDaemon) Shutdown() {
+ d.Stopped = true
+ d.ShutdownChannel <- "Down"
+ <-d.ShutdownChannel
+ close(d.ShutdownChannel)
+}
+
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
if m.GuildID != "" && d.channelName != "" {
if d.channelID == "" {
diff --git a/html/admin.html b/html/admin.html
index 6286144..bc0f80d 100644
--- a/html/admin.html
+++ b/html/admin.html
@@ -328,6 +328,18 @@
{{ end }}
+ {{ if .discord_enabled }}
+
+
+
{{ .strings.linkDiscord }}×
+
{{ .strings.searchDiscordUser }}
+
+
+
+
+
+
+ {{ end }}
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json
index 8fb42d1..51d809c 100644
--- a/lang/admin/en-us.json
+++ b/lang/admin/en-us.json
@@ -20,6 +20,7 @@
"create": "Create",
"apply": "Apply",
"delete": "Delete",
+ "add": "Add",
"name": "Name",
"date": "Date",
"enabled": "Enabled",
@@ -94,7 +95,8 @@
"notifyEvent": "Notify on:",
"notifyInviteExpiry": "On expiry",
"notifyUserCreation": "On user creation",
- "sendPIN": "Ask the user to send the PIN below to the bot."
+ "sendPIN": "Ask the user to send the PIN below to the bot.",
+ "searchDiscordUser": "Start typing the Discord username to link it."
},
"notifications": {
"changedEmailAddress": "Changed email address of {n}.",
@@ -107,6 +109,7 @@
"updateApplied": "Update applied, please restart.",
"updateAppliedRefresh": "Update applied, please refresh.",
"telegramVerified": "Telegram account verified.",
+ "accountConnected": "Account connected.",
"errorConnection": "Couldn't connect to jfa-go.",
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
diff --git a/models.go b/models.go
index d018738..3641eb1 100644
--- a/models.go
+++ b/models.go
@@ -261,3 +261,18 @@ type SetContactMethodsDTO struct {
Discord bool `json:"discord"`
Telegram bool `json:"telegram"`
}
+
+type DiscordUserDTO struct {
+ Name string `json:"name"`
+ AvatarURL string `json:"avatar_url"`
+ ID string `json:"id"`
+}
+
+type DiscordUsersDTO struct {
+ Users []DiscordUserDTO `json:"users"`
+}
+
+type DiscordConnectUserDTO struct {
+ JellyfinID string `json:"jf_id"`
+ DiscordID string `json:"discord_id"`
+}
diff --git a/router.go b/router.go
index 8dcf793..38e3c97 100644
--- a/router.go
+++ b/router.go
@@ -165,10 +165,12 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/telegram/pin", app.TelegramGetPin)
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
api.POST(p+"/users/telegram", app.TelegramAddUser)
- }
- if discordEnabled || telegramEnabled {
api.POST(p+"/users/contact", app.SetContactMethods)
}
+ if discordEnabled {
+ api.GET(p+"/users/discord/:username", app.DiscordGetUsers)
+ api.POST(p+"/users/discord", app.DiscordConnect)
+ }
if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET(p+"/ombi/users", app.OmbiUsers)
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)
diff --git a/ts/admin.ts b/ts/admin.ts
index 6c14be7..edfb009 100644
--- a/ts/admin.ts
+++ b/ts/admin.ts
@@ -66,6 +66,10 @@ window.availableProfiles = window.availableProfiles || [];
if (window.telegramEnabled) {
window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
}
+
+ if (window.discordEnabled) {
+ window.modals.discord = new Modal(document.getElementById("modal-discord"));
+ }
})();
var inviteCreator = new createInvite();
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index b8473e2..d969777 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -24,6 +24,12 @@ interface getPinResponse {
username: string;
}
+interface DiscordUser {
+ name: string;
+ avatar_url: string;
+ id: string;
+}
+
class user implements User {
private _row: HTMLTableRowElement;
private _check: HTMLInputElement;
@@ -218,7 +224,7 @@ class user implements User {
this._discordUsername = u;
if (u == "") {
this._discord.innerHTML = `Add`;
- // (this._discord.querySelector("span") as HTMLSpanElement).onclick = this._addDiscord;
+ (this._discord.querySelector("span") as HTMLSpanElement).onclick = this._addDiscord;
} else {
let innerHTML = `
${u}
@@ -401,6 +407,76 @@ class user implements User {
}
});
}
+
+ private _timer: NodeJS.Timer;
+
+ private _discordKbListener = () => {
+ clearTimeout(this._timer);
+ const list = document.getElementById("discord-list") as HTMLTableElement;
+ const input = document.getElementById("discord-search") as HTMLInputElement;
+ if (input.value.length < 2) {
+ return;
+ }
+ list.innerHTML = ``;
+ addLoader(list);
+ list.parentElement.classList.add("mb-1", "mt-1");
+ this._timer = setTimeout(() => {
+ _get("/users/discord/" + input.value, null, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ if (req.status != 200) {
+ removeLoader(list);
+ list.parentElement.classList.remove("mb-1", "mt-1");
+ return;
+ }
+ const users = req.response["users"] as Array;
+ let innerHTML = ``;
+ for (let i = 0; i < users.length; i++) {
+ innerHTML += `
+
+
+
+ |
+
+ ${users[i].name}
+ |
+
+ ${window.lang.strings("add")}
+ |
+
+ `;
+ }
+ list.innerHTML = innerHTML;
+ removeLoader(list);
+ list.parentElement.classList.remove("mb-1", "mt-1");
+ for (let i = 0; i < users.length; i++) {
+ const button = document.getElementById(`discord-user-${users[i].id}`) as HTMLInputElement;
+ button.onclick = () => _post("/users/discord", {jf_id: this.id, discord_id: users[i].id}, (req: XMLHttpRequest) => {
+ if (req.readyState == 4) {
+ document.dispatchEvent(new CustomEvent("accounts-reload"));
+ if (req.status != 200) {
+ window.notifications.customError("errorConnectDiscord", window.lang.notif("errorFailureCheckLogs"));
+ return
+ }
+ window.notifications.customSuccess("discordConnected", window.lang.notif("accountConnected"));
+ window.modals.discord.close()
+ }
+ });
+ }
+ }
+ });
+ }, 750);
+ }
+
+ private _addDiscord = () => {
+ if (!window.discordEnabled) { return; }
+ const input = document.getElementById("discord-search") as HTMLInputElement;
+ const list = document.getElementById("discord-list") as HTMLDivElement;
+ list.innerHTML = ``;
+ input.value = "";
+ input.removeEventListener("keyup", this._discordKbListener);
+ input.addEventListener("keyup", this._discordKbListener);
+ window.modals.discord.show();
+ }
private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 200) {
diff --git a/ts/typings/d.ts b/ts/typings/d.ts
index f768063..ba40de0 100644
--- a/ts/typings/d.ts
+++ b/ts/typings/d.ts
@@ -102,6 +102,7 @@ declare interface Modals {
extendExpiry: Modal;
updateInfo: Modal;
telegram: Modal;
+ discord: Modal;
}
interface Invite {
From b3ce7acfcb033a7b065a3fcef4099c12511ad908 Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Sat, 22 May 2021 23:02:16 +0100
Subject: [PATCH 10/25] Accounts: Always inline icons, only one settings cog
Admin chip, email edit bot and contact method cog icon are now always inline.
Only one cog icon is shown now.
---
css/base.css | 10 +++-
ts/modules/accounts.ts | 128 +++++++++++++++++++++--------------------
2 files changed, 74 insertions(+), 64 deletions(-)
diff --git a/css/base.css b/css/base.css
index c4889b6..d40e6f9 100644
--- a/css/base.css
+++ b/css/base.css
@@ -30,12 +30,12 @@
}
}
-@media screen and (max-width: 750px) {
+@media screen and (max-width: 1000px) {
:root {
font-size: 0.9rem;
}
.table-responsive table {
- min-width: 660px;
+ min-width: 800px;
}
}
@@ -428,6 +428,7 @@ p.top {
.table-responsive {
overflow-x: auto;
+ font-size: 0.9rem;
}
#notification-box {
@@ -506,3 +507,8 @@ img.img-circle {
padding-top: 0.1rem;
padding-bottom: 0.1rem;
}
+
+.table-inline {
+ display: flex !important;
+ align-items: center;
+}
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index d969777..c685465 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -102,7 +102,9 @@ class user implements User {
this._notifyEmail = s;
if (window.telegramEnabled && this._telegramUsername != "") {
const email = this._telegram.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
- email.checked = s;
+ if (email) {
+ email.checked = s;
+ }
}
if (window.discordEnabled && this._discordUsername != "") {
const email = this._discord.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
@@ -120,52 +122,50 @@ class user implements User {
} else {
let innerHTML = `
@${u}
-
-
-
-
- `;
- this._discord.innerHTML = innerHTML;
- // Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
- const button = this._telegram.querySelector("i");
- const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
- const checks = this._telegram.querySelectorAll("input") as NodeListOf
;
- for (let i = 0; i < checks.length; i++) {
- checks[i].onclick = () => this._setNotifyMethod("telegram");
+ `;
}
-
- button.onclick = () => {
- dropdown.classList.add("selected");
- document.addEventListener("click", outerClickListener);
- };
- const outerClickListener = (event: Event) => {
- if (!(event.target instanceof HTMLElement && (this._telegram.contains(event.target) || button.contains(event.target)))) {
- dropdown.classList.remove("selected");
- document.removeEventListener("click", outerClickListener);
+ this._telegram.innerHTML = innerHTML;
+ if (!window.discordEnabled || this._discordUsername == "") {
+ // Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
+ const button = this._telegram.querySelector("i");
+ const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
+ const checks = this._telegram.querySelectorAll("input") as NodeListOf;
+ for (let i = 0; i < checks.length; i++) {
+ checks[i].onclick = () => this._setNotifyMethod("telegram");
}
- };
+
+ button.onclick = () => {
+ dropdown.classList.add("selected");
+ document.addEventListener("click", outerClickListener);
+ };
+ const outerClickListener = (event: Event) => {
+ if (!(event.target instanceof HTMLElement && (this._telegram.contains(event.target) || button.contains(event.target)))) {
+ dropdown.classList.remove("selected");
+ document.removeEventListener("click", outerClickListener);
+ }
+ };
+ }
}
}
@@ -174,7 +174,9 @@ class user implements User {
if (!window.telegramEnabled || !this._telegramUsername) return;
this._notifyTelegram = s;
const telegram = this._telegram.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
- telegram.checked = s;
+ if (telegram) {
+ telegram.checked = s;
+ }
if (window.discordEnabled && this._discordUsername != "") {
const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
telegram.checked = s;
@@ -227,30 +229,32 @@ class user implements User {
(this._discord.querySelector("span") as HTMLSpanElement).onclick = this._addDiscord;
} else {
let innerHTML = `
- ${u}
-
-
-
-
-
${window.lang.strings("contactThrough")}
-
-
+
@@ -323,8 +327,8 @@ class user implements User {
this._row = document.createElement("tr") as HTMLTableRowElement;
let innerHTML = `
|
-
|
-
|
+
|
+
|
`;
if (window.telegramEnabled) {
innerHTML += `
From e98c9b46f106dea71b8fe183952789e8ce4628bb Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Sat, 22 May 2021 23:18:43 +0100
Subject: [PATCH 11/25] Accounts: no wrapping for contact dropdown
---
css/base.css | 4 ++++
ts/modules/accounts.ts | 14 +++++++-------
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/css/base.css b/css/base.css
index d40e6f9..57d7f31 100644
--- a/css/base.css
+++ b/css/base.css
@@ -443,6 +443,10 @@ p.top {
margin-bottom: -0.5rem;
}
+.dropdown-display.lg {
+ white-space: nowrap;
+}
+
pre {
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index c685465..18f4b77 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -128,14 +128,14 @@ class user implements User {
-
+
${window.lang.strings("contactThrough")}
-