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 }} + + {{ 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} {{ end }} + {{ if .discord_enabled }} + + {{ 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} - -