diff --git a/api-messages.go b/api-messages.go index f971717..dcaecc4 100644 --- a/api-messages.go +++ b/api-messages.go @@ -15,12 +15,16 @@ import ( // @Router /config/emails [get] // @Security Bearer // @tags Configuration -func (app *appContext) GetCustomEmails(gc *gin.Context) { +func (app *appContext) GetCustomContent(gc *gin.Context) { lang := gc.Query("lang") if _, ok := app.storage.lang.Email[lang]; !ok { lang = app.storage.lang.chosenEmailLang } - gc.JSON(200, emailListDTO{ + adminLang := lang + if _, ok := app.storage.lang.Admin[lang]; !ok { + adminLang = app.storage.lang.chosenAdminLang + } + list := emailListDTO{ "UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled}, "InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled}, "PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled}, @@ -31,13 +35,25 @@ func (app *appContext) GetCustomEmails(gc *gin.Context) { "WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled}, "EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled}, "UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.customEmails.UserExpired.Enabled}, - }) + "UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.userPage.Login.Enabled}, + "UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.userPage.Page.Enabled}, + } + + filter := gc.Query("filter") + if filter == "user" { + list = emailListDTO{"UserLogin": list["UserLogin"], "UserPage": list["UserPage"]} + } else { + delete(list, "UserLogin") + delete(list, "UserPage") + } + + gc.JSON(200, list) } -func (app *appContext) getCustomEmail(id string) *customEmail { +func (app *appContext) getCustomMessage(id string) *customContent { switch id { case "Announcement": - return &customEmail{} + return &customContent{} case "UserCreated": return &app.storage.customEmails.UserCreated case "InviteExpiry": @@ -58,13 +74,17 @@ func (app *appContext) getCustomEmail(id string) *customEmail { return &app.storage.customEmails.EmailConfirmation case "UserExpired": return &app.storage.customEmails.UserExpired + case "UserLogin": + return &app.storage.userPage.Login + case "UserPage": + return &app.storage.userPage.Page } return nil } // @Summary Sets the corresponding custom email. // @Produce json -// @Param customEmail body customEmail true "Content = email (in markdown)." +// @Param customEmails body customEmails true "Content = email (in markdown)." // @Success 200 {object} boolResponse // @Failure 400 {object} boolResponse // @Failure 500 {object} boolResponse @@ -72,25 +92,29 @@ func (app *appContext) getCustomEmail(id string) *customEmail { // @Router /config/emails/{id} [post] // @Security Bearer // @tags Configuration -func (app *appContext) SetCustomEmail(gc *gin.Context) { - var req customEmail +func (app *appContext) SetCustomMessage(gc *gin.Context) { + var req customContent gc.BindJSON(&req) id := gc.Param("id") if req.Content == "" { respondBool(400, false, gc) return } - email := app.getCustomEmail(id) - if email == nil { + message := app.getCustomMessage(id) + if message == nil { respondBool(400, false, gc) return } - email.Content = req.Content - email.Enabled = true + message.Content = req.Content + message.Enabled = true if app.storage.storeCustomEmails() != nil { respondBool(500, false, gc) return } + if app.storage.storeUserPageContent() != nil { + respondBool(500, false, gc) + return + } respondBool(200, true, gc) } @@ -104,7 +128,7 @@ func (app *appContext) SetCustomEmail(gc *gin.Context) { // @Router /config/emails/{id}/state/{enable/disable} [post] // @Security Bearer // @tags Configuration -func (app *appContext) SetCustomEmailState(gc *gin.Context) { +func (app *appContext) SetCustomMessageState(gc *gin.Context) { id := gc.Param("id") s := gc.Param("state") enabled := false @@ -113,20 +137,24 @@ func (app *appContext) SetCustomEmailState(gc *gin.Context) { } else if s != "disable" { respondBool(400, false, gc) } - email := app.getCustomEmail(id) - if email == nil { + message := app.getCustomMessage(id) + if message == nil { respondBool(400, false, gc) return } - email.Enabled = enabled + message.Enabled = enabled if app.storage.storeCustomEmails() != nil { respondBool(500, false, gc) return } + if app.storage.storeUserPageContent() != nil { + respondBool(500, false, gc) + return + } respondBool(200, true, gc) } -// @Summary Returns the custom email (generating it if not set) and list of used variables in it. +// @Summary Returns the custom email/message (generating it if not set) and list of used variables in it. // @Produce json // @Success 200 {object} customEmailDTO // @Failure 400 {object} boolResponse @@ -135,7 +163,7 @@ func (app *appContext) SetCustomEmailState(gc *gin.Context) { // @Router /config/emails/{id} [get] // @Security Bearer // @tags Configuration -func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) { +func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) { lang := app.storage.lang.chosenEmailLang id := gc.Param("id") var content string @@ -146,20 +174,26 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) { var values map[string]interface{} username := app.storage.lang.Email[lang].Strings.get("username") emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress") - email := app.getCustomEmail(id) - if email == nil { - app.err.Printf("Failed to get custom email with ID \"%s\"", id) + customMessage := app.getCustomMessage(id) + if customMessage == nil { + app.err.Printf("Failed to get custom message with ID \"%s\"", id) respondBool(400, false, gc) return } if id == "WelcomeEmail" { conditionals = []string{"{yourAccountWillExpire}"} - email.Conditionals = conditionals + customMessage.Conditionals = conditionals + } else if id == "UserPage" { + variables = []string{"{username}"} + customMessage.Variables = variables + } else if id == "UserLogin" { + variables = []string{} + customMessage.Variables = variables } - content = email.Content + content = customMessage.Content noContent := content == "" if !noContent { - variables = email.Variables + variables = customMessage.Variables } switch id { case "Announcement": @@ -215,12 +249,14 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) { msg, err = app.email.constructUserExpired(app, true) } values = app.email.userExpiredValues(app, false) + case "UserLogin", "UserPage": + values = map[string]interface{}{} } if err != nil { respondBool(500, false, gc) return } - if noContent && id != "Announcement" { + if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" { content = msg.Text variables = make([]string, strings.Count(content, "{")) i := 0 @@ -239,7 +275,7 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) { i++ } } - email.Variables = variables + customMessage.Variables = variables } if variables == nil { variables = []string{} @@ -248,10 +284,21 @@ func (app *appContext) GetCustomEmailTemplate(gc *gin.Context) { respondBool(500, false, gc) return } - mail, err := app.email.constructTemplate("", "
", app) - if err != nil { + if app.storage.storeUserPageContent() != nil { respondBool(500, false, gc) - return + } + var mail *Message + if id != "UserLogin" && id != "UserPage" { + mail, err = app.email.constructTemplate("", "
", app) + if err != nil { + respondBool(500, false, gc) + return + } + } else { + mail = &Message{ + HTML: "
", + Markdown: "
", + } } gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Conditionals: conditionals, Values: values, HTML: mail.HTML, Plaintext: mail.Text}) } diff --git a/config.go b/config.go index a4316ab..7fb388c 100644 --- a/config.go +++ b/config.go @@ -46,7 +46,7 @@ func (app *appContext) loadConfig() error { key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json")))) } } - for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users", "announcements"} { + for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users", "announcements", "custom_user_page_content"} { app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json")))) } for _, key := range []string{"matrix_sql"} { @@ -160,6 +160,12 @@ func (app *appContext) loadConfig() error { app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String() app.storage.loadCustomEmails() + app.MustSetValue("user_page", "enabled", "true") + if app.config.Section("user_page").Key("enabled").MustBool(false) { + app.storage.userPage_path = app.config.Section("files").Key("custom_user_page_content").String() + app.storage.loadUserPageContent() + } + substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("") if substituteStrings != "" { diff --git a/config/config-base.json b/config/config-base.json index 20b6e7b..2a77336 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -1551,6 +1551,14 @@ "value": "", "description": "JSON file generated by program in settings, different from email_html/email_text. See wiki for more info." }, + "custom_user_page_content": { + "name": "Custom user page content", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "JSON file generated by program in settings, containing user page messages. See wiki for more info." + }, "telegram_users": { "name": "Telegram users", "required": false, diff --git a/css/base.css b/css/base.css index 5358312..5599eb3 100644 --- a/css/base.css +++ b/css/base.css @@ -13,6 +13,8 @@ --border-width-2: 3px; --border-width-4: 5px; --border-width-8: 8px; + + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; } .light { diff --git a/email.go b/email.go index 808369c..777ed25 100644 --- a/email.go +++ b/email.go @@ -24,7 +24,7 @@ import ( sMail "github.com/xhit/go-simple-mail/v2" ) -var renderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants}) +var markdownRenderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants}) // EmailClient implements email sending, right now via smtp, mailgun or a dummy client. type EmailClient interface { @@ -353,7 +353,7 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext, u subject = templateEmail(subject, []string{"{username}"}, nil, map[string]interface{}{"username": username[0]}) } email := &Message{Subject: subject} - html := markdown.ToHTML([]byte(md), nil, renderer) + html := markdown.ToHTML([]byte(md), nil, markdownRenderer) text := stripMarkdown(md) message := app.config.Section("messages").Key("message").String() var err error diff --git a/html/header.html b/html/header.html index 1babab1..db2cdab 100644 --- a/html/header.html +++ b/html/header.html @@ -1,4 +1,4 @@ - + diff --git a/html/login-modal.html b/html/login-modal.html index f640f04..3226d9e 100644 --- a/html/login-modal.html +++ b/html/login-modal.html @@ -1,11 +1,20 @@ diff --git a/html/user.html b/html/user.html index 609f7c0..b14c940 100644 --- a/html/user.html +++ b/html/user.html @@ -43,6 +43,7 @@ + {{ template "login-modal.html" . }} {{ template "account-linking.html" . }}
@@ -68,7 +69,6 @@ {{ .strings.logout }}
- {{ template "login-modal.html" . }}
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index 05b083d..9bc51e1 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -113,7 +113,9 @@ "actions": "Actions", "searchOptions": "Search Options", "matchText": "Match Text", - "jellyfinID": "Jellyfin ID" + "jellyfinID": "Jellyfin ID", + "userPageLogin": "User Page: Login", + "userPagePage": "User Page: Page" }, "notifications": { "changedEmailAddress": "Changed email address of {n}.", @@ -209,4 +211,4 @@ "plural": "Extended expiry for {n} users." } } -} \ No newline at end of file +} diff --git a/lang/common/en-us.json b/lang/common/en-us.json index 688b5a2..266216f 100644 --- a/lang/common/en-us.json +++ b/lang/common/en-us.json @@ -61,4 +61,4 @@ "plural": "{n} Days" } } -} \ No newline at end of file +} diff --git a/lang/form/en-us.json b/lang/form/en-us.json index d41904c..652856e 100644 --- a/lang/form/en-us.json +++ b/lang/form/en-us.json @@ -21,6 +21,8 @@ "sendPINDiscord": "Type {command} in {server_channel} on Discord, then send the PIN below.", "matrixEnterUser": "Enter your User ID, press submit, and a PIN will be sent to you. Enter it here to continue.", "welcomeUser": "Welcome, {user}!", + "addContactMethod": "Add Contact Method", + "editContactMethod": "Edit Contact Method", "joinTheServer": "Join the server:" }, "notifications": { @@ -61,4 +63,4 @@ "plural": "Must have at least {n} special characters" } } -} \ No newline at end of file +} diff --git a/matrix.go b/matrix.go index 932a6c0..9935a2c 100644 --- a/matrix.go +++ b/matrix.go @@ -249,7 +249,7 @@ func (d *MatrixDaemon) Send(message *Message, users ...MatrixUser) (err error) { md := "" if message.Markdown != "" { // Convert images to links - md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, renderer)) + md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, markdownRenderer)) } content := &event.MessageEventContent{ MsgType: "m.text", diff --git a/router.go b/router.go index bf9bf99..72faa01 100644 --- a/router.go +++ b/router.go @@ -196,10 +196,10 @@ func (app *appContext) loadRoutes(router *gin.Engine) { api.GET(p+"/config/update", app.CheckUpdate) api.POST(p+"/config/update", app.ApplyUpdate) - api.GET(p+"/config/emails", app.GetCustomEmails) - api.GET(p+"/config/emails/:id", app.GetCustomEmailTemplate) - api.POST(p+"/config/emails/:id", app.SetCustomEmail) - api.POST(p+"/config/emails/:id/state/:state", app.SetCustomEmailState) + api.GET(p+"/config/emails", app.GetCustomContent) + api.GET(p+"/config/emails/:id", app.GetCustomMessageTemplate) + api.POST(p+"/config/emails/:id", app.SetCustomMessage) + api.POST(p+"/config/emails/:id/state/:state", app.SetCustomMessageState) api.GET(p+"/config", app.GetConfig) api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/restart", app.restart) diff --git a/storage.go b/storage.go index d24b960..095075d 100644 --- a/storage.go +++ b/storage.go @@ -21,23 +21,24 @@ type matrixStore map[string]MatrixUser type emailStore map[string]EmailAddress type Storage struct { - timePattern string - invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path string - users map[string]time.Time // Map of Jellyfin User IDs to their expiry times. - invites Invites - profiles map[string]Profile - defaultProfile string - displayprefs, ombi_template map[string]interface{} - emails emailStore - telegram telegramStore // Map of Jellyfin User IDs to telegram users. - discord discordStore // Map of Jellyfin user IDs to discord users. - matrix matrixStore // Map of Jellyfin user IDs to Matrix users. - customEmails customEmails - policy mediabrowser.Policy - configuration mediabrowser.Configuration - lang Lang - announcements map[string]announcementTemplate - invitesLock, usersLock, discordLock, telegramLock, matrixLock, emailsLock sync.Mutex + timePattern string + invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path, userPage_path string + users map[string]time.Time // Map of Jellyfin User IDs to their expiry times. + invites Invites + profiles map[string]Profile + defaultProfile string + displayprefs, ombi_template map[string]interface{} + emails emailStore + telegram telegramStore // Map of Jellyfin User IDs to telegram users. + discord discordStore // Map of Jellyfin user IDs to discord users. + matrix matrixStore // Map of Jellyfin user IDs to Matrix users. + customEmails customEmails + userPage userPageContent + policy mediabrowser.Policy + configuration mediabrowser.Configuration + lang Lang + announcements map[string]announcementTemplate + invitesLock, usersLock, discordLock, telegramLock, matrixLock, emailsLock sync.Mutex } // GetEmails returns a copy of the store. @@ -172,25 +173,30 @@ type EmailAddress struct { } type customEmails struct { - UserCreated customEmail `json:"userCreated"` - InviteExpiry customEmail `json:"inviteExpiry"` - PasswordReset customEmail `json:"passwordReset"` - UserDeleted customEmail `json:"userDeleted"` - UserDisabled customEmail `json:"userDisabled"` - UserEnabled customEmail `json:"userEnabled"` - InviteEmail customEmail `json:"inviteEmail"` - WelcomeEmail customEmail `json:"welcomeEmail"` - EmailConfirmation customEmail `json:"emailConfirmation"` - UserExpired customEmail `json:"userExpired"` -} - -type customEmail struct { + UserCreated customContent `json:"userCreated"` + InviteExpiry customContent `json:"inviteExpiry"` + PasswordReset customContent `json:"passwordReset"` + UserDeleted customContent `json:"userDeleted"` + UserDisabled customContent `json:"userDisabled"` + UserEnabled customContent `json:"userEnabled"` + InviteEmail customContent `json:"inviteEmail"` + WelcomeEmail customContent `json:"welcomeEmail"` + EmailConfirmation customContent `json:"emailConfirmation"` + UserExpired customContent `json:"userExpired"` +} + +type customContent struct { Enabled bool `json:"enabled,omitempty"` Content string `json:"content"` Variables []string `json:"variables,omitempty"` Conditionals []string `json:"conditionals,omitempty"` } +type userPageContent struct { + Login customContent `json:"login"` + Page customContent `json:"page"` +} + // timePattern: %Y-%m-%dT%H:%M:%S.%f type Profile struct { @@ -981,6 +987,14 @@ func (st *Storage) storeCustomEmails() error { return storeJSON(st.customEmails_path, st.customEmails) } +func (st *Storage) loadUserPageContent() error { + return loadJSON(st.userPage_path, &st.userPage) +} + +func (st *Storage) storeUserPageContent() error { + return storeJSON(st.userPage_path, st.userPage) +} + func (st *Storage) loadPolicy() error { return loadJSON(st.policy_path, &st.policy) } diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts index 6c5282e..7d9a74d 100644 --- a/ts/modules/settings.ts +++ b/ts/modules/settings.ts @@ -542,7 +542,7 @@ export class settingsList { private _sections: { [name: string]: sectionPanel } private _buttons: { [name: string]: HTMLSpanElement } private _needsRestart: boolean = false; - private _emailEditor = new EmailEditor(); + private _messageEditor = new MessageEditor(); addSection = (name: string, s: Section, subButton?: HTMLElement) => { const section = new sectionPanel(s, name); @@ -713,7 +713,7 @@ export class settingsList { if (name in this._sections) { this._sections[name].update(settings.sections[name]); } else { - if (name == "messages") { + if (name == "messages" || name == "user_page") { const editButton = document.createElement("div"); editButton.classList.add("tooltip", "left"); editButton.innerHTML = ` @@ -724,7 +724,9 @@ export class settingsList { ${window.lang.get("strings", "customizeMessages")} `; - (editButton.querySelector("span.button") as HTMLSpanElement).onclick = this._emailEditor.showList; + (editButton.querySelector("span.button") as HTMLSpanElement).onclick = () => { + this._messageEditor.showList(name == "messages" ? "email" : "user"); + }; this.addSection(name, settings.sections[name], editButton); } else if (name == "updates") { const icon = document.createElement("span") as HTMLSpanElement; @@ -773,7 +775,7 @@ interface emailListEl { enabled: boolean; } -class EmailEditor { +class MessageEditor { private _currentID: string; private _names: { [id: string]: emailListEl }; private _content: string; @@ -884,8 +886,8 @@ class EmailEditor { // }, true); } - showList = () => { - _get("/config/emails?lang=" + window.language, null, (req: XMLHttpRequest) => { + showList = (filter?: string) => { + _get("/config/emails?lang=" + window.language + (filter ? "&filter=" + filter : ""), null, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status != 200) { window.notifications.customError("loadTemplateError", window.lang.notif("errorFailureCheckLogs")); diff --git a/views.go b/views.go index 055ca88..6c145e2 100644 --- a/views.go +++ b/views.go @@ -12,6 +12,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" + "github.com/gomarkdown/markdown" "github.com/hrfee/mediabrowser" "github.com/steambap/captcha" ) @@ -203,6 +204,24 @@ func (app *appContext) MyUserPage(gc *gin.Context) { data["discordServerName"] = app.discord.serverName data["discordInviteLink"] = app.discord.inviteChannelName != "" } + + pageMessages := map[string]*customContent{ + "Login": app.getCustomMessage("UserLogin"), + "Page": app.getCustomMessage("UserPage"), + } + + for name, msg := range pageMessages { + if msg == nil { + continue + } + data[name+"MessageEnabled"] = msg.Enabled + if !msg.Enabled { + continue + } + // We don't template here, since the username is only known after login. + data[name+"MessageContent"] = template.HTML(markdown.ToHTML([]byte(msg.Content), nil, markdownRenderer)) + } + gcHTML(gc, http.StatusOK, "user.html", data) }