1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-10-18 17:10:11 +00:00

Compare commits

..

No commits in common. "0efd7c5718cf14b95622a2aed14a6b91d284be78" and "72bf280e2d7212b2404eac5edd7079f9dcbd3d2f" have entirely different histories.

37 changed files with 365 additions and 743 deletions

2
.gitignore vendored
View File

@ -14,4 +14,4 @@ server.pem
server.crt server.crt
instructions-debian.txt instructions-debian.txt
cl.md cl.md
./telegram/ telegram/

View File

@ -17,12 +17,11 @@ jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jelly
* ⌛ User expiry: Specify a validity period, and new users accounts will be disabled/deleted after it. The period can be manually extended too. * ⌛ User expiry: Specify a validity period, and new users accounts will be disabled/deleted after it. The period can be manually extended too.
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions. * 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason. * Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
* Telegram Integration: Verify users via telegram, and send Password Resets, Announcements, etc. through it.
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation. * 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
* Email addresses can optionally be used instead of usernames * Email addresses can optionally be used instead of usernames
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email/telegram. * 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email.
* Notifications: Get notified when someone creates an account, or an invite expires. * Notifications: Get notified when someone creates an account, or an invite expires.
* 📣 Announcements: Bulk message your users with announcements about your server. * 📣 Announcements: Bulk email your users with announcements about your server.
* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider. * Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
* Enables the usage of jfa-go by multiple people * Enables the usage of jfa-go by multiple people
* 🌓 Customizations * 🌓 Customizations

226
api.go
View File

