emails: fix and confirm function of all emails

both custom and standard emails tested, quite a few fixes made,
including to an old bug with admin notifs.
This commit is contained in:
Harvey Tindall
2025-08-31 17:12:50 +01:00
parent 60dbfa2d1e
commit e67f1bf1a9
12 changed files with 135 additions and 125 deletions

View File

@@ -124,7 +124,7 @@ func (app *appContext) deleteExpiredInvite(data Invite) {
func (app *appContext) sendAdminExpiryNotification(data Invite) *sync.WaitGroup {
notify := data.Notify
if !emailEnabled || !app.config.Section("notifications").Key("enabled").MustBool(false) || len(notify) != 0 {
if !emailEnabled || !app.config.Section("notifications").Key("enabled").MustBool(false) || len(notify) == 0 {
return nil
}
var wait sync.WaitGroup
@@ -283,7 +283,7 @@ func (app *appContext) GetInviteCount(gc *gin.Context) {
// @Summary Get the number of invites stored in the database that have been used (but are still valid).
// @Produce json
// @Success 200 {object} PageCountDTO
// @Router /invites/count [get]
// @Router /invites/count/used [get]
// @Security Bearer
// @tags Invites
func (app *appContext) GetInviteUsedCount(gc *gin.Context) {

View File

@@ -148,11 +148,11 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
case "PasswordReset":
msg, err = app.email.constructReset(PasswordReset{}, true)
case "UserDeleted":
msg, err = app.email.constructDeleted("", true)
msg, err = app.email.constructDeleted("", "", true)
case "UserDisabled":
msg, err = app.email.constructDisabled("", true)
msg, err = app.email.constructDisabled("", "", true)
case "UserEnabled":
msg, err = app.email.constructEnabled("", true)
msg, err = app.email.constructEnabled("", "", true)
case "UserExpiryAdjusted":
msg, err = app.email.constructExpiryAdjusted("", time.Time{}, "", true)
case "ExpiryReminder":
@@ -164,7 +164,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
case "EmailConfirmation":
msg, err = app.email.constructConfirmation("", "", "", true)
case "UserExpired":
msg, err = app.email.constructUserExpired(true)
msg, err = app.email.constructUserExpired("", true)
case "Announcement":
case "UserPage":
case "UserLogin":
@@ -181,14 +181,13 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
}
}
var mail *Message
var mail *Message = nil
if contentInfo.ContentType == CustomMessage {
mail = &Message{}
err = app.email.construct(EmptyCustomContent, CustomContent{
mail, err = app.email.construct(EmptyCustomContent, CustomContent{
Name: EmptyCustomContent.Name,
Enabled: true,
Content: "<div class=\"preview-content\"></div>",
}, map[string]any{}, mail)
}, map[string]any{})
if err != nil {
respondBool(500, false, gc)
return

View File

@@ -380,19 +380,6 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
"SetPolicy": map[string]string{},
}
sendMail := messagesEnabled
var msg *Message
var err error
if sendMail {
if req.Enabled {
msg, err = app.email.constructEnabled(req.Reason, false)
} else {
msg, err = app.email.constructDisabled(req.Reason, false)
}
if err != nil {
app.err.Printf(lm.FailedConstructEnableDisableMessage, "?", err)
sendMail = false
}
}
activityType := ActivityDisabled
if req.Enabled {
activityType = ActivityEnabled
@@ -404,6 +391,18 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err)
continue
}
var msg *Message
if sendMail {
if req.Enabled {
msg, err = app.email.constructEnabled(user.Name, req.Reason, false)
} else {
msg, err = app.email.constructDisabled(user.Name, req.Reason, false)
}
if err != nil {
app.err.Printf(lm.FailedConstructEnableDisableMessage, "?", err)
sendMail = false
}
}
err, _, _ = app.SetUserDisabled(user, !req.Enabled)
if err != nil {
errors["SetPolicy"][user.ID] = err.Error()
@@ -449,15 +448,6 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
gc.BindJSON(&req)
errors := map[string]string{}
sendMail := messagesEnabled
var msg *Message
var err error
if sendMail {
msg, err = app.email.constructDeleted(req.Reason, false)
if err != nil {
app.err.Printf(lm.FailedConstructDeletionMessage, "?", err)
sendMail = false
}
}
for _, userID := range req.Users {
user, err := app.jf.UserByID(userID, false)
if err != nil {
@@ -465,6 +455,15 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
errors[userID] = err.Error()
}
var msg *Message = nil
if sendMail {
msg, err = app.email.constructDeleted(user.Name, req.Reason, false)
if err != nil {
app.err.Printf(lm.FailedConstructDeletionMessage, "?", err)
sendMail = false
}
}
deleted := false
err, deleted = app.DeleteUser(user)
if err != nil {
@@ -677,11 +676,10 @@ func (app *appContext) Announce(gc *gin.Context) {
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
continue
}
msg := &Message{}
err = app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{
msg, err := app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{
Enabled: true,
Content: req.Message,
}, map[string]any{"username": user.Name}, msg)
}, map[string]any{"username": user.Name})
if err != nil {
app.err.Printf(lm.FailedConstructAnnouncementMessage, userID, err)
respondBool(500, false, gc)
@@ -694,11 +692,10 @@ func (app *appContext) Announce(gc *gin.Context) {
}
// app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
} else {
msg := &Message{}
err := app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{
msg, err := app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{
Enabled: true,
Content: req.Message,
}, map[string]any{"username": ""}, msg)
}, map[string]any{"username": ""})
if err != nil {
app.err.Printf(lm.FailedConstructAnnouncementMessage, "*", err)
respondBool(500, false, gc)

View File

@@ -31,6 +31,7 @@ func (app *appContext) loadArgs(firstCall bool) {
SWAGGER = flag.Bool("swagger", false, "Enable swagger at /swagger/index.html")
flag.BoolVar(&NO_API_AUTH_DO_NOT_USE, "disable-api-auth-do-not-use", false, "Disables API authentication. DO NOT USE!")
flag.StringVar(&NO_API_AUTH_FORCE_JFID, "disable-api-auth-force-jf-id", "", "Assume given JFID when API auth is disabled.")
flag.Parse()
if *help {
@@ -52,11 +53,14 @@ func (app *appContext) loadArgs(firstCall bool) {
if NO_API_AUTH_DO_NOT_USE && *DEBUG {
NO_API_AUTH_DO_NOT_USE = false
forceJfID := NO_API_AUTH_FORCE_JFID
NO_API_AUTH_FORCE_JFID = ""
buf := bufio.NewReader(os.Stdin)
app.err.Print(lm.NoAPIAuthPrompt)
sentence, err := buf.ReadBytes('\n')
if err == nil && strings.ContainsRune(string(sentence), 'y') {
NO_API_AUTH_DO_NOT_USE = true
NO_API_AUTH_FORCE_JFID = forceJfID
}
}
}

11
auth.go
View File

@@ -40,8 +40,12 @@ func (app *appContext) logIpErr(gc *gin.Context, user bool, out string) {
}
func (app *appContext) webAuth() gin.HandlerFunc {
if NO_API_AUTH_DO_NOT_USE {
return app.bogusAuthenticate
} else {
return app.authenticate
}
}
func (app *appContext) authLog(v any) { app.debug.PrintfCustomLevel(4, lm.FailedAuthRequest, v) }
@@ -138,6 +142,13 @@ func (app *appContext) authenticate(gc *gin.Context) {
gc.Next()
}
// bogusAuthenticate is for use with NO_API_AUTH_DO_NOT_USE, it sets the jfId/userId value from NO_API_AUTH_FORCE_JF_ID.
func (app *appContext) bogusAuthenticate(gc *gin.Context) {
gc.Set("jfId", NO_API_AUTH_FORCE_JFID)
gc.Set("userId", NO_API_AUTH_FORCE_JFID)
gc.Next()
}
func checkToken(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"])

View File

@@ -245,7 +245,7 @@ var customContent = map[string]CustomContentInfo{
"reason",
),
Placeholders: defaultVals(map[string]any{
"newExpiry": "",
"newExpiry": "01/01/01 00:00",
"reason": "Reason",
}),
SourceFile: ContentSourceFileInfo{

View File

@@ -246,14 +246,21 @@ type templ interface {
Execute(wr io.Writer, data interface{}) error
}
func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomContent, data map[string]any, msg *Message) error {
func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomContent, data map[string]any) (*Message, error) {
msg := &Message{
Subject: contentInfo.Subject(emailer.config, &emailer.lang),
}
// Template the subject for bonus points
if subject, err := templateEmail(msg.Subject, contentInfo.Variables, contentInfo.Conditionals, data); err == nil {
msg.Subject = subject
}
if cc.Enabled {
// Use template email, rather than the built-in's email file.
contentInfo.SourceFile = customContent["TemplateEmail"].SourceFile
content, err := templateEmail(cc.Content, contentInfo.Variables, contentInfo.Conditionals, data)
if err != nil {
emailer.err.Printf(lm.FailedConstructCustomContent, msg.Subject, err)
return err
return msg, err
}
html := markdown.ToHTML([]byte(content), nil, markdownRenderer)
text := stripMarkdown(content)
@@ -268,10 +275,6 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten
data = templateData
}
var err error = nil
// Template the subject for bonus points
if subject, err := templateEmail(msg.Subject, contentInfo.Variables, contentInfo.Conditionals, data); err == nil {
msg.Subject = subject
}
var tpl templ
msg.Text = ""
@@ -313,7 +316,7 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten
tpl, err = textTemplate.ParseFS(filesystem, fpath)
}
if err != nil {
return fmt.Errorf("error reading from fs path \"%s\": %v", fpath, err)
return msg, fmt.Errorf("error reading from fs path \"%s\": %v", fpath, err)
}
// For constructTemplate, if "md" is found in data it's used in stead of "text".
foundMarkdown := false
@@ -326,7 +329,7 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten
var tplData bytes.Buffer
err = tpl.Execute(&tplData, data)
if err != nil {
return err
return msg, err
}
if foundMarkdown {
data["plaintext"], data["md"] = data["md"], data["plaintext"]
@@ -339,10 +342,10 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten
msg.Markdown = tplData.String()
}
}
return nil
return msg, nil
}
func (emailer *Emailer) baseValues(name string, username string, placeholders bool, values map[string]any) (CustomContentInfo, map[string]any, *Message) {
func (emailer *Emailer) baseValues(name string, username string, placeholders bool, values map[string]any) (CustomContentInfo, map[string]any) {
contentInfo := customContent[name]
template := map[string]any{
"username": username,
@@ -355,17 +358,14 @@ func (emailer *Emailer) baseValues(name string, username string, placeholders bo
template[v] = "{" + v + "}"
}
}
email := &Message{
Subject: contentInfo.Subject(emailer.config, &emailer.lang),
}
return contentInfo, template, email
return contentInfo, template
}
func (emailer *Emailer) constructConfirmation(code, username, key string, placeholders bool) (*Message, error) {
if placeholders {
username = "{username}"
}
contentInfo, template, msg := emailer.baseValues("EmailConfirmation", username, placeholders, map[string]any{
contentInfo, template := emailer.baseValues("EmailConfirmation", username, placeholders, map[string]any{
"helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
@@ -381,15 +381,14 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, placeh
template["confirmationURL"] = inviteLink
}
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructInvite(invite Invite, placeholders bool) (*Message, error) {
expiry := invite.ValidTill
d, t, expiresIn := emailer.formatExpiry(expiry, false)
inviteLink := fmt.Sprintf("%s%s/%s", ExternalURI(nil), PAGES.Form, invite.Code)
contentInfo, template, msg := emailer.baseValues("InviteEmail", "", placeholders, map[string]any{
contentInfo, template := emailer.baseValues("InviteEmail", "", placeholders, map[string]any{
"hello": emailer.lang.InviteEmail.get("hello"),
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
"toJoin": emailer.lang.InviteEmail.get("toJoin"),
@@ -404,13 +403,12 @@ func (emailer *Emailer) constructInvite(invite Invite, placeholders bool) (*Mess
template["inviteExpiry"] = emailer.lang.InviteEmail.template("inviteExpiry", template)
}
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructExpiry(invite Invite, placeholders bool) (*Message, error) {
expiry := formatDatetime(invite.ValidTill)
contentInfo, template, msg := emailer.baseValues("InviteExpiry", "", placeholders, map[string]any{
contentInfo, template := emailer.baseValues("InviteExpiry", "", placeholders, map[string]any{
"inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"),
"notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"),
"expiredAt": emailer.lang.InviteExpiry.get("expiredAt"),
@@ -421,14 +419,13 @@ func (emailer *Emailer) constructExpiry(invite Invite, placeholders bool) (*Mess
template["expiredAt"] = emailer.lang.InviteExpiry.template("expiredAt", template)
}
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructCreated(username, address string, when time.Time, invite Invite, placeholders bool) (*Message, error) {
// NOTE: This was previously invite.Created, not sure why.
created := formatDatetime(when)
contentInfo, template, msg := emailer.baseValues("UserCreated", username, placeholders, map[string]any{
contentInfo, template := emailer.baseValues("UserCreated", username, placeholders, map[string]any{
"aUserWasCreated": emailer.lang.UserCreated.get("aUserWasCreated"),
"nameString": emailer.lang.Strings.get("name"),
"addressString": emailer.lang.Strings.get("emailAddress"),
@@ -446,8 +443,7 @@ func (emailer *Emailer) constructCreated(username, address string, when time.Tim
}
}
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructReset(pwr PasswordReset, placeholders bool) (*Message, error) {
@@ -456,7 +452,7 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, placeholders bool) (*M
}
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true)
linkResetEnabled := emailer.config.Section("password_resets").Key("link_reset").MustBool(false)
contentInfo, template, msg := emailer.baseValues("PasswordReset", pwr.Username, placeholders, map[string]any{
contentInfo, template := emailer.baseValues("PasswordReset", pwr.Username, placeholders, map[string]any{
"helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": pwr.Username}),
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
"ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"),
@@ -487,50 +483,49 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, placeholders bool) (*M
}
}
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructDeleted(reason string, placeholders bool) (*Message, error) {
func (emailer *Emailer) constructDeleted(username, reason string, placeholders bool) (*Message, error) {
if placeholders {
username = "{username}"
reason = "{reason}"
}
contentInfo, template, msg := emailer.baseValues("UserDeleted", "", placeholders, map[string]any{
contentInfo, template := emailer.baseValues("UserDeleted", username, placeholders, map[string]any{
"yourAccountWas": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
"reasonString": emailer.lang.Strings.get("reason"),
"reason": reason,
})
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructDisabled(reason string, placeholders bool) (*Message, error) {
func (emailer *Emailer) constructDisabled(username, reason string, placeholders bool) (*Message, error) {
if placeholders {
username = "{username}"
reason = "{reason}"
}
contentInfo, template, msg := emailer.baseValues("UserDeleted", "", placeholders, map[string]any{
contentInfo, template := emailer.baseValues("UserDisabled", username, placeholders, map[string]any{
"yourAccountWas": emailer.lang.UserDisabled.get("yourAccountWasDisabled"),
"reasonString": emailer.lang.Strings.get("reason"),
"reason": reason,
})
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructEnabled(reason string, placeholders bool) (*Message, error) {
func (emailer *Emailer) constructEnabled(username, reason string, placeholders bool) (*Message, error) {
if placeholders {
username = "{username}"
reason = "{reason}"
}
contentInfo, template, msg := emailer.baseValues("UserDeleted", "", placeholders, map[string]any{
contentInfo, template := emailer.baseValues("UserEnabled", username, placeholders, map[string]any{
"yourAccountWas": emailer.lang.UserEnabled.get("yourAccountWasEnabled"),
"reasonString": emailer.lang.Strings.get("reason"),
"reason": reason,
})
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructExpiryAdjusted(username string, expiry time.Time, reason string, placeholders bool) (*Message, error) {
@@ -538,7 +533,7 @@ func (emailer *Emailer) constructExpiryAdjusted(username string, expiry time.Tim
username = "{username}"
}
exp := formatDatetime(expiry)
contentInfo, template, msg := emailer.baseValues("UserExpiryAdjusted", username, placeholders, map[string]any{
contentInfo, template := emailer.baseValues("UserExpiryAdjusted", username, placeholders, map[string]any{
"helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"yourExpiryWasAdjusted": emailer.lang.UserExpiryAdjusted.get("yourExpiryWasAdjusted"),
"ifPreviouslyDisabled": emailer.lang.UserExpiryAdjusted.get("ifPreviouslyDisabled"),
@@ -554,8 +549,7 @@ func (emailer *Emailer) constructExpiryAdjusted(username string, expiry time.Tim
})
}
}
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructExpiryReminder(username string, expiry time.Time, placeholders bool) (*Message, error) {
@@ -563,7 +557,7 @@ func (emailer *Emailer) constructExpiryReminder(username string, expiry time.Tim
username = "{username}"
}
d, t, expiresIn := emailer.formatExpiry(expiry, false)
contentInfo, template, msg := emailer.baseValues("ExpiryReminder", username, placeholders, map[string]any{
contentInfo, template := emailer.baseValues("ExpiryReminder", username, placeholders, map[string]any{
"helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"yourAccountIsDueToExpire": emailer.lang.ExpiryReminder.get("yourAccountIsDueToExpire"),
"expiresIn": expiresIn,
@@ -576,8 +570,7 @@ func (emailer *Emailer) constructExpiryReminder(username string, expiry time.Tim
template["yourAccountIsDueToExpire"] = emailer.lang.ExpiryReminder.template("yourAccountIsDueToExpire", template)
}
}
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructWelcome(username string, expiry time.Time, placeholders bool) (*Message, error) {
@@ -586,7 +579,7 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, plac
username = "{username}"
exp = "{yourAccountWillExpire}"
}
contentInfo, template, msg := emailer.baseValues("WelcomeEmail", username, placeholders, map[string]any{
contentInfo, template := emailer.baseValues("WelcomeEmail", username, placeholders, map[string]any{
"welcome": emailer.lang.WelcomeEmail.get("welcome"),
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"),
"jellyfinURLString": emailer.lang.WelcomeEmail.get("jellyfinURL"),
@@ -604,18 +597,16 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, plac
template["yourAccountWillExpire"] = exp
}
}
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructUserExpired(placeholders bool) (*Message, error) {
contentInfo, template, msg := emailer.baseValues("UserExpired", "", placeholders, map[string]any{
func (emailer *Emailer) constructUserExpired(username string, placeholders bool) (*Message, error) {
contentInfo, template := emailer.baseValues("UserExpired", username, placeholders, map[string]any{
"yourAccountHasExpired": emailer.lang.UserExpired.get("yourAccountHasExpired"),
"contactTheAdmin": emailer.lang.UserExpired.get("contactTheAdmin"),
})
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg)
return msg, err
return emailer.construct(contentInfo, cc, template)
}
// calls the send method in the underlying emailClient.

View File

@@ -11,6 +11,7 @@ import (
type GenericDaemon struct {
Stopped bool
ShutdownChannel chan string
TriggerChannel chan bool
Interval time.Duration
period time.Duration
jobs []func(app *appContext)
@@ -27,6 +28,7 @@ func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app
d := GenericDaemon{
Stopped: false,
ShutdownChannel: make(chan string),
TriggerChannel: make(chan bool),
Interval: interval,
period: interval,
app: app,
@@ -46,6 +48,8 @@ func (d *GenericDaemon) run() {
case <-d.ShutdownChannel:
d.ShutdownChannel <- "Down"
return
case <-d.TriggerChannel:
break
case <-time.After(d.period):
break
}
@@ -61,6 +65,10 @@ func (d *GenericDaemon) run() {
}
}
func (d *GenericDaemon) Trigger() {
d.TriggerChannel <- true
}
func (d *GenericDaemon) Shutdown() {
d.Stopped = true
d.ShutdownChannel <- "Down"

13
main.go
View File

@@ -123,6 +123,7 @@ type appContext struct {
telegram *TelegramDaemon
discord *DiscordDaemon
matrix *MatrixDaemon
housekeepingDaemon, userDaemon *GenericDaemon
contactMethods []ContactMethodLinker
LoggerSet
host string
@@ -505,13 +506,13 @@ func start(asDaemon, firstCall bool) {
os.Exit(0)
}
invDaemon := newHousekeepingDaemon(time.Duration(60*time.Second), app)
go invDaemon.run()
defer invDaemon.Shutdown()
app.housekeepingDaemon = newHousekeepingDaemon(time.Duration(60*time.Second), app)
go app.housekeepingDaemon.run()
defer app.housekeepingDaemon.Shutdown()
userDaemon := newUserDaemon(time.Duration(60*time.Second), app)
go userDaemon.run()
defer userDaemon.Shutdown()
app.userDaemon = newUserDaemon(time.Duration(60*time.Second), app)
go app.userDaemon.run()
defer app.userDaemon.Shutdown()
var jellyseerrDaemon *GenericDaemon
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) && app.config.Section("jellyseerr").Key("import_existing").MustBool(false) {

View File

@@ -18,6 +18,7 @@ import (
var (
// Disables authentication for the API. Do not use!
NO_API_AUTH_DO_NOT_USE = false
NO_API_AUTH_FORCE_JFID = ""
)
// loads HTML templates. If [files]/html_templates is set, alternative files inside the directory are loaded in place of the internal templates.
@@ -188,11 +189,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
}
var api *gin.RouterGroup
if NO_API_AUTH_DO_NOT_USE && *DEBUG {
api = router.Group("/")
} else {
api = router.Group("/", app.webAuth())
}
for _, p := range routePrefixes {
var user *gin.RouterGroup
@@ -244,6 +241,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart)
api.GET(p+"/logs", app.GetLog)
api.POST(p+"/tasks/housekeeping", func(gc *gin.Context) { app.housekeepingDaemon.Trigger(); gc.Status(http.StatusNoContent) })
api.POST(p+"/tasks/users", func(gc *gin.Context) { app.userDaemon.Trigger(); gc.Status(http.StatusNoContent) })
api.POST(p+"/backups", app.CreateBackup)
api.GET(p+"/backups/:fname", app.GetBackup)
api.GET(p+"/backups", app.GetBackups)

View File

@@ -1126,9 +1126,9 @@ class MessageEditor {
this._variables.innerHTML = innerHTML
let buttons = this._variables.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>;
for (let i = 0; i < this._templ.variables.length; i++) {
buttons[i].innerHTML = `<span class="font-mono bg-inherit">` + this._templ.variables[i] + `</span>`;
buttons[i].innerHTML = `<span class="font-mono bg-inherit">` + "{" + this._templ.variables[i] + "}" + `</span>`;
buttons[i].onclick = () => {
insertText(this._textArea, this._templ.variables[i]);
insertText(this._textArea, "{" + this._templ.variables[i] + "}");
this.loadPreview();
// this._timeout = setTimeout(this.loadPreview, this._finishInterval);
}
@@ -1146,9 +1146,9 @@ class MessageEditor {
this._conditionals.innerHTML = innerHTML
buttons = this._conditionals.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>;
for (let i = 0; i < this._templ.conditionals.length; i++) {
buttons[i].innerHTML = `<span class="font-mono bg-inherit">{if ` + this._templ.conditionals[i].slice(1) + `</span>`;
buttons[i].innerHTML = `<span class="font-mono bg-inherit">{if ` + this._templ.conditionals[i] + "}" + `</span>`;
buttons[i].onclick = () => {
insertText(this._textArea, "{if " + this._templ.conditionals[i].slice(1) + "{endif}");
insertText(this._textArea, "{if " + this._templ.conditionals[i] + "}" + "{endif}");
this.loadPreview();
// this._timeout = setTimeout(this.loadPreview, this._finishInterval);
}
@@ -1162,9 +1162,9 @@ class MessageEditor {
let content = this._textArea.value;
if (this._templ.variables) {
for (let variable of this._templ.variables) {
let value = this._templ.values[variable.slice(1, -1)];
if (value === undefined) { value = variable; }
content = content.replace(new RegExp(variable, "g"), value);
let value = this._templ.values[variable];
if (value === undefined) { value = "{" + variable + "}"; }
content = content.replace(new RegExp("{" + variable + "}", "g"), value);
}
}
if (this._templ.html == "") {

View File

@@ -173,7 +173,7 @@ func (app *appContext) checkUsers(remindBeforeExpiry *DayTimerSet) {
if name == "" {
continue
}
msg, err := app.email.constructUserExpired(false)
msg, err := app.email.constructUserExpired(user.Name, false)
if err != nil {
app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err)
} else if err := app.sendByID(msg, user.ID); err != nil {