From 72bf280e2d7212b2404eac5edd7079f9dcbd3d2f Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 7 May 2021 14:32:51 +0100 Subject: [PATCH] telegram: Fix UI and store useful Telegram info Creation now works, and language preferences made before signup are kept. telegram file storage now uses the Jellyfin ID as a key, which makes much more sense. Also added radios to select preferred notification method (email/telegram) as well, which the admin will soon be able to change also. --- api.go | 76 ++++++++++++++++++++++++++++++++++++-------- html/form.html | 12 +++++-- lang/form/en-us.json | 6 +++- models.go | 10 +++--- storage.go | 3 +- telegram.go | 69 +++++++++++++++++++++++++--------------- ts/form.ts | 34 ++++++++++++++++---- 7 files changed, 157 insertions(+), 53 deletions(-) diff --git a/api.go b/api.go index ee09b09..4a00fbb 100644 --- a/api.go +++ b/api.go @@ -330,15 +330,44 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc success = false return } + telegramTokenIndex := -1 + if app.config.Section("telegram").Key("enabled").MustBool(false) { + if req.TelegramPIN == "" { + if app.config.Section("telegram").Key("required").MustBool(false) { + f = func(gc *gin.Context) { + app.debug.Printf("%s: New user failed: Telegram verification not completed", req.Code) + respond(401, "errorTelegramVerification", gc) + } + success = false + return + } + } else { + for i, v := range app.telegram.verifiedTokens { + if v.Token == req.TelegramPIN { + telegramTokenIndex = i + break + } + } + if telegramTokenIndex == -1 { + f = func(gc *gin.Context) { + app.debug.Printf("%s: New user failed: Telegram PIN was invalid", req.Code) + respond(401, "errorInvalidPIN", gc) + } + success = false + return + } + } + } if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed { claims := jwt.MapClaims{ - "valid": true, - "invite": req.Code, - "email": req.Email, - "username": req.Username, - "password": req.Password, - "exp": strconv.FormatInt(time.Now().Add(time.Hour*12).Unix(), 10), - "type": "confirmation", + "valid": true, + "invite": req.Code, + "email": req.Email, + "username": req.Username, + "password": req.Password, + "telegramPIN": req.TelegramPIN, + "exp": strconv.FormatInt(time.Now().Add(time.Hour*12).Unix(), 10), + "type": "confirmation", } tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) @@ -450,6 +479,27 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc app.err.Printf("Failed to store user duration: %v", err) } } + + if app.config.Section("telegram").Key("enabled").MustBool(false) && telegramTokenIndex != -1 { + tgToken := app.telegram.verifiedTokens[telegramTokenIndex] + tgUser := TelegramUser{ + ChatID: tgToken.ChatID, + Username: tgToken.Username, + Contact: req.TelegramContact, + } + if lang, ok := app.telegram.languages[tgToken.ChatID]; ok { + tgUser.Lang = lang + } + app.storage.telegram[user.ID] = tgUser + err := app.storage.storeTelegramUsers() + if 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] + app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1] + } + } + if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" { app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email) msg, err := app.email.constructWelcome(req.Username, expiry, app, false) @@ -1891,16 +1941,16 @@ func (app *appContext) TelegramVerified(gc *gin.Context) { pin := gc.Param("pin") tokenIndex := -1 for i, v := range app.telegram.verifiedTokens { - if v == pin { + if v.Token == pin { tokenIndex = i break } } - if tokenIndex != -1 { - length := len(app.telegram.verifiedTokens) - app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1] - app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1] - } + // if tokenIndex != -1 { + // length := len(app.telegram.verifiedTokens) + // app.telegram.verifiedTokens[length-1], app.telegram.verifiedTokens[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[length-1] + // app.telegram.verifiedTokens = app.telegram.verifiedTokens[:length-1] + // } respondBool(200, tokenIndex != -1, gc) } diff --git a/html/form.html b/html/form.html index 7f14330..df3ee14 100644 --- a/html/form.html +++ b/html/form.html @@ -33,7 +33,7 @@ @{{ .telegramUsername }} - . + {{ .strings.success }} {{ end }} @@ -68,7 +68,15 @@ {{ if .telegramEnabled }} - {{ .strings.linkTelegram }} + {{ .strings.linkTelegram }} +
+ + +
{{ end }} diff --git a/lang/form/en-us.json b/lang/form/en-us.json index c6411e6..36b1089 100644 --- a/lang/form/en-us.json +++ b/lang/form/en-us.json @@ -19,11 +19,15 @@ "confirmationRequiredMessage": "Please check your email inbox to verify your address.", "yourAccountIsValidUntil": "Your account will be valid until {date}.", "linkTelegram": "Link Telegram", - "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.", + "contactEmail": "Contact through Email", + "contactTelegram": "Contact through Telegram" }, "notifications": { "errorUserExists": "User already exists.", "errorInvalidCode": "Invalid invite code.", + "errorTelegramVerification": "Telegram verification required.", + "errorInvalidPIN": "Telegram PIN is invalid.", "telegramVerified": "Telegram account verified." }, "validationStrings": { diff --git a/models.go b/models.go index 0dd9be3..28a2241 100644 --- a/models.go +++ b/models.go @@ -11,10 +11,12 @@ type boolResponse struct { } type newUserDTO struct { - Username string `json:"username" example:"jeff" binding:"required"` // User's username - Password string `json:"password" example:"guest" binding:"required"` // User's password - Email string `json:"email" example:"jeff@jellyf.in"` // User's email address - Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser) + Username string `json:"username" example:"jeff" binding:"required"` // User's username + Password string `json:"password" example:"guest" binding:"required"` // User's password + Email string `json:"email" example:"jeff@jellyf.in"` // User's email address + 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 } type newUserResponse struct { diff --git a/storage.go b/storage.go index 93dcc1a..0fe7333 100644 --- a/storage.go +++ b/storage.go @@ -22,7 +22,7 @@ type Storage struct { profiles map[string]Profile defaultProfile string emails, displayprefs, ombi_template map[string]interface{} - telegram map[int64]TelegramUser + telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users. customEmails customEmails policy mediabrowser.Policy configuration mediabrowser.Configuration @@ -34,6 +34,7 @@ type TelegramUser struct { ChatID int64 Username string Lang string + Contact bool // Whether to contact through telegram or not } type customEmails struct { diff --git a/telegram.go b/telegram.go index 5d06e93..f8655f0 100644 --- a/telegram.go +++ b/telegram.go @@ -10,13 +10,20 @@ import ( tg "github.com/go-telegram-bot-api/telegram-bot-api" ) +type VerifiedToken struct { + Token string + ChatID int64 + Username string +} + type TelegramDaemon struct { Stopped bool ShutdownChannel chan string bot *tg.BotAPI username string tokens []string - verifiedTokens []string + verifiedTokens []VerifiedToken + 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 } @@ -30,16 +37,23 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) { if err != nil { return nil, err } - return &TelegramDaemon{ + td := &TelegramDaemon{ Stopped: false, ShutdownChannel: make(chan string), bot: bot, username: bot.Self.UserName, tokens: []string{}, - verifiedTokens: []string{}, + verifiedTokens: []VerifiedToken{}, + languages: map[int64]string{}, link: "https://t.me/" + bot.Self.UserName, app: app, - }, nil + } + for _, user := range app.storage.telegram { + if user.Lang != "" { + td.languages[user.ChatID] = user.Lang + } + } + return td, nil } var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") @@ -80,28 +94,21 @@ func (t *TelegramDaemon) run() { continue } lang := t.app.storage.lang.chosenTelegramLang - user, ok := t.app.storage.telegram[upd.Message.Chat.ID] + storedLang, ok := t.languages[upd.Message.Chat.ID] if !ok { - user := TelegramUser{ - Username: upd.Message.Chat.UserName, - ChatID: upd.Message.Chat.ID, - Lang: "", - } - t.app.storage.telegram[upd.Message.Chat.ID] = user - err := t.app.storage.storeTelegramUsers() - if err != nil { - t.app.err.Printf("Failed to store Telegram users: %v", err) - } - } - if user.Lang != "" { - lang = user.Lang - } else { + found := false for code := range t.app.storage.lang.Telegram { if code[:2] == upd.Message.From.LanguageCode { lang = code + found = true break } } + if found { + t.languages[upd.Message.Chat.ID] = lang + } + } else { + lang = storedLang } switch msg := sects[0]; msg { case "/start": @@ -125,11 +132,17 @@ func (t *TelegramDaemon) run() { continue } if _, ok := t.app.storage.lang.Telegram[sects[1]]; ok { - user.Lang = sects[1] - t.app.storage.telegram[upd.Message.Chat.ID] = user - err := t.app.storage.storeTelegramUsers() - if err != nil { - t.app.err.Printf("Failed to store Telegram users: %v", err) + t.languages[upd.Message.Chat.ID] = sects[1] + for jfID, user := range t.app.storage.telegram { + 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 { + t.app.err.Printf("Failed to store Telegram users: %v", err) + } + break + } } } continue @@ -148,11 +161,15 @@ func (t *TelegramDaemon) run() { } continue } - err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("success")) + err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess")) 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, upd.Message.Text) + t.verifiedTokens = append(t.verifiedTokens, VerifiedToken{ + Token: upd.Message.Text, + ChatID: upd.Message.Chat.ID, + Username: upd.Message.Chat.UserName, + }) t.tokens[len(t.tokens)-1], t.tokens[tokenIndex] = t.tokens[tokenIndex], t.tokens[len(t.tokens)-1] t.tokens = t.tokens[:len(t.tokens)-1] } diff --git a/ts/form.ts b/ts/form.ts index c6a1adb..5e3a019 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -1,5 +1,5 @@ import { Modal } from "./modules/modal.js"; -import { notificationBox } from "./modules/common.js"; +import { notificationBox, whichAnimationEvent } from "./modules/common.js"; import { _get, _post, toggleLoader, toDateString } from "./modules/common.js"; import { loadLangSelector } from "./modules/lang.js"; @@ -41,26 +41,39 @@ loadLangSelector("form"); window.notifications = new notificationBox(document.getElementById("notification-box") as HTMLDivElement); +window.animationEvent = whichAnimationEvent(); + window.successModal = new Modal(document.getElementById("modal-success"), true); +var telegramVerified = false; if (window.telegramEnabled) { window.telegramModal = new Modal(document.getElementById("modal-telegram"), window.telegramRequired); - (document.getElementById("link-telegram") as HTMLSpanElement).onclick = () => { + const telegramButton = document.getElementById("link-telegram") as HTMLSpanElement; + telegramButton.onclick = () => { const waiting = document.getElementById("telegram-waiting") as HTMLSpanElement; toggleLoader(waiting); window.telegramModal.show(); + let modalClosed = false; + window.telegramModal.onclose = () => { modalClosed = true; } const checkVerified = () => _get("/invite/" + window.code + "/telegram/verified/" + window.telegramPIN, null, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status == 401) { window.telegramModal.close(); - window.notifications.customError("invalidCodeError", window.lang.notif("errorInvalidCode")); + window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]); return; } else if (req.status == 200) { if (req.response["success"] as boolean) { + telegramVerified = true; toggleLoader(waiting); - window.telegramModal.close() - window.notifications.customSuccess("accountVerified", window.lang.notif("telegramVerified")) - } else { + waiting.classList.add("~positive"); + waiting.classList.remove("~info"); + window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]); + setTimeout(window.telegramModal.close, 2000); + telegramButton.classList.add("unfocused"); + document.getElementById("contact-via").classList.remove("unfocused"); + const radio = document.getElementById("contact-via-telegram") as HTMLInputElement; + radio.checked = true; + } else if (!modalClosed) { setTimeout(checkVerified, 1500); } } @@ -145,6 +158,8 @@ interface sendDTO { email: string; username: string; password: string; + telegram_pin?: string; + telegram_contact?: boolean; } const create = (event: SubmitEvent) => { @@ -156,6 +171,13 @@ const create = (event: SubmitEvent) => { email: emailField.value, password: passwordField.value }; + if (telegramVerified) { + send.telegram_pin = window.telegramPIN; + const radio = document.getElementById("contact-via-telegram") as HTMLInputElement; + if (radio.checked) { + send.telegram_contact = true; + } + } _post("/newUser", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { let vals = req.response as respDTO;