@ -39,9 +39,9 @@ func respondBool(code int, val bool, gc *gin.Context) {
} }
func (app *appContext) loadStrftime() { func (app *appContext) loadStrftime() {
app.datePattern = app.config.Section("messages").Key("date_format").String() app.datePattern = app.config.Section("email").Key("date_format").String()
app.timePattern = `%H:%M` app.timePattern = `%H:%M`
if val, _ := app.config.Section("messages").Key("use_24h").Bool(); !val { if val, _ := app.config.Section("email").Key("use_24h").Bool(); !val {
app.timePattern = `%I:%M %p` app.timePattern = `%I:%M %p`
} }
return return
@ -282,7 +282,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
} }
} }
app.jf.CacheExpiry = time.Now() app.jf.CacheExpiry = time.Now()
if emailEnabled { if app.config.Section("password_resets").Key("enabled").MustBool(false) {
app.storage.emails[id] = req.Email app.storage.emails[id] = req.Email
app.storage.storeEmails() app.storage.storeEmails()
} }
@ -331,7 +331,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
return return
} }
telegramTokenIndex := -1 telegramTokenIndex := -1
if telegramEnabled { if app.config.Section("telegram").Key("enabled").MustBool(false) {
if req.TelegramPIN == "" { if req.TelegramPIN == "" {
if app.config.Section("telegram").Key("required").MustBool(false) { if app.config.Section("telegram").Key("required").MustBool(false) {
f = func(gc *gin.Context) { f = func(gc *gin.Context) {
@ -480,7 +480,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
} }
} }
if telegramEnabled && telegramTokenIndex != -1 { if app.config.Section("telegram").Key("enabled").MustBool(false) && telegramTokenIndex != -1 {
tgToken := app.telegram.verifiedTokens[telegramTokenIndex] tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
tgUser := TelegramUser{ tgUser := TelegramUser{
ChatID: tgToken.ChatID, ChatID: tgToken.ChatID,
@ -490,9 +490,6 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok { if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
tgUser.Lang = lang tgUser.Lang = lang
} }
if app.storage.telegram == nil {
app.storage.telegram = map[string]TelegramUser{}
}
app.storage.telegram[user.ID] = tgUser app.storage.telegram[user.ID] = tgUser
err := app.storage.storeTelegramUsers() err := app.storage.storeTelegramUsers()
if err != nil { if err != nil {
@ -503,16 +500,15 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
} }
} }
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 { if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" {
name := app.getAddressOrName(user.ID) app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email)
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
msg, err := app.email.constructWelcome(req.Username, expiry, app, false) msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
if err != nil { if err != nil {
app.err.Printf("%s: Failed to construct welcome message: %v", req.Username, err) app.err.Printf("%s: Failed to construct welcome email: %v", req.Username, err)
} else if err := app.sendByID(msg, user.ID); err != nil { } else if err := app.email.send(msg, req.Email); err != nil {
app.err.Printf("%s: Failed to send welcome message: %v", req.Username, err) app.err.Printf("%s: Failed to send welcome email: %v", req.Username, err)
} else { } else {
app.info.Printf("%s: Sent welcome message to \"%s\"", req.Username, name) app.info.Printf("%s: Sent welcome email to \"%s\"", req.Username, req.Email)
} }
} }
app.jf.CacheExpiry = time.Now() app.jf.CacheExpiry = time.Now()
@ -579,20 +575,7 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
"GetUser": map[string]string{}, "GetUser": map[string]string{},
"SetPolicy": map[string]string{}, "SetPolicy": map[string]string{},
} }
sendMail := messagesEnabled var addresses []string
var msg *Message
var err error
if sendMail {
if req.Enabled {
msg, err = app.email.constructEnabled(req.Reason, app, false)
} else {
msg, err = app.email.constructDisabled(req.Reason, app, false)
}
if err != nil {
app.err.Printf("Failed to construct account enabled/disabled emails: %v", err)
sendMail = false
}
}
for _, userID := range req.Users { for _, userID := range req.Users {
user, status, err := app.jf.UserByID(userID, false) user, status, err := app.jf.UserByID(userID, false)
if status != 200 || err != nil { if status != 200 || err != nil {
@ -607,13 +590,31 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
app.err.Printf("Failed to set policy for user \"%s\" (%d): %v", userID, status, err) app.err.Printf("Failed to set policy for user \"%s\" (%d): %v", userID, status, err)
continue continue
} }
if sendMail && req.Notify { if emailEnabled && req.Notify {
if err := app.sendByID(msg, userID); err != nil { addr, ok := app.storage.emails[userID]
app.err.Printf("Failed to send account enabled/disabled email: %v", err) if addr != nil && ok {
continue addresses = append(addresses, addr.(string))
} }
} }
} }
if len(addresses) != 0 {
go func(reason string, addresses []string) {
var msg *Message
var err error
if req.Enabled {
msg, err = app.email.constructEnabled(reason, app, false)
} else {
msg, err = app.email.constructDisabled(reason, app, false)
}
if err != nil {
app.err.Printf("Failed to construct account enabled/disabled emails: %v", err)
} else if err := app.email.send(msg, addresses...); err != nil {
app.err.Printf("Failed to send account enabled/disabled emails: %v", err)
} else {
app.info.Println("Sent account enabled/disabled emails")
}
}(req.Reason, addresses)
}
app.jf.CacheExpiry = time.Now() app.jf.CacheExpiry = time.Now()
if len(errors["GetUser"]) != 0 || len(errors["SetPolicy"]) != 0 { if len(errors["GetUser"]) != 0 || len(errors["SetPolicy"]) != 0 {
gc.JSON(500, errors) gc.JSON(500, errors)
@ -633,19 +634,10 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
// @tags Users // @tags Users
func (app *appContext) DeleteUsers(gc *gin.Context) { func (app *appContext) DeleteUsers(gc *gin.Context) {
var req deleteUserDTO var req deleteUserDTO
var addresses []string
gc.BindJSON(&req) gc.BindJSON(&req)
errors := map[string]string{} errors := map[string]string{}
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
sendMail := messagesEnabled
var msg *Message
var err error
if sendMail {
msg, err = app.email.constructDeleted(req.Reason, app, false)
if err != nil {
app.err.Printf("Failed to construct account deletion emails: %v", err)
sendMail = false
}
}
for _, userID := range req.Users { for _, userID := range req.Users {
if ombiEnabled { if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(userID) ombiUser, code, err := app.getOmbiUser(userID)
@ -668,12 +660,25 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
errors[userID] += msg errors[userID] += msg
} }
} }
if sendMail && req.Notify { if emailEnabled && req.Notify {
if err := app.sendByID(msg, userID); err != nil { addr, ok := app.storage.emails[userID]
app.err.Printf("Failed to send account deletion email: %v", err) if addr != nil && ok {
addresses = append(addresses, addr.(string))
} }
} }
} }
if len(addresses) != 0 {
go func(reason string, addresses []string) {
msg, err := app.email.constructDeleted(reason, app, false)
if err != nil {
app.err.Printf("Failed to construct account deletion emails: %v", err)
} else if err := app.email.send(msg, addresses...); err != nil {
app.err.Printf("Failed to send account deletion emails: %v", err)
} else {
app.info.Println("Sent account deletion emails")
}
}(req.Reason, addresses)
}
app.jf.CacheExpiry = time.Now() app.jf.CacheExpiry = time.Now()
if len(errors) == len(req.Users) { if len(errors) == len(req.Users) {
respondBool(500, false, gc) respondBool(500, false, gc)
@ -730,21 +735,29 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
func (app *appContext) Announce(gc *gin.Context) { func (app *appContext) Announce(gc *gin.Context) {
var req announcementDTO var req announcementDTO
gc.BindJSON(&req) gc.BindJSON(&req)
if !messagesEnabled { if !emailEnabled {
respondBool(400, false, gc) respondBool(400, false, gc)
return return
} }
addresses := []string{}
for _, userID := range req.Users {
addr, ok := app.storage.emails[userID]
if !ok || addr == "" {
continue
}
addresses = append(addresses, addr.(string))
}
msg, err := app.email.constructTemplate(req.Subject, req.Message, app) msg, err := app.email.constructTemplate(req.Subject, req.Message, app)
if err != nil { if err != nil {
app.err.Printf("Failed to construct announcement messages: %v", err) app.err.Printf("Failed to construct announcement emails: %v", err)
respondBool(500, false, gc) respondBool(500, false, gc)
return return
} else if err := app.sendByID(msg, req.Users...); err != nil { } else if err := app.email.send(msg, addresses...); err != nil {
app.err.Printf("Failed to send announcement messages: %v", err) app.err.Printf("Failed to send announcement emails: %v", err)
respondBool(500, false, gc) respondBool(500, false, gc)
return return
} }
app.info.Println("Sent announcement messages") app.info.Printf("Sent announcement email to %d users", len(addresses))
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -1174,9 +1187,7 @@ func (app *appContext) GetUsers(gc *gin.Context) {
if ok { if ok {
user.Expiry = expiry.Unix() user.Expiry = expiry.Unix()
} }
if tgUser, ok := app.storage.telegram[jfUser.ID]; ok {
user.Telegram = tgUser.Username
}
resp.UserList[i] = user resp.UserList[i] = user
i++ i++
} }
@ -1384,10 +1395,6 @@ func (app *appContext) GetConfig(gc *gin.Context) {
el := resp.Sections["email"].Settings["language"] el := resp.Sections["email"].Settings["language"]
el.Options = emailOptions el.Options = emailOptions
el.Value = app.config.Section("email").Key("language").MustString("en-us") el.Value = app.config.Section("email").Key("language").MustString("en-us")
telegramOptions := app.storage.lang.Email.getOptions()
tl := resp.Sections["telegram"].Settings["language"]
tl.Options = telegramOptions
tl.Value = app.config.Section("telegram").Key("language").MustString("en-us")
if updater == "" { if updater == "" {
delete(resp.Sections, "updates") delete(resp.Sections, "updates")
for i, v := range resp.Order { for i, v := range resp.Order {
@ -1416,7 +1423,6 @@ func (app *appContext) GetConfig(gc *gin.Context) {
resp.Sections["ui"].Settings["language-admin"] = al resp.Sections["ui"].Settings["language-admin"] = al
resp.Sections["email"].Settings["language"] = el resp.Sections["email"].Settings["language"] = el
resp.Sections["password_resets"].Settings["language"] = pl resp.Sections["password_resets"].Settings["language"] = pl
resp.Sections["telegram"].Settings["language"] = tl
gc.JSON(200, resp) gc.JSON(200, resp)
} }
@ -1441,9 +1447,6 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
tempConfig.NewSection(section) tempConfig.NewSection(section)
} }
for setting, value := range settings.(map[string]interface{}) { for setting, value := range settings.(map[string]interface{}) {
if section == "email" && setting == "method" && value == "disabled" {
value = ""
}
if value.(string) != app.config.Section(section).Key(setting).MustString("") { if value.(string) != app.config.Section(section).Key(setting).MustString("") {
tempConfig.Section(section).Key(setting).SetValue(value.(string)) tempConfig.Section(section).Key(setting).SetValue(value.(string))
} }
@ -1485,7 +1488,6 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
// @Param lang query string false "Language for email titles." // @Param lang query string false "Language for email titles."
// @Success 200 {object} emailListDTO // @Success 200 {object} emailListDTO
// @Router /config/emails [get] // @Router /config/emails [get]
// @Security Bearer
// @tags Configuration // @tags Configuration
func (app *appContext) GetCustomEmails(gc *gin.Context) { func (app *appContext) GetCustomEmails(gc *gin.Context) {
lang := gc.Query("lang") lang := gc.Query("lang")
@ -1514,7 +1516,6 @@ func (app *appContext) GetCustomEmails(gc *gin.Context) {
// @Failure 500 {object} boolResponse // @Failure 500 {object} boolResponse
// @Param id path string true "ID of email" // @Param id path string true "ID of email"
// @Router /config/emails/{id} [post] // @Router /config/emails/{id} [post]
// @Security Bearer
// @tags Configuration // @tags Configuration
func (app *appContext) SetCustomEmail(gc *gin.Context) { func (app *appContext) SetCustomEmail(gc *gin.Context) {
var req customEmail var req customEmail
@ -1574,7 +1575,6 @@ func (app *appContext) SetCustomEmail(gc *gin.Context) {
// @Param enable/disable path string true "enable/disable" // @Param enable/disable path string true "enable/disable"
// @Param id path string true "ID of email" // @Param id path string true "ID of email"
// @Router /config/emails/{id}/state/{enable/disable} [post] // @Router /config/emails/{id}/state/{enable/disable} [post]
// @Security Bearer
// @tags Configuration // @tags Configuration
func (app *appContext) SetCustomEmailState(gc *gin.Context) { func (app *appContext) SetCustomEmailState(gc *gin.Context) {
id := gc.Param("id") id := gc.Param("id")
@ -1624,7 +1624,6 @@ func (app *appContext) SetCustomEmailState(gc *gin.Context) {
// @Failure 500 {object} boolResponse // @Failure 500 {object} boolResponse
// @Param id path string true "ID of email" // @Param id path string true "ID of email"
// @Router /config/emails/{id} [get] // @Router /config/emails/{id} [get]
// @Security Bearer
// @tags Configuration // @tags Configuration
func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) { func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
lang := app.storage.lang.chosenEmailLang lang := app.storage.lang.chosenEmailLang
@ -1813,7 +1812,6 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) {
// @Produce json // @Produce json
// @Success 200 {object} checkUpdateDTO // @Success 200 {object} checkUpdateDTO
// @Router /config/update [get] // @Router /config/update [get]
// @Security Bearer
// @tags Configuration // @tags Configuration
func (app *appContext) CheckUpdate(gc *gin.Context) { func (app *appContext) CheckUpdate(gc *gin.Context) {
if !app.newUpdate { if !app.newUpdate {
@ -1828,7 +1826,6 @@ func (app *appContext) CheckUpdate(gc *gin.Context) {
// @Success 400 {object} stringResponse // @Success 400 {object} stringResponse
// @Success 500 {object} boolResponse // @Success 500 {object} boolResponse
// @Router /config/update [post] // @Router /config/update [post]
// @Security Bearer
// @tags Configuration // @tags Configuration
func (app *appContext) ApplyUpdate(gc *gin.Context) { func (app *appContext) ApplyUpdate(gc *gin.Context) {
if !app.update.CanUpdate { if !app.update.CanUpdate {
@ -1854,7 +1851,6 @@ func (app *appContext) ApplyUpdate(gc *gin.Context) {
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse // @Failure 500 {object} stringResponse
// @Router /logout [post] // @Router /logout [post]
// @Security Bearer
// @tags Other // @tags Other
func (app *appContext) Logout(gc *gin.Context) { func (app *appContext) Logout(gc *gin.Context) {
cookie, err := gc.Cookie("refresh") cookie, err := gc.Cookie("refresh")
@ -1928,94 +1924,7 @@ func (app *appContext) ServeLang(gc *gin.Context) {
respondBool(400, false, gc) respondBool(400, false, gc)
} }
// @Summary Returns a new Telegram verification PIN, and the bot username. // @Summary Returns true/false on whether or not a telegram PIN was verified.
// @Produce json
// @Success 200 {object} telegramPinDTO
// @Router /telegram/pin [get]
// @Security Bearer
// @tags Other
func (app *appContext) TelegramGetPin(gc *gin.Context) {
gc.JSON(200, telegramPinDTO{
Token: app.telegram.NewAuthToken(),
Username: app.telegram.username,
})
}
// @Summary Link a Jellyfin & Telegram user together via a verification PIN.
// @Produce json
// @Param telegramSetDTO body telegramSetDTO true "Token and user's Jellyfin ID."
// @Success 200 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Router /users/telegram [post]
// @Security Bearer
// @tags Other
func (app *appContext) TelegramAddUser(gc *gin.Context) {
var req telegramSetDTO
gc.BindJSON(&req)
if req.Token == "" || req.ID == "" {
respondBool(400, false, gc)
return
}
tokenIndex := -1
for i, v := range app.telegram.verifiedTokens {
if v.Token == req.Token {
tokenIndex = i
break
}
}
if tokenIndex == -1 {
respondBool(500, false, gc)
return
}
tgToken := app.telegram.verifiedTokens[tokenIndex]
tgUser := TelegramUser{
ChatID: tgToken.ChatID,
Username: tgToken.Username,
Contact: true,
}
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
tgUser.Lang = lang
}
if app.storage.telegram == nil {
app.storage.telegram = map[string]TelegramUser{}
}
app.storage.telegram[req.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[tokenIndex] = app.telegram.verifiedTokens[tokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
}
respondBool(200, true, gc)
}
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
// @Produce json
// @Success 200 {object} boolResponse
// @Param pin path string true "PIN code to check"
// @Router /telegram/verified/{pin} [get]
// @Security Bearer
// @tags Other
func (app *appContext) TelegramVerified(gc *gin.Context) {
pin := gc.Param("pin")
tokenIndex := -1
for i, v := range app.telegram.verifiedTokens {
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]
// }
respondBool(200, tokenIndex != -1, gc)
}
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code.
// @Produce json // @Produce json
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
// @Success 401 {object} boolResponse // @Success 401 {object} boolResponse
@ -2023,7 +1932,7 @@ func (app *appContext) TelegramVerified(gc *gin.Context) {
// @Param invCode path string true "invite Code" // @Param invCode path string true "invite Code"
// @Router /invite/{invCode}/telegram/verified/{pin} [get] // @Router /invite/{invCode}/telegram/verified/{pin} [get]
// @tags Other // @tags Other
func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) { func (app *appContext) TelegramVerified(gc *gin.Context) {
code := gc.Param("invCode") code := gc.Param("invCode")
if _, ok := app.storage.invites[code]; !ok { if _, ok := app.storage.invites[code]; !ok {
respondBool(401, false, gc) respondBool(401, false, gc)
@ -2047,7 +1956,6 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
// @Summary Restarts the program. No response means success. // @Summary Restarts the program. No response means success.
// @Router /restart [post] // @Router /restart [post]
// @Security Bearer
// @tags Other // @tags Other
func (app *appContext) restart(gc *gin.Context) { func (app *appContext) restart(gc *gin.Context) {
app.info.Println("Restarting...") app.info.Println("Restarting...")

View File

@ -12,8 +12,6 @@ import (
) )
var emailEnabled = false var emailEnabled = false
var messagesEnabled = false
var telegramEnabled = false
func (app *appContext) GetPath(sect, key string) (fs.FS, string) { func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
val := app.config.Section(sect).Key(key).MustString("") val := app.config.Section(sect).Key(key).MustString("")
@ -85,19 +83,12 @@ func (app *appContext) loadConfig() error {
app.config.Section("jellyfin").Key("version").SetValue(version) app.config.Section("jellyfin").Key("version").SetValue(version)
app.config.Section("jellyfin").Key("device").SetValue("jfa-go") app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit)) app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false) if app.config.Section("email").Key("method").MustString("") == "" {
if !messagesEnabled {
emailEnabled = false
telegramEnabled = false
} else if app.config.Section("email").Key("method").MustString("") == "" {
emailEnabled = false emailEnabled = false
} else { } else {
emailEnabled = true emailEnabled = true
} }
if !emailEnabled && !telegramEnabled {
messagesEnabled = false
}
app.MustSetValue("updates", "enabled", "true") app.MustSetValue("updates", "enabled", "true")
releaseChannel := app.config.Section("updates").Key("channel").String() releaseChannel := app.config.Section("updates").Key("channel").String()
@ -143,28 +134,3 @@ func (app *appContext) loadConfig() error {
return nil return nil
} }
func (app *appContext) migrateEmailConfig() {
tempConfig, _ := ini.Load(app.configPath)
fmt.Println(warning("Part of your email configuration will be migrated to the new \"messages\" section.\nA backup will be made."))
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
if err != nil {
app.err.Fatalf("Failed to backup config: %v", err)
return
}
for _, setting := range []string{"use_24h", "date_format", "message"} {
if val := app.config.Section("email").Key(setting).Value(); val != "" {
tempConfig.Section("email").Key(setting).SetValue("")
tempConfig.Section("messages").Key(setting).SetValue(val)
}
}
if app.config.Section("messages").Key("enabled").MustBool(false) || app.config.Section("telegram").Key("enabled").MustBool(false) {
tempConfig.Section("messages").Key("enabled").SetValue("true")
}
err = tempConfig.SaveTo(app.configPath)
if err != nil {
app.err.Fatalf("Failed to save config: %v", err)
return
}
app.loadConfig()
}

View File

@ -345,20 +345,33 @@
} }
} }
}, },
"messages": { "email": {
"order": [], "order": [],
"meta": { "meta": {
"name": "Messages/Notifications", "name": "Email",
"description": "General settings for emails/messages." "description": "General email settings."
}, },
"settings": { "settings": {
"enabled": { "language": {
"name": "Enabled", "name": "Email Language",
"required": true, "required": false,
"requires_restart": true, "requires_restart": false,
"depends_true": "method",
"type": "select",
"options": [
["en-us", "English (US)"]
],
"value": "en-us",
"description": "Default email language. Submit a PR on github if you'd like to translate."
},
"no_username": {
"name": "Use email addresses as username",
"required": false,
"requires_restart": false,
"depends_true": "method",
"type": "bool", "type": "bool",
"value": true, "value": false,
"description": "Enable the sending of emails/messages such as password resets, announcements, etc." "description": "Use email address from invite form as username on Jellyfin."
}, },
"use_24h": { "use_24h": {
"name": "Use 24h time", "name": "Use 24h time",
@ -386,37 +399,6 @@
"type": "text", "type": "text",
"value": "Need help? contact me.", "value": "Need help? contact me.",
"description": "Message displayed at bottom of emails." "description": "Message displayed at bottom of emails."
}
}
},
"email": {
"order": [],
"meta": {
"name": "Email",
"description": "General email settings.",
"depends_true": "messages|enabled"
},
"settings": {
"language": {
"name": "Email Language",
"required": false,
"requires_restart": false,
"depends_true": "method",
"type": "select",
"options": [
["en-us", "English (US)"]
],
"value": "en-us",
"description": "Default email language. Submit a PR on github if you'd like to translate."
},
"no_username": {
"name": "Use email addresses as username",
"required": false,
"requires_restart": false,
"depends_true": "method",
"type": "bool",
"value": false,
"description": "Use email address from invite form as username on Jellyfin."
}, },
"method": { "method": {
"name": "Email method", "name": "Email method",
@ -461,143 +443,12 @@
} }
} }
}, },
"mailgun": {
"order": [],
"meta": {
"name": "Mailgun (Email)",
"description": "Mailgun API connection settings",
"depends_true": "email|method"
},
"settings": {
"api_url": {
"name": "API URL",
"required": false,
"requires_restart": false,
"type": "text",
"value": "https://api.mailgun.net..."
},
"api_key": {
"name": "API Key",
"required": false,
"requires_restart": false,
"type": "text",
"value": "your api key"
}
}
},
"smtp": {
"order": [],
"meta": {
"name": "SMTP (Email)",
"description": "SMTP Server connection settings.",
"depends_true": "email|method"
},
"settings": {
"username": {
"name": "Username",
"required": false,
"requires_restart": false,
"type": "text",
"value": "",
"description": "Username for SMTP. Leave blank to user send from address as username."
},
"encryption": {
"name": "Encryption Method",
"required": false,
"requires_restart": false,
"type": "select",
"options": [
["ssl_tls", "SSL/TLS"],
["starttls", "STARTTLS"]
],
"value": "starttls",
"description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls."
},
"server": {
"name": "Server address",
"required": false,
"requires_restart": false,
"type": "text",
"value": "smtp.jellyf.in",
"description": "SMTP Server address."
},
"port": {
"name": "Port",
"required": false,
"requires_restart": false,
"type": "number",
"value": 465
},
"password": {
"name": "Password",
"required": false,
"requires_restart": false,
"type": "password",
"value": "smtp password"
},
"ssl_cert": {
"name": "Path to custom SSL certificate",
"required": false,
"requires_restart": false,
"advanced": true,
"type": "text",
"value": "",
"description": "Use if your SMTP server's SSL Certificate is not trusted by the system."
}
}
},
"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.\nSee the jfa-go wiki for setting up a bot."
},
"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."
}
}
},
"password_resets": { "password_resets": {
"order": [], "order": [],
"meta": { "meta": {
"name": "Password Resets", "name": "Password Resets",
"description": "Settings for the password reset handler.", "description": "Settings for the password reset handler.",
"depends_true": "messages|enabled" "depends_true": "email|method"
}, },
"settings": { "settings": {
"enabled": { "enabled": {
@ -729,7 +580,7 @@
"meta": { "meta": {
"name": "Notifications", "name": "Notifications",
"description": "Notification related settings.", "description": "Notification related settings.",
"depends_true": "messages|enabled" "depends_true": "email|method"
}, },
"settings": { "settings": {
"enabled": { "enabled": {
@ -782,6 +633,91 @@
} }
} }
}, },
"mailgun": {
"order": [],
"meta": {
"name": "Mailgun (Email)",
"description": "Mailgun API connection settings",
"depends_true": "email|method"
},
"settings": {
"api_url": {
"name": "API URL",
"required": false,
"requires_restart": false,
"type": "text",
"value": "https://api.mailgun.net..."
},
"api_key": {
"name": "API Key",
"required": false,
"requires_restart": false,
"type": "text",
"value": "your api key"
}
}
},
"smtp": {
"order": [],
"meta": {
"name": "SMTP (Email)",
"description": "SMTP Server connection settings.",
"depends_true": "email|method"
},
"settings": {
"username": {
"name": "Username",
"required": false,
"requires_restart": false,
"type": "text",
"value": "",
"description": "Username for SMTP. Leave blank to user send from address as username."
},
"encryption": {
"name": "Encryption Method",
"required": false,
"requires_restart": false,
"type": "select",
"options": [
["ssl_tls", "SSL/TLS"],
["starttls", "STARTTLS"]
],
"value": "starttls",
"description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls."
},
"server": {
"name": "Server address",
"required": false,
"requires_restart": false,
"type": "text",
"value": "smtp.jellyf.in",
"description": "SMTP Server address."
},
"port": {
"name": "Port",
"required": false,
"requires_restart": false,
"type": "number",
"value": 465
},
"password": {
"name": "Password",
"required": false,
"requires_restart": false,
"type": "password",
"value": "smtp password"
},
"ssl_cert": {
"name": "Path to custom SSL certificate",
"required": false,
"requires_restart": false,
"advanced": true,
"type": "text",
"value": "",
"description": "Use if your SMTP server's SSL Certificate is not trusted by the system."
}
}
},
"ombi": { "ombi": {
"order": [], "order": [],
"meta": { "meta": {
@ -820,9 +756,9 @@
"welcome_email": { "welcome_email": {
"order": [], "order": [],
"meta": { "meta": {
"name": "Welcome Message", "name": "Welcome Emails",
"description": "Optionally send a welcome message to new users with the Jellyfin URL and their username.", "description": "Optionally send a welcome email to new users with the Jellyfin URL and their username.",
"depends_true": "messages|enabled" "depends_true": "email|method"
}, },
"settings": { "settings": {
"enabled": { "enabled": {
@ -929,14 +865,14 @@
"requires_restart": false, "requires_restart": false,
"type": "bool", "type": "bool",
"value": true, "value": true,
"depends_true": "messages|enabled", "depends_true": "email|method",
"description": "Send an email when a user's account expires." "description": "Send an email when a user's account expires."
}, },
"subject": { "subject": {
"name": "Email subject", "name": "Email subject",
"required": false, "required": false,
"requires_restart": false, "requires_restart": false,
"depends_true": "messages|enabled", "depends_true": "email|method",
"type": "text", "type": "text",
"value": "", "value": "",
"description": "Subject of user expiry emails." "description": "Subject of user expiry emails."
@ -946,7 +882,7 @@
"required": false, "required": false,
"requires_restart": false, "requires_restart": false,
"advanced": true, "advanced": true,
"depends_true": "messages|enabled", "depends_true": "email|method",
"type": "text", "type": "text",
"value": "", "value": "",
"description": "Path to custom email html" "description": "Path to custom email html"
@ -956,7 +892,7 @@
"required": false, "required": false,
"requires_restart": false, "requires_restart": false,
"advanced": true, "advanced": true,
"depends_true": "messages|enabled", "depends_true": "email|method",
"type": "text", "type": "text",
"value": "", "value": "",
"description": "Path to custom email in plain text" "description": "Path to custom email in plain text"
@ -968,7 +904,7 @@
"meta": { "meta": {
"name": "Account Disabling/Enabling", "name": "Account Disabling/Enabling",
"description": "Subject/email files for account disabling/enabling emails.", "description": "Subject/email files for account disabling/enabling emails.",
"depends_true": "messages|enabled" "depends_true": "email|method"
}, },
"settings": { "settings": {
"subject_disabled": { "subject_disabled": {
@ -1030,7 +966,7 @@
"meta": { "meta": {
"name": "Account Deletion", "name": "Account Deletion",
"description": "Subject/email files for account deletion emails.", "description": "Subject/email files for account deletion emails.",
"depends_true": "messages|enabled" "depends_true": "email|method"
}, },
"settings": { "settings": {
"subject": { "subject": {
@ -1061,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": { "files": {
"order": [], "order": [],
"meta": { "meta": {

View File

@ -39,11 +39,6 @@
} }
} }
.chip.btn:hover:not([disabled]):not(.textarea),
.chip.btn:focus:not([disabled]):not(.textarea) {
filter: brightness(var(--button-filter-brightness,95%));
}
.banner { .banner {
margin: calc(-1 * var(--spacing-4,1rem)); margin: calc(-1 * var(--spacing-4,1rem));
} }

118
email.go
View File

@ -9,7 +9,6 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"io/fs"
"net/smtp" "net/smtp"
"os" "os"
"strconv" "strconv"
@ -103,10 +102,9 @@ type Emailer struct {
// Message stores content. // Message stores content.
type Message struct { type Message struct {
Subject string `json:"subject"` Subject string `json:"subject"`
HTML string `json:"html"` HTML string `json:"html"`
Text string `json:"text"` Text string `json:"text"`
Markdown string `json:"markdown"`
} }
func (emailer *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern, timePattern string) (d, t, expiresIn string) { func (emailer *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern, timePattern string) (d, t, expiresIn string) {
@ -206,7 +204,7 @@ type templ interface {
Execute(wr io.Writer, data interface{}) error Execute(wr io.Writer, data interface{}) error
} }
func (emailer *Emailer) construct(app *appContext, section, keyFragment string, data map[string]interface{}) (html, text, markdown string, err error) { func (emailer *Emailer) construct(app *appContext, section, keyFragment string, data map[string]interface{}) (html, text string, err error) {
var tpl templ var tpl templ
if substituteStrings == "" { if substituteStrings == "" {
data["jellyfin"] = "Jellyfin" data["jellyfin"] = "Jellyfin"
@ -214,31 +212,14 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
data["jellyfin"] = substituteStrings data["jellyfin"] = substituteStrings
} }
var keys []string var keys []string
plaintext := app.config.Section("email").Key("plaintext").MustBool(false) if app.config.Section("email").Key("plaintext").MustBool(false) {
telegram := app.config.Section("telegram").Key("enabled").MustBool(false) keys = []string{"text"}
if plaintext { text = ""
if telegram {
keys = []string{"text"}
text, markdown = "", ""
} else {
keys = []string{"text"}
text = ""
}
} else { } else {
if telegram { keys = []string{"html", "text"}
keys = []string{"html", "text", "markdown"}
} else {
keys = []string{"html", "text"}
}
} }
for _, key := range keys { for _, key := range keys {
var filesystem fs.FS filesystem, fpath := app.GetPath(section, keyFragment+key)
var fpath string
if key == "markdown" {
filesystem, fpath = app.GetPath(section, keyFragment+"text")
} else {
filesystem, fpath = app.GetPath(section, keyFragment+key)
}
if key == "html" { if key == "html" {
tpl, err = template.ParseFS(filesystem, fpath) tpl, err = template.ParseFS(filesystem, fpath)
} else { } else {
@ -247,28 +228,15 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
if err != nil { if err != nil {
return return
} }
// For constructTemplate, if "md" is found in data it's used in stead of "text".
foundMarkdown := false
if key == "markdown" {
_, foundMarkdown = data["md"]
if foundMarkdown {
data["plaintext"], data["md"] = data["md"], data["plaintext"]
}
}
var tplData bytes.Buffer var tplData bytes.Buffer
err = tpl.Execute(&tplData, data) err = tpl.Execute(&tplData, data)
if err != nil { if err != nil {
return return
} }
if foundMarkdown {
data["plaintext"], data["md"] = data["md"], data["plaintext"]
}
if key == "html" { if key == "html" {
html = tplData.String() html = tplData.String()
} else if key == "text" {
text = tplData.String()
} else { } else {
markdown = tplData.String() text = tplData.String()
} }
} }
return return
@ -289,7 +257,7 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
template[v] = "{" + v + "}" template[v] = "{" + v + "}"
} }
} else { } else {
message := app.config.Section("messages").Key("message").String() message := app.config.Section("email").Key("message").String()
inviteLink := app.config.Section("invite_emails").Key("url_base").String() inviteLink := app.config.Section("invite_emails").Key("url_base").String()
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key) inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key)
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username}) template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username})
@ -314,7 +282,7 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
) )
email, err = emailer.constructTemplate(email.Subject, content, app) email, err = emailer.constructTemplate(email.Subject, content, app)
} else { } else {
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "email_confirmation", "email_", template) email.HTML, email.Text, err = emailer.construct(app, "email_confirmation", "email_", template)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -327,13 +295,12 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (
renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants}) renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
html := markdown.ToHTML([]byte(md), nil, renderer) html := markdown.ToHTML([]byte(md), nil, renderer)
text := stripMarkdown(md) text := stripMarkdown(md)
message := app.config.Section("messages").Key("message").String() message := app.config.Section("email").Key("message").String()
var err error var err error
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "template_email", "email_", map[string]interface{}{ email.HTML, email.Text, err = emailer.construct(app, "template_email", "email_", map[string]interface{}{
"text": template.HTML(html), "text": template.HTML(html),
"plaintext": text, "plaintext": text,
"message": message, "message": message,
"md": md,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -344,7 +311,7 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (
func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext, noSub bool) map[string]interface{} { func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext, noSub bool) map[string]interface{} {
expiry := invite.ValidTill expiry := invite.ValidTill
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern) d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
message := app.config.Section("messages").Key("message").String() message := app.config.Section("email").Key("message").String()
inviteLink := app.config.Section("invite_emails").Key("url_base").String() inviteLink := app.config.Section("invite_emails").Key("url_base").String()
inviteLink = fmt.Sprintf("%s/%s", inviteLink, code) inviteLink = fmt.Sprintf("%s/%s", inviteLink, code)
template := map[string]interface{}{ template := map[string]interface{}{
@ -386,7 +353,7 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
) )
email, err = emailer.constructTemplate(email.Subject, content, app) email, err = emailer.constructTemplate(email.Subject, content, app)
} else { } else {
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "invite_emails", "email_", template) email.HTML, email.Text, err = emailer.construct(app, "invite_emails", "email_", template)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -425,7 +392,7 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
) )
email, err = emailer.constructTemplate(email.Subject, content, app) email, err = emailer.constructTemplate(email.Subject, content, app)
} else { } else {
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "notifications", "expiry_", template) email.HTML, email.Text, err = emailer.construct(app, "notifications", "expiry_", template)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -479,7 +446,7 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
) )
email, err = emailer.constructTemplate(email.Subject, content, app) email, err = emailer.constructTemplate(email.Subject, content, app)
} else { } else {
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "notifications", "created_", template) email.HTML, email.Text, err = emailer.construct(app, "notifications", "created_", template)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -489,7 +456,7 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bool) map[string]interface{} { func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bool) map[string]interface{} {
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern) d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
message := app.config.Section("messages").Key("message").String() message := app.config.Section("email").Key("message").String()
template := map[string]interface{}{ template := map[string]interface{}{
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"), "someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
@ -553,7 +520,7 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
) )
email, err = emailer.constructTemplate(email.Subject, content, app) email, err = emailer.constructTemplate(email.Subject, content, app)
} else { } else {
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "password_resets", "email_", template) email.HTML, email.Text, err = emailer.construct(app, "password_resets", "email_", template)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -574,7 +541,7 @@ func (emailer *Emailer) deletedValues(reason string, app *appContext, noSub bool
} }
} else { } else {
template["reason"] = reason template["reason"] = reason
template["message"] = app.config.Section("messages").Key("message").String() template["message"] = app.config.Section("email").Key("message").String()
} }
return template return template
} }
@ -594,7 +561,7 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
) )
email, err = emailer.constructTemplate(email.Subject, content, app) email, err = emailer.constructTemplate(email.Subject, content, app)
} else { } else {
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "deletion", "email_", template) email.HTML, email.Text, err = emailer.construct(app, "deletion", "email_", template)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -615,7 +582,7 @@ func (emailer *Emailer) disabledValues(reason string, app *appContext, noSub boo
} }
} else { } else {
template["reason"] = reason template["reason"] = reason
template["message"] = app.config.Section("messages").Key("message").String() template["message"] = app.config.Section("email").Key("message").String()
} }
return template return template
} }
@ -635,7 +602,7 @@ func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub
) )
email, err = emailer.constructTemplate(email.Subject, content, app) email, err = emailer.constructTemplate(email.Subject, content, app)
} else { } else {
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "disable_enable", "disabled_", template) email.HTML, email.Text, err = emailer.construct(app, "disable_enable", "disabled_", template)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -656,7 +623,7 @@ func (emailer *Emailer) enabledValues(reason string, app *appContext, noSub bool
} }
} else { } else {
template["reason"] = reason template["reason"] = reason
template["message"] = app.config.Section("messages").Key("message").String() template["message"] = app.config.Section("email").Key("message").String()
} }
return template return template
} }
@ -676,7 +643,7 @@ func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub b
) )
email, err = emailer.constructTemplate(email.Subject, content, app) email, err = emailer.constructTemplate(email.Subject, content, app)
} else { } else {
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "disable_enable", "enabled_", template) email.HTML, email.Text, err = emailer.construct(app, "disable_enable", "enabled_", template)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -701,7 +668,7 @@ func (emailer *Emailer) welcomeValues(username string, expiry time.Time, app *ap
} else { } else {
template["jellyfinURL"] = app.config.Section("jellyfin").Key("public_server").String() template["jellyfinURL"] = app.config.Section("jellyfin").Key("public_server").String()
template["username"] = username template["username"] = username
template["message"] = app.config.Section("messages").Key("message").String() template["message"] = app.config.Section("email").Key("message").String()
exp := app.formatDatetime(expiry) exp := app.formatDatetime(expiry)
if !expiry.IsZero() { if !expiry.IsZero() {
if custom { if custom {
@ -741,7 +708,7 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app
) )
email, err = emailer.constructTemplate(email.Subject, content, app) email, err = emailer.constructTemplate(email.Subject, content, app)
} else { } else {
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "welcome_email", "email_", template) email.HTML, email.Text, err = emailer.construct(app, "welcome_email", "email_", template)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -756,7 +723,7 @@ func (emailer *Emailer) userExpiredValues(app *appContext, noSub bool) map[strin
"message": "", "message": "",
} }
if !noSub { if !noSub {
template["message"] = app.config.Section("messages").Key("message").String() template["message"] = app.config.Section("email").Key("message").String()
} }
return template return template
} }
@ -776,7 +743,7 @@ func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Mess
) )
email, err = emailer.constructTemplate(email.Subject, content, app) email, err = emailer.constructTemplate(email.Subject, content, app)
} else { } else {
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "user_expiry", "email_", template) email.HTML, email.Text, err = emailer.construct(app, "user_expiry", "email_", template)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -788,28 +755,3 @@ func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Mess
func (emailer *Emailer) send(email *Message, address ...string) error { func (emailer *Emailer) send(email *Message, address ...string) error {
return emailer.sender.Send(emailer.fromName, emailer.fromAddr, email, address...) return emailer.sender.Send(emailer.fromName, emailer.fromAddr, email, address...)
} }
func (app *appContext) sendByID(email *Message, ID ...string) error {
for _, id := range ID {
var err error
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
err = app.telegram.Send(email, tgChat.ChatID)
} else if address, ok := app.storage.emails[id]; ok {
err = app.email.send(email, address.(string))
}
if err != nil {
return err
}
}
return nil
}
func (app *appContext) getAddressOrName(jfID string) string {
if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled {
return "@" + tgChat.Username
}
if addr, ok := app.storage.emails[jfID]; ok {
return addr.(string)
}
return ""
}

View File

@ -6,7 +6,6 @@
window.URLBase = "{{ .urlBase }}"; window.URLBase = "{{ .urlBase }}";
window.notificationsEnabled = {{ .notifications }}; window.notificationsEnabled = {{ .notifications }};
window.emailEnabled = {{ .email_enabled }}; window.emailEnabled = {{ .email_enabled }};
window.telegramEnabled = {{ .telegram_enabled }};
window.ombiEnabled = {{ .ombiEnabled }}; window.ombiEnabled = {{ .ombiEnabled }};
window.usernameEnabled = {{ .username }}; window.usernameEnabled = {{ .username }};
window.langFile = JSON.parse({{ .language }}); window.langFile = JSON.parse({{ .language }});
@ -180,8 +179,8 @@
</div> </div>
<div id="modal-customize" class="modal"> <div id="modal-customize" class="modal">
<div class="modal-content card"> <div class="modal-content card">
<span class="heading">{{ .strings.customizeMessages }} <span class="modal-close">&times;</span></span> <span class="heading">{{ .strings.customizeEmails }} <span class="modal-close">&times;</span></span>
<p class="content">{{ .strings.customizeMessagesDescription }}</p> <p class="content">{{ .strings.customizeEmailsDescription }}</p>
<div class="table-responsive"> <div class="table-responsive">
<table class="table"> <table class="table">
<thead> <thead>
@ -309,24 +308,6 @@
<span class="button ~urge !normal full-width center" id="update-update">{{ .strings.update }}</span> <span class="button ~urge !normal full-width center" id="update-update">{{ .strings.update }}</span>
</div> </div>
</div> </div>
{{ if .telegram_enabled }}
<div id="modal-telegram" class="modal">
<div class="modal-content card">
<span class="heading mb-1">{{ .strings.linkTelegram }}</span>
<p class="content mb-1">{{ .strings.sendPIN }}</p>
<h1 class="ac" id="telegram-pin"></h1>
<a class="subheading link-center" id="telegram-link" target="_blank">
<span class="shield ~info mr-1">
<span class="icon">
<i class="ri-telegram-line"></i>
</span>
</span>
&#64;<span id="telegram-username">
</a>
<span class="button ~info !normal full-width center mt-1" id="telegram-waiting">{{ .strings.success }}</span>
</div>
</div>
{{ end }}
<div id="notification-box"></div> <div id="notification-box"></div>
<span class="dropdown" tabindex="0" id="lang-dropdown"> <span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button"> <span class="button ~urge dropdown-button">
@ -522,9 +503,6 @@
<th><input type="checkbox" value="" id="accounts-select-all"></th> <th><input type="checkbox" value="" id="accounts-select-all"></th>
<th>{{ .strings.username }}</th> <th>{{ .strings.username }}</th>
<th>{{ .strings.emailAddress }}</th> <th>{{ .strings.emailAddress }}</th>
{{ if .telegram_enabled }}
<th>Telegram</th>
{{ end }}
<th>{{ .strings.expiry }}</th> <th>{{ .strings.expiry }}</th>
<th>{{ .strings.lastActiveTime }}</th> <th>{{ .strings.lastActiveTime }}</th>
</tr> </tr>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -69,8 +69,8 @@
"preview": "Vorschau", "preview": "Vorschau",
"reset": "Zurücksetzen", "reset": "Zurücksetzen",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"customizeMessages": "E-Mails anpassen", "customizeEmails": "E-Mails anpassen",
"customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.", "customizeEmailsDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
"announce": "Ankündigen", "announce": "Ankündigen",
"subject": "E-Mail-Betreff", "subject": "E-Mail-Betreff",
"message": "Nachricht", "message": "Nachricht",

View File

@ -69,9 +69,9 @@
"preview": "Προεπισκόπηση", "preview": "Προεπισκόπηση",
"reset": "Επαναφορά", "reset": "Επαναφορά",
"edit": "Επεξεργασία", "edit": "Επεξεργασία",
"customizeMessages": "Παραμετροποίηση Emails", "customizeEmails": "Παραμετροποίηση Emails",
"advancedSettings": "Προχωρημένες Ρυθμίσεις", "advancedSettings": "Προχωρημένες Ρυθμίσεις",
"customizeMessagesDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.", "customizeEmailsDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",
"updates": "Ενημερώσεις", "updates": "Ενημερώσεις",
"update": "Ενημέρωση", "update": "Ενημέρωση",
"download": "Λήψη", "download": "Λήψη",

View File

@ -46,7 +46,7 @@
"unknown": "Unknown", "unknown": "Unknown",
"label": "Label", "label": "Label",
"announce": "Announce", "announce": "Announce",
"subject": "Subject", "subject": "Email Subject",
"message": "Message", "message": "Message",
"variables": "Variables", "variables": "Variables",
"conditionals": "Conditionals", "conditionals": "Conditionals",
@ -55,13 +55,13 @@
"edit": "Edit", "edit": "Edit",
"donate": "Donate", "donate": "Donate",
"extendExpiry": "Extend expiry", "extendExpiry": "Extend expiry",
"customizeMessages": "Customize Messages", "customizeEmails": "Customize Emails",
"customizeMessagesDescription": "If you don't want to use jfa-go's message templates, you can create your own using Markdown.", "customizeEmailsDescription": "If you don't want to use jfa-go's email templates, you can create your own using Markdown.",
"markdownSupported": "Markdown is supported.", "markdownSupported": "Markdown is supported.",
"modifySettings": "Modify Settings", "modifySettings": "Modify Settings",
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.", "modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
"applyHomescreenLayout": "Apply homescreen layout", "applyHomescreenLayout": "Apply homescreen layout",
"sendDeleteNotificationEmail": "Send notification message", "sendDeleteNotificationEmail": "Send notification email",
"sendDeleteNotifiationExample": "Your account has been deleted.", "sendDeleteNotifiationExample": "Your account has been deleted.",
"settingsRestart": "Restart", "settingsRestart": "Restart",
"settingsRestarting": "Restarting…", "settingsRestarting": "Restarting…",
@ -92,8 +92,7 @@
"inviteExpiresInTime": "Expires in {n}", "inviteExpiresInTime": "Expires in {n}",
"notifyEvent": "Notify on:", "notifyEvent": "Notify on:",
"notifyInviteExpiry": "On expiry", "notifyInviteExpiry": "On expiry",
"notifyUserCreation": "On user creation", "notifyUserCreation": "On user creation"
"sendPIN": "Ask the user to send the PIN below to the bot."
}, },
"notifications": { "notifications": {
"changedEmailAddress": "Changed email address of {n}.", "changedEmailAddress": "Changed email address of {n}.",
@ -105,7 +104,6 @@
"setOmbiDefaults": "Stored ombi defaults.", "setOmbiDefaults": "Stored ombi defaults.",
"updateApplied": "Update applied, please restart.", "updateApplied": "Update applied, please restart.",
"updateAppliedRefresh": "Update applied, please refresh.", "updateAppliedRefresh": "Update applied, please refresh.",
"telegramVerified": "Telegram account verified.",
"errorConnection": "Couldn't connect to jfa-go.", "errorConnection": "Couldn't connect to jfa-go.",
"error401Unauthorized": "Unauthorized. Try refreshing the page.", "error401Unauthorized": "Unauthorized. Try refreshing the page.",
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.", "errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
@ -128,7 +126,7 @@
"errorFailureCheckLogs": "Failed (check console/logs)", "errorFailureCheckLogs": "Failed (check console/logs)",
"errorPartialFailureCheckLogs": "Partial failure (check console/logs)", "errorPartialFailureCheckLogs": "Partial failure (check console/logs)",
"errorUserCreated": "Failed to create user {n}.", "errorUserCreated": "Failed to create user {n}.",
"errorSendWelcomeEmail": "Failed to send welcome message (check console/logs)", "errorSendWelcomeEmail": "Failed to send welcome email (check console/logs)",
"errorApplyUpdate": "Failed to apply update, try manually.", "errorApplyUpdate": "Failed to apply update, try manually.",
"errorCheckUpdate": "Failed to check for update.", "errorCheckUpdate": "Failed to check for update.",
"updateAvailable": "A new update is available, check settings.", "updateAvailable": "A new update is available, check settings.",

View File

@ -53,8 +53,8 @@
"reset": "Reiniciar", "reset": "Reiniciar",
"edit": "Editar", "edit": "Editar",
"extendExpiry": "Extender el vencimiento", "extendExpiry": "Extender el vencimiento",
"customizeMessages": "Personalizar emails", "customizeEmails": "Personalizar emails",
"customizeMessagesDescription": "Si no desea utilizar las plantillas de correo electrónico de jfa-go, puede crear las suyas propias con Markdown.", "customizeEmailsDescription": "Si no desea utilizar las plantillas de correo electrónico de jfa-go, puede crear las suyas propias con Markdown.",
"markdownSupported": "Se admite Markdown.", "markdownSupported": "Se admite Markdown.",
"modifySettings": "Modificar configuración", "modifySettings": "Modificar configuración",
"modifySettingsDescription": "Aplique la configuración de un perfil existente u obténgalos directamente de un usuario.", "modifySettingsDescription": "Aplique la configuración de un perfil existente u obténgalos directamente de un usuario.",

View File

@ -68,15 +68,15 @@
"settingsRestarting": "Redémarrage…", "settingsRestarting": "Redémarrage…",
"settingsRestart": "Redémarrer", "settingsRestart": "Redémarrer",
"announce": "Annoncer", "announce": "Annoncer",
"subject": "Sujet", "subject": "Sujet du courriel",
"message": "Message", "message": "Message",
"markdownSupported": "Markdown est pris en charge.", "markdownSupported": "Markdown est pris en charge.",
"customizeMessagesDescription": "Si vous ne souhaitez pas utiliser les modèles d'e-mails de jfa-go, vous pouvez créer les vôtres à l'aide de Markdown.", "customizeEmailsDescription": "Si vous ne souhaitez pas utiliser les modèles d'e-mails de jfa-go, vous pouvez créer les vôtres à l'aide de Markdown.",
"variables": "Variables", "variables": "Variables",
"preview": "Aperçu", "preview": "Aperçu",
"reset": "Réinitialiser", "reset": "Réinitialiser",
"edit": "Éditer", "edit": "Éditer",
"customizeMessages": "Personnaliser les e-mails", "customizeEmails": "Personnaliser les e-mails",
"inviteDuration": "Durée de l'invitation", "inviteDuration": "Durée de l'invitation",
"enabled": "Activé", "enabled": "Activé",
"disabled": "Désactivé", "disabled": "Désactivé",

View File

@ -69,8 +69,8 @@
"preview": "Pratinjau", "preview": "Pratinjau",
"reset": "Setel ulang", "reset": "Setel ulang",
"edit": "Edit", "edit": "Edit",
"customizeMessages": "Sesuaikan Email", "customizeEmails": "Sesuaikan Email",
"customizeMessagesDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.", "customizeEmailsDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.",
"announce": "Mengumumkan", "announce": "Mengumumkan",
"subject": "Subjek Email", "subject": "Subjek Email",
"message": "Pesan", "message": "Pesan",

View File

@ -70,11 +70,11 @@
"subject": "E-mailonderwerp", "subject": "E-mailonderwerp",
"message": "Bericht", "message": "Bericht",
"variables": "Variabelen", "variables": "Variabelen",
"customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.", "customizeEmailsDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
"preview": "Voorbeeld", "preview": "Voorbeeld",
"reset": "Resetten", "reset": "Resetten",
"edit": "Bewerken", "edit": "Bewerken",
"customizeMessages": "E-mails aanpassen", "customizeEmails": "E-mails aanpassen",
"inviteDuration": "Geldigheidsduur uitnodiging", "inviteDuration": "Geldigheidsduur uitnodiging",
"userExpiryDescription": "Een bepaalde tijd na elke aanmelding, wordt de account verwijderd/uitgeschakeld door jfa-go. Dit kan aangepast worden in de instellingen.", "userExpiryDescription": "Een bepaalde tijd na elke aanmelding, wordt de account verwijderd/uitgeschakeld door jfa-go. Dit kan aangepast worden in de instellingen.",
"enabled": "Ingeschakeld", "enabled": "Ingeschakeld",

View File

@ -69,12 +69,12 @@
"subject": "Assunto do email", "subject": "Assunto do email",
"message": "Mensagem", "message": "Mensagem",
"markdownSupported": "Suporte a Markdown.", "markdownSupported": "Suporte a Markdown.",
"customizeMessagesDescription": "Se não quiser usar os modelos de email do jfa-go, você pode criar o seu próprio usando o Markdown.", "customizeEmailsDescription": "Se não quiser usar os modelos de email do jfa-go, você pode criar o seu próprio usando o Markdown.",
"variables": "Variáveis", "variables": "Variáveis",
"preview": "Pre-visualizar", "preview": "Pre-visualizar",
"reset": "Reiniciar", "reset": "Reiniciar",
"edit": "Editar", "edit": "Editar",
"customizeMessages": "Customizar Emails", "customizeEmails": "Customizar Emails",
"disabled": "Desativado", "disabled": "Desativado",
"userExpiryDescription": "Após um determinado período de tempo de cada inscrição, o jfa-go apagará/desabilitará a conta. Você pode alterar essa opção nas configurações.", "userExpiryDescription": "Após um determinado período de tempo de cada inscrição, o jfa-go apagará/desabilitará a conta. Você pode alterar essa opção nas configurações.",
"inviteDuration": "Duração do Convite", "inviteDuration": "Duração do Convite",

View File

@ -37,8 +37,8 @@
"preview": "Förhandsvisning", "preview": "Förhandsvisning",
"reset": "Återställ", "reset": "Återställ",
"edit": "Redigera", "edit": "Redigera",
"customizeMessages": "Anpassa e-post", "customizeEmails": "Anpassa e-post",
"customizeMessagesDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.", "customizeEmailsDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.",
"markdownSupported": "Markdown stöds.", "markdownSupported": "Markdown stöds.",
"modifySettings": "Ändra inställningar", "modifySettings": "Ändra inställningar",
"modifySettingsDescription": "Tillämpa inställningar från en befintlig profil eller kopiera dem direkt från en användare.", "modifySettingsDescription": "Tillämpa inställningar från en befintlig profil eller kopiera dem direkt från en användare.",

View File

@ -14,9 +14,6 @@
"copied": "Copied", "copied": "Copied",
"time24h": "24h Time", "time24h": "24h Time",
"time12h": "12h Time", "time12h": "12h Time",
"linkTelegram": "Link Telegram",
"contactEmail": "Contact through Email",
"contactTelegram": "Contact through Telegram",
"theme": "Theme" "theme": "Theme"
} }
} }

