From 99875b917619c9e76b7728acd9cf700472e0a6ae Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Fri, 7 May 2021 01:08:12 +0100 Subject: [PATCH] almost complete telegram user verification When signing up, the user is given a pin code which they send to a telegram bot. This provides user verification, but more importantly allows the bot to message the user, as the Telegram API requires the user to interact with the bot before it can do the opposite. The bot should recognize the correct language, but a /lang command is also provided to change it. The verification process is pretty much functional but ui is still broken, and it isn't properly integrated yet. --- .gitignore | 1 + api.go | 34 ++++++- config.go | 3 +- config/config-base.json | 55 +++++++++++ css/base.css | 9 ++ email.go | 70 +++++++------- go.mod | 13 ++- go.sum | 26 +++--- html/form-base.html | 3 + html/form.html | 23 ++++- internal.go | 2 +- lang.go | 17 ++++ lang/form/en-us.json | 7 +- main.go | 16 ++++ router.go | 3 + storage.go | 157 +++++++++++++++++++++++++------ telegram.go | 201 ++++++++++++++++++++++++++++++++++++++++ ts/form.ts | 43 ++++++++- views.go | 12 ++- 19 files changed, 598 insertions(+), 97 deletions(-) create mode 100644 telegram.go diff --git a/.gitignore b/.gitignore index 6208b48..228ac42 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ server.pem server.crt instructions-debian.txt cl.md +telegram/ diff --git a/api.go b/api.go index 574dccb..ee09b09 100644 --- a/api.go +++ b/api.go @@ -549,7 +549,7 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) { } if len(addresses) != 0 { go func(reason string, addresses []string) { - var msg *Email + var msg *Message var err error if req.Enabled { msg, err = app.email.constructEnabled(reason, app, false) @@ -1580,7 +1580,7 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) { id := gc.Param("id") var content string var err error - var msg *Email + var msg *Message var variables []string var conditionals []string var values map[string]interface{} @@ -1874,6 +1874,36 @@ func (app *appContext) ServeLang(gc *gin.Context) { respondBool(400, false, gc) } +// @Summary Returns true/false on whether or not a telegram PIN was verified. +// @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}/telegram/verified/{pin} [get] +// @tags Other +func (app *appContext) TelegramVerified(gc *gin.Context) { + code := gc.Param("invCode") + if _, ok := app.storage.invites[code]; !ok { + respondBool(401, false, gc) + return + } + pin := gc.Param("pin") + tokenIndex := -1 + for i, v := range app.telegram.verifiedTokens { + if v == 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] + } + respondBool(200, tokenIndex != -1, gc) +} + // @Summary Restarts the program. No response means success. // @Router /restart [post] // @tags Other diff --git a/config.go b/config.go index 7b3092e..9ef527f 100644 --- a/config.go +++ b/config.go @@ -40,7 +40,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"} { + for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_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(""), "/") @@ -128,6 +128,7 @@ func (app *appContext) loadConfig() error { app.storage.lang.chosenAdminLang = app.config.Section("ui").Key("language-admin").MustString("en-us") app.storage.lang.chosenEmailLang = app.config.Section("email").Key("language").MustString("en-us") app.storage.lang.chosenPWRLang = app.config.Section("password_resets").Key("language").MustString("en-us") + app.storage.lang.chosenTelegramLang = app.config.Section("telegram").Key("language").MustString("en-us") app.email = NewEmailer(app) diff --git a/config/config-base.json b/config/config-base.json index 5cd3838..61be322 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -997,6 +997,53 @@ } } }, + "telegram": { + "order": [], + "meta": { + "name": "Telegram", + "description": "Settings for Telegram signup/notifications" + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": false, + "requires_restart": true, + "type": "bool", + "value": false, + "description": "Enable signup verification through Telegram and the sending of notifications through it." + }, + "required": { + "name": "Require on sign-up", + "required": false, + "required_restart": true, + "type": "bool", + "value": false, + "description": "Require telegram connection on sign-up." + }, + "token": { + "name": "API Token", + "required": false, + "requires_restart": true, + "depends_true": "enabled", + "type": "text", + "value": "", + "description": "Telegram Bot API Token." + }, + "language": { + "name": "Language", + "required": false, + "requires_restart": false, + "depends_true": "enabled", + "type": "select", + "options": [ + ["en-us", "English (US)"] + ], + "value": "en-us", + "description": "Default telegram message language. Visit weblate if you'd like to translate." + } + } + + }, "files": { "order": [], "meta": { @@ -1068,6 +1115,14 @@ "type": "text", "value": "", "description": "JSON file generated by program in settings, different from email_html/email_text. See wiki for more info." + }, + "telegram_users": { + "name": "Telegram users", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "Stores telegram user IDs and language preferences." } } } diff --git a/css/base.css b/css/base.css index e666869..95f1b4b 100644 --- a/css/base.css +++ b/css/base.css @@ -121,6 +121,10 @@ div.card:contains(section.banner.footer) { text-align: right; } +.ac { + text-align: center; +} + .inline-block { display: inline-block; } @@ -459,6 +463,11 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) { color: var(--color-urge-200); } +.link-center { + display: block; + text-align: center; +} + .search { max-width: 15rem; min-width: 10rem; diff --git a/email.go b/email.go index 694ff0e..e7c4f65 100644 --- a/email.go +++ b/email.go @@ -25,13 +25,13 @@ import ( ) // implements email sending, right now via smtp or mailgun. -type emailClient interface { - send(fromName, fromAddr string, email *Email, address ...string) error +type EmailClient interface { + Send(fromName, fromAddr string, message *Message, address ...string) error } -type dummyClient struct{} +type DummyClient struct{} -func (dc *dummyClient) send(fromName, fromAddr string, email *Email, address ...string) error { +func (dc *DummyClient) Send(fromName, fromAddr string, email *Message, address ...string) error { fmt.Printf("FROM: %s <%s>\nTO: %s\nTEXT: %s\n", fromName, fromAddr, strings.Join(address, ", "), email.Text) return nil } @@ -41,7 +41,7 @@ type Mailgun struct { client *mailgun.MailgunImpl } -func (mg *Mailgun) send(fromName, fromAddr string, email *Email, address ...string) error { +func (mg *Mailgun) Send(fromName, fromAddr string, email *Message, address ...string) error { message := mg.client.NewMessage( fmt.Sprintf("%s <%s>", fromName, fromAddr), email.Subject, @@ -67,7 +67,7 @@ type SMTP struct { tlsConfig *tls.Config } -func (sm *SMTP) send(fromName, fromAddr string, email *Email, address ...string) error { +func (sm *SMTP) Send(fromName, fromAddr string, email *Message, address ...string) error { server := fmt.Sprintf("%s:%d", sm.server, sm.port) from := fmt.Sprintf("%s <%s>", fromName, fromAddr) var wg sync.WaitGroup @@ -93,15 +93,15 @@ func (sm *SMTP) send(fromName, fromAddr string, email *Email, address ...string) return err } -// Emailer contains the email sender, email content, and methods to construct message content. +// Emailer contains the email sender, translations, and methods to construct messages. type Emailer struct { fromAddr, fromName string lang emailLang - sender emailClient + sender EmailClient } -// Email stores content. -type Email struct { +// Message stores content. +type Message struct { Subject string `json:"subject"` HTML string `json:"html"` Text string `json:"text"` @@ -154,7 +154,7 @@ func NewEmailer(app *appContext) *Emailer { } else if method == "mailgun" { emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String()) } else if method == "dummy" { - emailer.sender = &dummyClient{} + emailer.sender = &DummyClient{} } return emailer } @@ -267,8 +267,8 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC return template } -func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Email, error) { - email := &Email{ +func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Message, error) { + email := &Message{ Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")), } var err error @@ -290,8 +290,8 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a return email, nil } -func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Email, error) { - email := &Email{Subject: subject} +func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Message, error) { + email := &Message{Subject: subject} renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants}) html := markdown.ToHTML([]byte(md), nil, renderer) text := stripMarkdown(md) @@ -338,8 +338,8 @@ func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext return template } -func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Email, error) { - email := &Email{ +func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Message, error) { + email := &Message{ Subject: app.config.Section("invite_emails").Key("subject").MustString(emailer.lang.InviteEmail.get("title")), } template := emailer.inviteValues(code, invite, app, noSub) @@ -377,8 +377,8 @@ func (emailer *Emailer) expiryValues(code string, invite Invite, app *appContext return template } -func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Email, error) { - email := &Email{ +func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Message, error) { + email := &Message{ Subject: emailer.lang.InviteExpiry.get("title"), } var err error @@ -431,8 +431,8 @@ func (emailer *Emailer) createdValues(code, username, address string, invite Inv return template } -func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Email, error) { - email := &Email{ +func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Message, error) { + email := &Message{ Subject: emailer.lang.UserCreated.get("title"), } template := emailer.createdValues(code, username, address, invite, app, noSub) @@ -505,8 +505,8 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo return template } -func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Email, error) { - email := &Email{ +func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Message, error) { + email := &Message{ Subject: app.config.Section("password_resets").Key("subject").MustString(emailer.lang.PasswordReset.get("title")), } template := emailer.resetValues(pwr, app, noSub) @@ -546,8 +546,8 @@ func (emailer *Emailer) deletedValues(reason string, app *appContext, noSub bool return template } -func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Email, error) { - email := &Email{ +func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Message, error) { + email := &Message{ Subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")), } var err error @@ -587,8 +587,8 @@ func (emailer *Emailer) disabledValues(reason string, app *appContext, noSub boo return template } -func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub bool) (*Email, error) { - email := &Email{ +func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub bool) (*Message, error) { + email := &Message{ Subject: app.config.Section("disable_enable").Key("subject_disabled").MustString(emailer.lang.UserDisabled.get("title")), } var err error @@ -628,8 +628,8 @@ func (emailer *Emailer) enabledValues(reason string, app *appContext, noSub bool return template } -func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub bool) (*Email, error) { - email := &Email{ +func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub bool) (*Message, error) { + email := &Message{ Subject: app.config.Section("disable_enable").Key("subject_enabled").MustString(emailer.lang.UserEnabled.get("title")), } var err error @@ -683,8 +683,8 @@ func (emailer *Emailer) welcomeValues(username string, expiry time.Time, app *ap return template } -func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app *appContext, noSub bool) (*Email, error) { - email := &Email{ +func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app *appContext, noSub bool) (*Message, error) { + email := &Message{ Subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")), } var err error @@ -728,8 +728,8 @@ func (emailer *Emailer) userExpiredValues(app *appContext, noSub bool) map[strin return template } -func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Email, error) { - email := &Email{ +func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Message, error) { + email := &Message{ Subject: app.config.Section("user_expiry").Key("subject").MustString(emailer.lang.UserExpired.get("title")), } var err error @@ -752,6 +752,6 @@ func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Emai } // calls the send method in the underlying emailClient. -func (emailer *Emailer) send(email *Email, address ...string) error { - return emailer.sender.send(emailer.fromName, emailer.fromAddr, email, address...) +func (emailer *Emailer) send(email *Message, address ...string) error { + return emailer.sender.Send(emailer.fromName, emailer.fromAddr, email, address...) } diff --git a/go.mod b/go.mod index 333a4ac..5be7638 100644 --- a/go.mod +++ b/go.mod @@ -17,29 +17,28 @@ require ( github.com/gin-contrib/pprof v1.3.0 github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e github.com/gin-gonic/gin v1.6.3 - github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-openapi/spec v0.20.3 // indirect github.com/go-openapi/swag v0.19.15 // indirect github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect github.com/golang/protobuf v1.4.3 // indirect - github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8 + github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd github.com/google/uuid v1.1.2 // indirect github.com/hrfee/jfa-go/common v0.0.0-20210105184019-fdc97b4e86cc github.com/hrfee/jfa-go/docs v0.0.0-20201112212552-b6f3cd7c1f71 - github.com/hrfee/jfa-go/logger v0.0.0-00010101000000-000000000000 // indirect + github.com/hrfee/jfa-go/logger v0.0.0-00010101000000-000000000000 github.com/hrfee/jfa-go/ombi v0.0.0-20201112212552-b6f3cd7c1f71 github.com/hrfee/mediabrowser v0.3.3 github.com/itchyny/timefmt-go v0.1.2 - github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/lithammer/shortuuid/v3 v3.0.4 - github.com/mailgun/mailgun-go/v4 v4.3.0 + github.com/mailgun/mailgun-go/v4 v4.5.1 github.com/mailru/easyjson v0.7.7 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 github.com/swaggo/gin-swagger v1.3.0 github.com/swaggo/swag v1.7.0 // indirect + github.com/technoweenie/multipartstreamer v1.0.1 // indirect github.com/ugorji/go v1.2.0 // indirect github.com/writeas/go-strip-markdown v2.0.1+incompatible golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect diff --git a/go.sum b/go.sum index 6a79883..5b09672 100644 --- a/go.sum +++ b/go.sum @@ -58,9 +58,6 @@ github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmC github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= -github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -96,6 +93,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= @@ -112,8 +111,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8 h1:nWU6p08f1VgIalT6iZyqXi4o5cZsz4X6qa87nusfcsc= -github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU= +github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd h1:0b8AqsWQb6A0jjx80UXLG/uMTXQkGD0IGuXWqsrNz1M= +github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -127,12 +126,14 @@ github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 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/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= github.com/itchyny/timefmt-go v0.1.2/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A= -github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible h1:CL0ooBNfbNyJTJATno+m0h+zM5bW6v7fKlboKUGP/dI= -github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -156,8 +157,8 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lithammer/shortuuid/v3 v3.0.4 h1:uj4xhotfY92Y1Oa6n6HUiFn87CdoEHYUlTy0+IgbLrs= github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw= -github.com/mailgun/mailgun-go/v4 v4.3.0 h1:9nAF7LI3k6bfDPbMZQMMl63Q8/vs+dr1FUN8eR1XMhk= -github.com/mailgun/mailgun-go/v4 v4.3.0/go.mod h1:fWuBI2iaS/pSSyo6+EBpHjatQO3lV8onwqcRy7joSJI= +github.com/mailgun/mailgun-go/v4 v4.5.1 h1:XrQQ/ZgqFvINRKy+eBqowLl7k3pQO6OCLpKphliMOFs= +github.com/mailgun/mailgun-go/v4 v4.5.1/go.mod h1:FJlF9rI5cQT+mrwujtJjPMbIVy3Ebor9bKTVsJ0QU40= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -180,9 +181,8 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= @@ -197,8 +197,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -216,6 +214,8 @@ github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+t github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc= github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E= github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo= +github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= +github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= diff --git a/html/form-base.html b/html/form-base.html index aa98d32..8a4a9ee 100644 --- a/html/form-base.html +++ b/html/form-base.html @@ -14,6 +14,9 @@ window.userExpiryHours = {{ .userExpiryHours }}; window.userExpiryMinutes = {{ .userExpiryMinutes }}; window.userExpiryMessage = {{ .userExpiryMessage }}; + window.telegramEnabled = {{ .telegramEnabled }}; + window.telegramRequired = {{ .telegramRequired }}; + window.telegramPIN = "{{ .telegramPIN }}"; {{ end }} diff --git a/html/form.html b/html/form.html index 9fe6694..7f14330 100644 --- a/html/form.html +++ b/html/form.html @@ -19,6 +19,24 @@

{{ .strings.confirmationRequiredMessage }}

+ {{ if .telegramEnabled }} + + {{ end }} @@ -29,6 +47,7 @@ +
@@ -48,7 +67,9 @@ - + {{ if .telegramEnabled }} + {{ .strings.linkTelegram }} + {{ end }} diff --git a/internal.go b/internal.go index 92732c3..a2fc105 100644 --- a/internal.go +++ b/internal.go @@ -13,7 +13,7 @@ const binaryType = "internal" //go:embed data data/html data/web data/web/css data/web/js var loFS embed.FS -//go:embed lang/common lang/admin lang/email lang/form lang/setup lang/pwreset +//go:embed lang/common lang/admin lang/email lang/form lang/setup lang/pwreset lang/telegram var laFS embed.FS var langFS rewriteFS diff --git a/lang.go b/lang.go index ca415e1..69c9ef4 100644 --- a/lang.go +++ b/lang.go @@ -136,6 +136,23 @@ func (ls *setupLangs) getOptions() [][2]string { return opts } +type telegramLangs map[string]telegramLang + +type telegramLang struct { + Meta langMeta `json:"meta"` + Strings langSection `json:"strings"` +} + +func (ts *telegramLangs) getOptions() [][2]string { + opts := make([][2]string, len(*ts)) + i := 0 + for key, lang := range *ts { + opts[i] = [2]string{key, lang.Meta.Name} + i++ + } + return opts +} + type langSection map[string]string type tmpl map[string]string diff --git a/lang/form/en-us.json b/lang/form/en-us.json index e1c0575..c6411e6 100644 --- a/lang/form/en-us.json +++ b/lang/form/en-us.json @@ -17,11 +17,14 @@ "successContinueButton": "Continue", "confirmationRequired": "Email confirmation required", "confirmationRequiredMessage": "Please check your email inbox to verify your address.", - "yourAccountIsValidUntil": "Your account will be valid until {date}." + "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." }, "notifications": { "errorUserExists": "User already exists.", - "errorInvalidCode": "Invalid invite code." + "errorInvalidCode": "Invalid invite code.", + "telegramVerified": "Telegram account verified." }, "validationStrings": { "length": { diff --git a/main.go b/main.go index dab521a..70af2e0 100644 --- a/main.go +++ b/main.go @@ -93,6 +93,7 @@ type appContext struct { storage Storage validator Validator email *Emailer + telegram *TelegramDaemon info, debug, err logger.Logger host string port int @@ -257,6 +258,7 @@ func start(asDaemon, firstCall bool) { app.storage.lang.FormPath = "form" app.storage.lang.AdminPath = "admin" app.storage.lang.EmailPath = "email" + app.storage.lang.TelegramPath = "telegram" app.storage.lang.PasswordResetPath = "pwreset" externalLang := app.config.Section("files").Key("lang_files").MustString("") var err error @@ -325,6 +327,10 @@ func start(asDaemon, firstCall bool) { if err := app.storage.loadUsers(); err != nil { app.err.Printf("Failed to load Users: %v", err) } + app.storage.telegram_path = app.config.Section("files").Key("telegram_users").String() + if err := app.storage.loadTelegramUsers(); err != nil { + app.err.Printf("Failed to load Telegram users: %v", err) + } app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String() app.storage.loadProfiles() @@ -541,6 +547,16 @@ func start(asDaemon, firstCall bool) { if app.config.Section("updates").Key("enabled").MustBool(false) { go app.checkForUpdates() } + + if app.config.Section("telegram").Key("enabled").MustBool(false) { + app.telegram, err = newTelegramDaemon(app) + if err != nil { + app.err.Printf("Failed to authenticate with Telegram: %v", err) + } else { + go app.telegram.run() + defer app.telegram.Shutdown() + } + } } else { debugMode = false address = "0.0.0.0:8056" diff --git a/router.go b/router.go index 4c15516..63892ad 100644 --- a/router.go +++ b/router.go @@ -118,6 +118,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) { router.POST(p+"/newUser", app.NewUser) router.Use(static.Serve(p+"/invite/", app.webFS)) router.GET(p+"/invite/:invCode", app.InviteProxy) + if app.config.Section("telegram").Key("enabled").MustBool(false) { + router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerified) + } } if *SWAGGER { app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n")) diff --git a/storage.go b/storage.go index 1bc7fef..93dcc1a 100644 --- a/storage.go +++ b/storage.go @@ -15,18 +15,25 @@ import ( ) type Storage struct { - timePattern string - invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path string - users map[string]time.Time - invites Invites - profiles map[string]Profile - defaultProfile string - emails, displayprefs, ombi_template map[string]interface{} - 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 string + users map[string]time.Time + invites Invites + profiles map[string]Profile + defaultProfile string + emails, displayprefs, ombi_template map[string]interface{} + telegram map[int64]TelegramUser + customEmails customEmails + policy mediabrowser.Policy + configuration mediabrowser.Configuration + lang Lang + invitesLock, usersLock sync.Mutex +} + +type TelegramUser struct { + ChatID int64 + Username string + Lang string } type customEmails struct { @@ -81,23 +88,26 @@ type Invite struct { } type Lang struct { - AdminPath string - chosenAdminLang string - Admin adminLangs - AdminJSON map[string]string - FormPath string - chosenFormLang string - Form formLangs - PasswordResetPath string - chosenPWRLang string - PasswordReset pwrLangs - EmailPath string - chosenEmailLang string - Email emailLangs - CommonPath string - Common commonLangs - SetupPath string - Setup setupLangs + AdminPath string + chosenAdminLang string + Admin adminLangs + AdminJSON map[string]string + FormPath string + chosenFormLang string + Form formLangs + PasswordResetPath string + chosenPWRLang string + PasswordReset pwrLangs + EmailPath string + chosenEmailLang string + Email emailLangs + CommonPath string + Common commonLangs + SetupPath string + Setup setupLangs + chosenTelegramLang string + TelegramPath string + Telegram telegramLangs } func (st *Storage) loadLang(filesystems ...fs.FS) (err error) { @@ -118,6 +128,10 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) { return } err = st.loadLangEmail(filesystems...) + if err != nil { + return + } + err = st.loadLangTelegram(filesystems...) return } @@ -620,6 +634,83 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error { return nil } +func (st *Storage) loadLangTelegram(filesystems ...fs.FS) error { + st.lang.Telegram = map[string]telegramLang{} + var english telegramLang + loadedLangs := make([]map[string]bool, len(filesystems)) + var load loadLangFunc + load = func(fsIndex int, fname string) error { + filesystem := filesystems[fsIndex] + index := strings.TrimSuffix(fname, filepath.Ext(fname)) + lang := telegramLang{} + f, err := fs.ReadFile(filesystem, FSJoin(st.lang.TelegramPath, fname)) + if err != nil { + return err + } + if substituteStrings != "" { + f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings)) + } + err = json.Unmarshal(f, &lang) + if err != nil { + return err + } + st.lang.Common.patchCommon(&lang.Strings, index) + if fname != "en-us.json" { + if lang.Meta.Fallback != "" { + fallback, ok := st.lang.Telegram[lang.Meta.Fallback] + err = nil + if !ok { + err = load(fsIndex, lang.Meta.Fallback+".json") + fallback = st.lang.Telegram[lang.Meta.Fallback] + } + if err == nil { + loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true + patchLang(&lang.Strings, &fallback.Strings, &english.Strings) + } + } + if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" { + patchLang(&lang.Strings, &english.Strings) + } + } + st.lang.Telegram[index] = lang + return nil + } + engFound := false + var err error + for i := range filesystems { + loadedLangs[i] = map[string]bool{} + err = load(i, "en-us.json") + if err == nil { + engFound = true + } + loadedLangs[i]["en-us.json"] = true + } + if !engFound { + return err + } + english = st.lang.Telegram["en-us"] + telegramLoaded := false + for i := range filesystems { + files, err := fs.ReadDir(filesystems[i], st.lang.TelegramPath) + if err != nil { + continue + } + for _, f := range files { + if !loadedLangs[i][f.Name()] { + err = load(i, f.Name()) + if err == nil { + telegramLoaded = true + loadedLangs[i][f.Name()] = true + } + } + } + } + if !telegramLoaded { + return err + } + return nil +} + type Invites map[string]Invite func (st *Storage) loadInvites() error { @@ -665,6 +756,14 @@ func (st *Storage) storeEmails() error { return storeJSON(st.emails_path, st.emails) } +func (st *Storage) loadTelegramUsers() error { + return loadJSON(st.telegram_path, &st.telegram) +} + +func (st *Storage) storeTelegramUsers() error { + return storeJSON(st.telegram_path, st.telegram) +} + func (st *Storage) loadCustomEmails() error { return loadJSON(st.customEmails_path, &st.customEmails) } diff --git a/telegram.go b/telegram.go new file mode 100644 index 0000000..5d06e93 --- /dev/null +++ b/telegram.go @@ -0,0 +1,201 @@ +package main + +import ( + "fmt" + "math/rand" + "strconv" + "strings" + "time" + + tg "github.com/go-telegram-bot-api/telegram-bot-api" +) + +type TelegramDaemon struct { + Stopped bool + ShutdownChannel chan string + bot *tg.BotAPI + username string + tokens []string + verifiedTokens []string + link string + app *appContext +} + +func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) { + token := app.config.Section("telegram").Key("token").String() + if token == "" { + return nil, fmt.Errorf("token was blank") + } + bot, err := tg.NewBotAPI(token) + if err != nil { + return nil, err + } + return &TelegramDaemon{ + Stopped: false, + ShutdownChannel: make(chan string), + bot: bot, + username: bot.Self.UserName, + tokens: []string{}, + verifiedTokens: []string{}, + link: "https://t.me/" + bot.Self.UserName, + app: app, + }, nil +} + +var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + +// NewAuthToken generates an 8-character pin in the form "A1-2B-CD". +func (t *TelegramDaemon) NewAuthToken() string { + rand.Seed(time.Now().UnixNano()) + pin := make([]rune, 8) + for i := range pin { + if i == 2 || i == 5 { + pin[i] = '-' + } else { + pin[i] = runes[rand.Intn(len(runes))] + } + } + t.tokens = append(t.tokens, string(pin)) + return string(pin) +} + +func (t *TelegramDaemon) run() { + t.app.info.Println("Starting Telegram bot daemon") + u := tg.NewUpdate(0) + u.Timeout = 60 + updates, err := t.bot.GetUpdatesChan(u) + if err != nil { + t.app.err.Printf("Failed to start Telegram daemon: %v", err) + return + } + for { + var upd tg.Update + select { + case upd = <-updates: + if upd.Message == nil { + continue + } + sects := strings.Split(upd.Message.Text, " ") + if len(sects) == 0 { + continue + } + lang := t.app.storage.lang.chosenTelegramLang + user, ok := t.app.storage.telegram[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 { + for code := range t.app.storage.lang.Telegram { + if code[:2] == upd.Message.From.LanguageCode { + lang = code + break + } + } + } + switch msg := sects[0]; msg { + case "/start": + content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n" + content += t.app.storage.lang.Telegram[lang].Strings.get("languageMessage") + 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) + } + continue + case "/lang": + if len(sects) == 1 { + 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) + } + err := t.Reply(&upd, list) + if err != nil { + t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err) + } + 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) + } + } + continue + default: + tokenIndex := -1 + for i, token := range t.tokens { + if upd.Message.Text == token { + tokenIndex = i + break + } + } + if tokenIndex == -1 { + err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN")) + if err != nil { + t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err) + } + continue + } + err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("success")) + 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.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] + } + + case <-t.ShutdownChannel: + t.ShutdownChannel <- "Down" + return + } + } +} + +func (t *TelegramDaemon) Reply(upd *tg.Update, content string) error { + msg := tg.NewMessage((*upd).Message.Chat.ID, content) + _, err := t.bot.Send(msg) + return err +} + +func (t *TelegramDaemon) QuoteReply(upd *tg.Update, content string) error { + msg := tg.NewMessage((*upd).Message.Chat.ID, content) + msg.ReplyToMessageID = (*upd).Message.MessageID + _, err := t.bot.Send(msg) + return err +} + +// Send adds compatibility with EmailClient, fromName/fromAddr are discarded, message.Text is used, addresses are Chat IDs as strings. +func (t *TelegramDaemon) Send(fromName, fromAddr string, message *Message, address ...string) error { + for _, addr := range address { + ChatID, err := strconv.ParseInt(addr, 10, 64) + if err != nil { + return err + } + msg := tg.NewMessage(ChatID, message.Text) + _, err = t.bot.Send(msg) + if err != nil { + return err + } + } + return nil +} + +func (t *TelegramDaemon) Shutdown() { + t.Stopped = true + t.ShutdownChannel <- "Down" + <-t.ShutdownChannel + close(t.ShutdownChannel) +} diff --git a/ts/form.ts b/ts/form.ts index 7bb56c6..c6a1adb 100644 --- a/ts/form.ts +++ b/ts/form.ts @@ -1,15 +1,20 @@ import { Modal } from "./modules/modal.js"; +import { notificationBox } from "./modules/common.js"; import { _get, _post, toggleLoader, toDateString } from "./modules/common.js"; import { loadLangSelector } from "./modules/lang.js"; interface formWindow extends Window { validationStrings: pwValStrings; invalidPassword: string; - modal: Modal; + successModal: Modal; + telegramModal: Modal; + confirmationModal: Modal code: string; messages: { [key: string]: string }; confirmation: boolean; - confirmationModal: Modal + telegramEnabled: boolean; + telegramRequired: boolean; + telegramPIN: string; userExpiryEnabled: boolean; userExpiryMonths: number; userExpiryDays: number; @@ -34,7 +39,37 @@ interface pwValStrings { loadLangSelector("form"); -window.modal = new Modal(document.getElementById("modal-success"), true); +window.notifications = new notificationBox(document.getElementById("notification-box") as HTMLDivElement); + +window.successModal = new Modal(document.getElementById("modal-success"), true); + +if (window.telegramEnabled) { + window.telegramModal = new Modal(document.getElementById("modal-telegram"), window.telegramRequired); + (document.getElementById("link-telegram") as HTMLSpanElement).onclick = () => { + const waiting = document.getElementById("telegram-waiting") as HTMLSpanElement; + toggleLoader(waiting); + window.telegramModal.show(); + 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")); + return; + } else if (req.status == 200) { + if (req.response["success"] as boolean) { + toggleLoader(waiting); + window.telegramModal.close() + window.notifications.customSuccess("accountVerified", window.lang.notif("telegramVerified")) + } else { + setTimeout(checkVerified, 1500); + } + } + } + }); + checkVerified(); + }; +} + if (window.confirmation) { window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true); } @@ -130,7 +165,7 @@ const create = (event: SubmitEvent) => { if (!vals[type]) { valid = false; } } if (req.status == 200 && valid) { - window.modal.show(); + window.successModal.show(); } else { submitSpan.classList.add("~critical"); submitSpan.classList.remove("~urge"); diff --git a/views.go b/views.go index 8e7e801..f103bab 100644 --- a/views.go +++ b/views.go @@ -259,7 +259,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) { if strings.Contains(email, "Failed") { email = "" } - gcHTML(gc, http.StatusOK, "form-loader.html", gin.H{ + data := gin.H{ "urlBase": app.getURLBase(gc), "cssClass": app.cssClass, "contactMessage": app.config.Section("ui").Key("contact_message").String(), @@ -282,7 +282,15 @@ func (app *appContext) InviteProxy(gc *gin.Context) { "userExpiryMinutes": inv.UserMinutes, "userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"), "langName": lang, - }) + "telegramEnabled": app.config.Section("telegram").Key("enabled").MustBool(false), + } + if data["telegramEnabled"].(bool) { + 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) + } + gcHTML(gc, http.StatusOK, "form-loader.html", data) } func (app *appContext) NoRouteHandler(gc *gin.Context) {