From f8f5f35cc1860d8efb04e3c82af9d10310f0f8d4 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 21 May 2021 21:35:25 +0100 Subject: [PATCH] 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) }