View File

@ -3,7 +3,7 @@
"name": "English (US)" "name": "English (US)"
}, },
"strings": { "strings": {
"ifItWasNotYou": "If this wasn't you, please ignore this.", "ifItWasNotYou": "If this wasn't you, please ignore this email.",
"helloUser": "Hi {username},", "helloUser": "Hi {username},",
"reason": "Reason" "reason": "Reason"
}, },
@ -55,7 +55,7 @@
"linkButton": "Setup your account" "linkButton": "Setup your account"
}, },
"welcomeEmail": { "welcomeEmail": {
"name": "Welcome", "name": "Welcome email",
"title": "Welcome to Jellyfin", "title": "Welcome to Jellyfin",
"welcome": "Welcome to Jellyfin!", "welcome": "Welcome to Jellyfin!",
"youCanLoginWith": "You can login with the details below", "youCanLoginWith": "You can login with the details below",

View File

@ -18,7 +18,10 @@
"confirmationRequired": "Email confirmation required", "confirmationRequired": "Email confirmation required",
"confirmationRequiredMessage": "Please check your email inbox to verify your address.", "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}.",
"sendPIN": "Send the PIN below to the bot, then come back here to link your account." "linkTelegram": "Link Telegram",
"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": { "notifications": {
"errorUserExists": "User already exists.", "errorUserExists": "User already exists.",

View File

@ -1,5 +0,0 @@
{
"meta": {
"name": "English (GB)"
}
}

View File

@ -1,11 +0,0 @@
{
"meta": {
"name": "English (US)"
},
"strings": {
"startMessage": "Hi!\nEnter your test PIN code here to verify your account.",
"languageMessage": "Note: See available languages with /lang, and set language with /lang <language code>.",
"invalidPIN": "That PIN was invalid, try again.",
"pinSuccess": "Success! You can now return to the sign-up page."
}
}

View File

@ -199,12 +199,6 @@ func start(asDaemon, firstCall bool) {
if err := app.loadConfig(); err != nil { if err := app.loadConfig(); err != nil {
app.err.Fatalf("Failed to load config file \"%s\": %v", app.configPath, err) app.err.Fatalf("Failed to load config file \"%s\": %v", app.configPath, err)
} }
// Some message settings have been moved from "email" to "messages", this will switch them.
if app.config.Section("email").Key("use_24h").Value() != "" {
app.migrateEmailConfig()
}
app.version = app.config.Section("jellyfin").Key("version").String() app.version = app.config.Section("jellyfin").Key("version").String()
// read from config... // read from config...
debugMode = app.config.Section("ui").Key("debug").MustBool(false) debugMode = app.config.Section("ui").Key("debug").MustBool(false)
@ -554,7 +548,7 @@ func start(asDaemon, firstCall bool) {
go app.checkForUpdates() go app.checkForUpdates()
} }
if telegramEnabled { if app.config.Section("telegram").Key("enabled").MustBool(false) {
app.telegram, err = newTelegramDaemon(app) app.telegram, err = newTelegramDaemon(app)
if err != nil { if err != nil {
app.err.Printf("Failed to authenticate with Telegram: %v", err) app.err.Printf("Failed to authenticate with Telegram: %v", err)

View File

@ -129,7 +129,6 @@ type respUser struct {
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time. Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
Disabled bool `json:"disabled"` // Whether or not the user is disabled. Disabled bool `json:"disabled"` // Whether or not the user is disabled.
Telegram string `json:"telegram"` // Telegram username (if known)
} }
type getUsersDTO struct { type getUsersDTO struct {
@ -237,13 +236,3 @@ type checkUpdateDTO struct {
New bool `json:"new"` // Whether or not there's a new update. New bool `json:"new"` // Whether or not there's a new update.
Update Update `json:"update"` Update Update `json:"update"`
} }
type telegramPinDTO struct {
Token string `json:"token" example:"A1-B2-3C"`
Username string `json:"username"`
}
type telegramSetDTO struct {
Token string `json:"token" example:"A1-B2-3C"`
ID string `json:"id"` // Jellyfin ID of user.
}

View File

@ -69,24 +69,27 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
return return
} }
app.storage.loadEmails() app.storage.loadEmails()
var address string
uid := user.ID uid := user.ID
if uid == "" { if uid == "" {
app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username) app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username)
return return
} }
name := app.getAddressOrName(uid) addr, ok := app.storage.emails[uid]
if name != "" { if !ok || addr == nil {
msg, err := app.email.constructReset(pwr, app, false) app.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username)
return
if err != nil { }
app.err.Printf("Failed to construct password reset message for %s", pwr.Username) address = addr.(string)
app.debug.Printf("%s: Error: %s", pwr.Username, err) msg, err := app.email.constructReset(pwr, app, false)
} else if err := app.sendByID(msg, uid); err != nil { if err != nil {
app.err.Printf("Failed to send password reset message to \"%s\"", name) app.err.Printf("Failed to construct password reset email for %s", pwr.Username)
app.debug.Printf("%s: Error: %s", pwr.Username, err) app.debug.Printf("%s: Error: %s", pwr.Username, err)
} else { } else if err := app.email.send(msg, address); err != nil {
app.info.Printf("Sent password reset message to \"%s\"", name) app.err.Printf("Failed to send password reset email to \"%s\"", address)
} app.debug.Printf("%s: Error: %s", pwr.Username, err)
} else {
app.info.Printf("Sent password reset email to \"%s\"", address)
} }
} else { } else {
app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry) app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry)

View File

@ -118,8 +118,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.POST(p+"/newUser", app.NewUser) router.POST(p+"/newUser", app.NewUser)
router.Use(static.Serve(p+"/invite/", app.webFS)) router.Use(static.Serve(p+"/invite/", app.webFS))
router.GET(p+"/invite/:invCode", app.InviteProxy) router.GET(p+"/invite/:invCode", app.InviteProxy)
if telegramEnabled { if app.config.Section("telegram").Key("enabled").MustBool(false) {
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite) router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerified)
} }
} }
if *SWAGGER { if *SWAGGER {
@ -158,11 +158,6 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/config", app.GetConfig) api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart) api.POST(p+"/restart", app.restart)
if telegramEnabled {
api.GET(p+"/telegram/pin", app.TelegramGetPin)
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
api.POST(p+"/users/telegram", app.TelegramAddUser)
}
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET(p+"/ombi/users", app.OmbiUsers) api.GET(p+"/ombi/users", app.OmbiUsers)
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults) api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)

