Files
jfa-go/user-d.go
Harvey Tindall e67f1bf1a9 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.
2025-08-31 17:12:50 +01:00

191 lines
5.8 KiB
Go

package main
import (
"time"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3"
)
func newUserDaemon(interval time.Duration, app *appContext) *GenericDaemon {
preExpiryCutoffDays := app.config.Section("user_expiry").Key("send_reminder_n_days_before").StringsWithShadows("|")
var as *DayTimerSet
if len(preExpiryCutoffDays) > 0 {
as = NewDayTimerSet(preExpiryCutoffDays, -24*time.Hour)
}
d := NewGenericDaemon(interval, app,
func(app *appContext) {
app.checkUsers(as)
},
)
d.Name("User daemon")
return d
}
const (
ExpiryModeDisable = iota
ExpiryModeDelete
)
func (app *appContext) checkUsers(remindBeforeExpiry *DayTimerSet) {
if len(app.storage.GetUserExpiries()) == 0 {
return
}
app.info.Println(lm.CheckUserExpiries)
users, err := app.jf.GetUsers(false)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
return
}
expiryMode := ExpiryModeDisable
if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" {
expiryMode = ExpiryModeDelete
}
deleteAfterPeriod := app.config.Section("user_expiry").Key("delete_expired_after_days").MustInt(0)
if expiryMode == ExpiryModeDelete {
deleteAfterPeriod = 0
}
shouldContact := messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true)
// Use a map to speed up checking for deleted users later
// FIXME: Maybe expose MediaBrowser.usersByID in some way and use that instead.
userExists := map[string]mediabrowser.User{}
for _, user := range users {
userExists[user.ID] = user
}
shouldInvalidateCache := false
for _, expiry := range app.storage.GetUserExpiries() {
id := expiry.JellyfinID
user, ok := userExists[id]
if !ok {
app.info.Printf(lm.DeleteExpiryForOldUser, id)
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
continue
}
if !time.Now().After(expiry.Expiry) {
if shouldContact && remindBeforeExpiry != nil {
app.debug.Printf("Checking for expiry reminder timers")
duration := remindBeforeExpiry.Check(expiry.Expiry, expiry.LastNotified)
if duration != 0 {
expiry.LastNotified = time.Now()
app.storage.SetUserExpiryKey(user.ID, expiry)
name := app.getAddressOrName(user.ID)
// Skip blank contact info
if name == "" {
continue
}
msg, err := app.email.constructExpiryReminder(user.Name, expiry.Expiry, false)
if err != nil {
app.err.Printf(lm.FailedConstructExpiryReminderMessage, user.ID, err)
} else if err := app.sendByID(msg, user.ID); err != nil {
app.err.Printf(lm.FailedSendExpiryReminderMessage, user.ID, name, err)
} else {
app.info.Printf(lm.SentExpiryReminderMessage, user.ID, name)
}
}
}
continue
}
// True when "Delete after period" enabled and this user's account has already expired.
alreadyExpired := false
// True when the user has expired and N days has passed for them to be deleted.
alreadyExpiredShouldDelete := false
if expiry.DeleteAfterPeriod {
// Delete hanging expiries after the admin disables "delete after N days"
if deleteAfterPeriod <= 0 {
app.storage.DeleteUserExpiryKey(user.ID)
continue
}
alreadyExpired = true
alreadyExpiredShouldDelete = time.Now().After(expiry.Expiry.AddDate(0, 0, deleteAfterPeriod))
if !alreadyExpiredShouldDelete {
continue
}
}
// Record activity
activity := Activity{
UserID: id,
SourceType: ActivityDaemon,
Time: time.Now(),
Type: ActivityUnknown,
}
// To save you the brain power: these conditions are fine because of all the "continue"s above us, no further checks are needed.
if expiryMode == ExpiryModeDelete || alreadyExpiredShouldDelete {
app.info.Printf(lm.DeleteExpiredUser, user.Name)
deleted := false
err, deleted = app.DeleteUser(user)
// Silence unimportant errors
if deleted {
err = nil
}
activity.Type = ActivityDeletion
// Store the user name, since there's no longer a user ID to reference back to
activity.Value = user.Name
} else {
app.info.Printf(lm.DisableExpiredUser, user.Name)
// Admins can't be disabled
// so they're not an admin anymore, sorry
user.Policy.IsAdministrator = false
err, _, _ = app.SetUserDisabled(user, true)
activity.Type = ActivityDisabled
}
if err != nil {
app.err.Printf(lm.FailedDeleteOrDisableExpiredUser, user.ID, err)
continue
}
// Sanity check
if activity.Type != ActivityUnknown {
app.storage.SetActivityKey(shortuuid.New(), activity, nil, false)
}
// If we're not gonna be deleting the user later, we don't need the expiry stored anymore:
// 1. Delete after N days is disabled, or Expiry mode is set to delete.
// 2. User has expired and been deleted after N days.
// 3. User has expired, but their account is not disabled (i.e. an Admin intervened).
if deleteAfterPeriod <= 0 || alreadyExpiredShouldDelete || (alreadyExpired && !user.Policy.IsDisabled) {
app.storage.DeleteUserExpiryKey(user.ID)
} else if deleteAfterPeriod > 0 && !alreadyExpired {
// Otherwise, mark the expiry as done pending a delete after N days.
expiry.DeleteAfterPeriod = true
// Sure, we haven't contacted them yet, but we're about to
if shouldContact {
expiry.LastNotified = time.Now()
}
app.storage.SetUserExpiryKey(user.ID, expiry)
}
shouldInvalidateCache = true
if shouldContact {
name := app.getAddressOrName(user.ID)
// Skip blank contact info
if name == "" {
continue
}
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 {
app.err.Printf(lm.FailedSendExpiryMessage, user.ID, name, err)
} else {
app.info.Printf(lm.SentExpiryMessage, user.ID, name)
}
}
}
if shouldInvalidateCache {
app.InvalidateJellyfinCache()
}
}