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) {