View File

@ -29,7 +29,7 @@ func (app *appContext) ServeSetup(gc *gin.Context) {
"success_message": app.config.Section("ui").Key("success_message").String(), "success_message": app.config.Section("ui").Key("success_message").String(),
}, },
"email": { "email": {
"message": app.config.Section("messages").Key("message").String(), "message": app.config.Section("email").Key("message").String(),
}, },
} }
msg, err := json.Marshal(messages) msg, err := json.Marshal(messages)

View File

@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"math/rand" "math/rand"
"strconv"
"strings" "strings"
"time" "time"
@ -174,7 +175,6 @@ func (t *TelegramDaemon) run() {
} }
case <-t.ShutdownChannel: case <-t.ShutdownChannel:
t.bot.StopReceivingUpdates()
t.ShutdownChannel <- "Down" t.ShutdownChannel <- "Down"
return return
} }
@ -194,17 +194,15 @@ func (t *TelegramDaemon) QuoteReply(upd *tg.Update, content string) error {
return err return err
} }
// Send will send a telegram message to a list of chat IDs. message.text is used if no markdown is given. // Send adds compatibility with EmailClient, fromName/fromAddr are discarded, message.Text is used, addresses are Chat IDs as strings.
func (t *TelegramDaemon) Send(message *Message, ID ...int64) error { func (t *TelegramDaemon) Send(fromName, fromAddr string, message *Message, address ...string) error {
for _, id := range ID { for _, addr := range address {
var msg tg.MessageConfig ChatID, err := strconv.ParseInt(addr, 10, 64)
if message.Markdown == "" { if err != nil {
msg = tg.NewMessage(id, message.Text) return err
} else {
msg = tg.NewMessage(id, strings.ReplaceAll(message.Markdown, ".", "\\."))
msg.ParseMode = "MarkdownV2"
} }
_, err := t.bot.Send(msg) msg := tg.NewMessage(ChatID, message.Text)
_, err = t.bot.Send(msg)
if err != nil { if err != nil {
return err return err
} }

View File

@ -62,10 +62,6 @@ window.availableProfiles = window.availableProfiles || [];
window.modals.extendExpiry = new Modal(document.getElementById("modal-extend-expiry")); window.modals.extendExpiry = new Modal(document.getElementById("modal-extend-expiry"));
window.modals.updateInfo = new Modal(document.getElementById("modal-update")); window.modals.updateInfo = new Modal(document.getElementById("modal-update"));
if (window.telegramEnabled) {
window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
}
})(); })();
var inviteCreator = new createInvite(); var inviteCreator = new createInvite();

View File

@ -12,6 +12,7 @@ interface formWindow extends Window {
code: string; code: string;
messages: { [key: string]: string }; messages: { [key: string]: string };
confirmation: boolean; confirmation: boolean;
telegramEnabled: boolean;
telegramRequired: boolean; telegramRequired: boolean;
telegramPIN: string; telegramPIN: string;
userExpiryEnabled: boolean; userExpiryEnabled: boolean;
@ -53,10 +54,7 @@ if (window.telegramEnabled) {
toggleLoader(waiting); toggleLoader(waiting);
window.telegramModal.show(); window.telegramModal.show();
let modalClosed = false; let modalClosed = false;
window.telegramModal.onclose = () => { window.telegramModal.onclose = () => { modalClosed = true; }
modalClosed = true;
toggleLoader(waiting);
}
const checkVerified = () => _get("/invite/" + window.code + "/telegram/verified/" + window.telegramPIN, null, (req: XMLHttpRequest) => { const checkVerified = () => _get("/invite/" + window.code + "/telegram/verified/" + window.telegramPIN, null, (req: XMLHttpRequest) => {
if (req.readyState == 4) { if (req.readyState == 4) {
if (req.status == 401) { if (req.status == 401) {
@ -66,6 +64,7 @@ if (window.telegramEnabled) {
} else if (req.status == 200) { } else if (req.status == 200) {
if (req.response["success"] as boolean) { if (req.response["success"] as boolean) {
telegramVerified = true; telegramVerified = true;
toggleLoader(waiting);
waiting.classList.add("~positive"); waiting.classList.add("~positive");
waiting.classList.remove("~info"); waiting.classList.remove("~info");
window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]); window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]);

View File

@ -1,4 +1,4 @@
import { _get, _post, _delete, toggleLoader, addLoader, removeLoader, toDateString } from "../modules/common.js"; import { _get, _post, _delete, toggleLoader, toDateString } from "../modules/common.js";
import { templateEmail } from "../modules/settings.js"; import { templateEmail } from "../modules/settings.js";
import { Marked } from "@ts-stack/markdown"; import { Marked } from "@ts-stack/markdown";
import { stripMarkdown } from "../modules/stripmd.js"; import { stripMarkdown } from "../modules/stripmd.js";
@ -11,12 +11,6 @@ interface User {
admin: boolean; admin: boolean;
disabled: boolean; disabled: boolean;
expiry: number; expiry: number;
telegram: string;
}
interface getPinResponse {
token: string;
username: string;
} }
class user implements User { class user implements User {
@ -28,8 +22,6 @@ class user implements User {
private _email: HTMLInputElement; private _email: HTMLInputElement;
private _emailAddress: string; private _emailAddress: string;
private _emailEditButton: HTMLElement; private _emailEditButton: HTMLElement;
private _telegram: HTMLTableDataCellElement;
private _telegramUsername: string;
private _expiry: HTMLTableDataCellElement; private _expiry: HTMLTableDataCellElement;
private _expiryUnix: number; private _expiryUnix: number;
private _lastActive: HTMLTableDataCellElement; private _lastActive: HTMLTableDataCellElement;
@ -80,19 +72,6 @@ class user implements User {
} }
} }
get telegram(): string { return this._telegramUsername; }
set telegram(u: string) {
if (!window.telegramEnabled) return;
this._telegramUsername = u;
if (u == "") {
this._telegram.innerHTML = `<span class="chip btn !low">Add</span>`;
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
} else {
this._telegram.innerHTML = `<a href="https://t.me/${u}" target="_blank">@${u}</a>`;
}
}
get expiry(): number { return this._expiryUnix; } get expiry(): number { return this._expiryUnix; }
set expiry(unix: number) { set expiry(unix: number) {
this._expiryUnix = unix; this._expiryUnix = unix;
@ -118,21 +97,13 @@ class user implements User {
constructor(user: User) { constructor(user: User) {
this._row = document.createElement("tr") as HTMLTableRowElement; this._row = document.createElement("tr") as HTMLTableRowElement;
let innerHTML = ` this._row.innerHTML = `
<td><input type="checkbox" value=""></td> <td><input type="checkbox" value=""></td>
<td><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></td> <td><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></td>
<td><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></td> <td><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></td>
<td class="accounts-expiry"></td>
<td class="accounts-last-active"></td>
`; `;
if (window.telegramEnabled) {
innerHTML += `
<td class="accounts-telegram"></td>
`;
}
innerHTML += `
<td class="accounts-expiry"></td>
<td class="accounts-last-active"></td>
`;
this._row.innerHTML = innerHTML;
const emailEditor = `<input type="email" class="input ~neutral !normal stealth-input">`; const emailEditor = `<input type="email" class="input ~neutral !normal stealth-input">`;
this._check = this._row.querySelector("input[type=checkbox]") as HTMLInputElement; this._check = this._row.querySelector("input[type=checkbox]") as HTMLInputElement;
this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement; this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement;
@ -140,7 +111,6 @@ class user implements User {
this._disabled = this._row.querySelector(".accounts-disabled") as HTMLSpanElement; this._disabled = this._row.querySelector(".accounts-disabled") as HTMLSpanElement;
this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement; this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement;
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement; this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement;
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement; this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement; this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
this._check.onchange = () => { this.selected = this._check.checked; } this._check.onchange = () => { this.selected = this._check.checked; }
@ -199,55 +169,10 @@ class user implements User {
}); });
} }
private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 200) {
const pin = document.getElementById("telegram-pin");
const link = document.getElementById("telegram-link") as HTMLAnchorElement;
const username = document.getElementById("telegram-username") as HTMLSpanElement;
const waiting = document.getElementById("telegram-waiting") as HTMLSpanElement;
let resp = req.response as getPinResponse;
pin.textContent = resp.token;
link.href = "https://t.me/" + resp.username;
username.textContent = resp.username;
addLoader(waiting);
let modalClosed = false;
window.modals.telegram.onclose = () => {
modalClosed = true;
removeLoader(waiting);
}
let send = {
token: resp.token,
id: this.id
};
const checkVerified = () => _post("/users/telegram", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status == 200 && req.response["success"] as boolean) {
removeLoader(waiting);
waiting.classList.add("~positive");
waiting.classList.remove("~info");
window.notifications.customSuccess("telegramVerified", window.lang.notif("telegramVerified"));
setTimeout(() => {
window.modals.telegram.close();
waiting.classList.add("~info");
waiting.classList.remove("~positive");
}, 2000);
document.dispatchEvent(new CustomEvent("accounts-reload"));
} else if (!modalClosed) {
setTimeout(checkVerified, 1500);
}
}
}, true);
window.modals.telegram.show();
checkVerified();
}
});
update = (user: User) => { update = (user: User) => {
this.id = user.id; this.id = user.id;
this.name = user.name; this.name = user.name;
this.email = user.email || ""; this.email = user.email || "";
this.telegram = user.telegram;
this.last_active = user.last_active; this.last_active = user.last_active;
this.admin = user.admin; this.admin = user.admin;
this.disabled = user.disabled; this.disabled = user.disabled;
@ -263,6 +188,9 @@ class user implements User {
} }
} }
export class accountsList { export class accountsList {
private _table = document.getElementById("accounts-list") as HTMLTableSectionElement; private _table = document.getElementById("accounts-list") as HTMLTableSectionElement;
@ -406,7 +334,7 @@ export class accountsList {
this._selectAll.checked = false; this._selectAll.checked = false;
this._modifySettings.classList.add("unfocused"); this._modifySettings.classList.add("unfocused");
this._deleteUser.classList.add("unfocused"); this._deleteUser.classList.add("unfocused");
if (window.emailEnabled || window.telegramEnabled) { if (window.emailEnabled) {
this._announceButton.classList.add("unfocused"); this._announceButton.classList.add("unfocused");
} }
this._extendExpiry.classList.add("unfocused"); this._extendExpiry.classList.add("unfocused");
@ -428,7 +356,7 @@ export class accountsList {
this._modifySettings.classList.remove("unfocused"); this._modifySettings.classList.remove("unfocused");
this._deleteUser.classList.remove("unfocused"); this._deleteUser.classList.remove("unfocused");
this._deleteUser.textContent = window.lang.quantity("deleteUser", list.length); this._deleteUser.textContent = window.lang.quantity("deleteUser", list.length);
if (window.emailEnabled || window.telegramEnabled) { if (window.emailEnabled) {
this._announceButton.classList.remove("unfocused"); this._announceButton.classList.remove("unfocused");
} }
let anyNonExpiries = list.length == 0 ? true : false; let anyNonExpiries = list.length == 0 ? true : false;
@ -773,7 +701,6 @@ export class accountsList {
this._selectAll.onchange = () => { this._selectAll.onchange = () => {
this.selectAll = this._selectAll.checked; this.selectAll = this._selectAll.checked;
}; };
document.addEventListener("accounts-reload", this.reload);
document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); }); document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); });
document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); }); document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); });
this._addUserButton.onclick = window.modals.addUser.toggle; this._addUserButton.onclick = window.modals.addUser.toggle;

View File

@ -179,22 +179,3 @@ export function toggleLoader(el: HTMLElement, small: boolean = true) {
el.appendChild(dot); el.appendChild(dot);
} }
} }
export function addLoader(el: HTMLElement, small: boolean = true) {
if (!el.classList.contains("loader")) {
el.classList.add("loader");
if (small) { el.classList.add("loader-sm"); }
const dot = document.createElement("span") as HTMLSpanElement;
dot.classList.add("dot")
el.appendChild(dot);
}
}
export function removeLoader(el: HTMLElement, small: boolean = true) {
if (el.classList.contains("loader")) {
el.classList.remove("loader");
el.classList.remove("loader-sm");
const dot = el.querySelector("span.dot");
if (dot) { dot.remove(); }
}
}

View File

@ -560,18 +560,10 @@ export class settingsList {
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => { document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
if (Boolean(event.detail) !== state) { if (Boolean(event.detail) !== state) {
button.classList.add("unfocused"); button.classList.add("unfocused");
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false }));
} else { } else {
button.classList.remove("unfocused"); button.classList.remove("unfocused");
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: true }));
} }
}); });
document.addEventListener(`settings-${dependant[0]}`, (event: settingsBoolEvent) => {
if (Boolean(event.detail) !== state) {
button.classList.add("unfocused");
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false }));
}
});
} }
if (s.meta.advanced) { if (s.meta.advanced) {
document.addEventListener("settings-advancedState", (event: settingsBoolEvent) => { document.addEventListener("settings-advancedState", (event: settingsBoolEvent) => {
@ -677,7 +669,7 @@ export class settingsList {
if (name in this._sections) { if (name in this._sections) {
this._sections[name].update(settings.sections[name]); this._sections[name].update(settings.sections[name]);
} else { } else {
if (name == "messages") { if (name == "email") {
const editButton = document.createElement("div"); const editButton = document.createElement("div");
editButton.classList.add("tooltip", "left"); editButton.classList.add("tooltip", "left");
editButton.innerHTML = ` editButton.innerHTML = `
@ -685,7 +677,7 @@ export class settingsList {
<i class="icon ri-edit-line"></i> <i class="icon ri-edit-line"></i>
</span> </span>
<span class="content sm"> <span class="content sm">
${window.lang.get("strings", "customizeMessages")} ${window.lang.get("strings", "customizeEmails")}
</span> </span>
`; `;
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = this._emailEditor.showList; (editButton.querySelector("span.button") as HTMLSpanElement).onclick = this._emailEditor.showList;

View File

@ -4,8 +4,6 @@ declare interface Modal {
show: () => void; show: () => void;
close: (event?: Event) => void; close: (event?: Event) => void;
toggle: () => void; toggle: () => void;
onopen: (f: () => void) => void;
onclose: (f: () => void) => void;
} }
interface ArrayConstructor { interface ArrayConstructor {
@ -20,7 +18,6 @@ declare interface Window {
jfUsers: Array<Object>; jfUsers: Array<Object>;
notificationsEnabled: boolean; notificationsEnabled: boolean;
emailEnabled: boolean; emailEnabled: boolean;
telegramEnabled: boolean;
ombiEnabled: boolean; ombiEnabled: boolean;
usernameEnabled: boolean; usernameEnabled: boolean;
token: string; token: string;
@ -100,7 +97,6 @@ declare interface Modals {
customizeEmails: Modal; customizeEmails: Modal;
extendExpiry: Modal; extendExpiry: Modal;
updateInfo: Modal; updateInfo: Modal;
telegram: Modal;
} }
interface Invite { interface Invite {

View File

@ -71,9 +71,9 @@ func (app *appContext) checkUsers() {
mode = "delete" mode = "delete"
termPlural = "Deleting" termPlural = "Deleting"
} }
contact := false email := false
if messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) { if emailEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) {
contact = true email = true
} }
// Use a map to speed up checking for deleted users later // Use a map to speed up checking for deleted users later
userExists := map[string]bool{} userExists := map[string]bool{}
@ -114,18 +114,18 @@ func (app *appContext) checkUsers() {
} }
delete(app.storage.users, id) delete(app.storage.users, id)
app.jf.CacheExpiry = time.Now() app.jf.CacheExpiry = time.Now()
if contact { if email {
address, ok := app.storage.emails[id]
if !ok { if !ok {
continue continue
} }
name := app.getAddressOrName(user.ID)
msg, err := app.email.constructUserExpired(app, false) msg, err := app.email.constructUserExpired(app, false)
if err != nil { if err != nil {
app.err.Printf("Failed to construct expiry message for \"%s\": %s", user.Name, err) app.err.Printf("Failed to construct expiry email for \"%s\": %s", user.Name, err)
} else if err := app.sendByID(msg, user.ID); err != nil { } else if err := app.email.send(msg, address.(string)); err != nil {
app.err.Printf("Failed to send expiry message to \"%s\": %s", name, err) app.err.Printf("Failed to send expiry email to \"%s\": %s", user.Name, err)
} else { } else {
app.info.Printf("Sent expiry notification to \"%s\"", name) app.info.Printf("Sent expiry notification to \"%s\"", address.(string))
} }
} }
} }

View File

@ -116,21 +116,20 @@ func (app *appContext) AdminPage(gc *gin.Context) {
} }
license = string(l) license = string(l)
gcHTML(gc, http.StatusOK, "admin.html", gin.H{ gcHTML(gc, http.StatusOK, "admin.html", gin.H{
"urlBase": app.getURLBase(gc), "urlBase": app.getURLBase(gc),
"cssClass": app.cssClass, "cssClass": app.cssClass,
"contactMessage": "", "contactMessage": "",
"email_enabled": emailEnabled, "email_enabled": emailEnabled,
"telegram_enabled": telegramEnabled, "notifications": notificationsEnabled,
"notifications": notificationsEnabled, "version": version,
"version": version, "commit": commit,
"commit": commit, "ombiEnabled": ombiEnabled,
"ombiEnabled": ombiEnabled, "username": !app.config.Section("email").Key("no_username").MustBool(false),
"username": !app.config.Section("email").Key("no_username").MustBool(false), "strings": app.storage.lang.Admin[lang].Strings,
"strings": app.storage.lang.Admin[lang].Strings, "quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
"quantityStrings": app.storage.lang.Admin[lang].QuantityStrings, "language": app.storage.lang.Admin[lang].JSON,
"language": app.storage.lang.Admin[lang].JSON, "langName": lang,
"langName": lang, "license": license,
"license": license,
}) })
} }
@ -283,7 +282,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"userExpiryMinutes": inv.UserMinutes, "userExpiryMinutes": inv.UserMinutes,
"userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"), "userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang, "langName": lang,
"telegramEnabled": telegramEnabled, "telegramEnabled": app.config.Section("telegram").Key("enabled").MustBool(false),
} }
if data["telegramEnabled"].(bool) { if data["telegramEnabled"].(bool) {
data["telegramPIN"] = app.telegram.NewAuthToken() data["telegramPIN"] = app.telegram.NewAuthToken()