From f348262f88f0260e3c7307ec7d61ba583a7cfb13 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 31 Jul 2024 18:49:52 +0100 Subject: [PATCH] logmessages: finish up to api-users (alphabetically), refactor .go files done in alphabetical order. Some refactoring done to checkInvite(s) so they share most code. Also removed some useless debug lines. --- :w | 1503 ++++++++++++++++++++++++++++++++++++ api-activities.go | 3 +- api-backups.go | 11 +- api-invites.go | 221 +++--- api-jellyseerr.go | 9 +- api-messages.go | 49 +- api-ombi.go | 6 +- api-profiles.go | 21 +- api-userpage.go | 66 +- api-users.go | 245 +++--- logmessages/logmessages.go | 165 +++- 11 files changed, 1949 insertions(+), 350 deletions(-) create mode 100644 :w diff --git a/:w b/:w new file mode 100644 index 0000000..2934bb7 --- /dev/null +++ b/:w @@ -0,0 +1,1503 @@ +package main + +import ( + "fmt" + "net/url" + "os" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" + "github.com/hrfee/jfa-go/jellyseerr" + lm "github.com/hrfee/jfa-go/logmessages" + "github.com/hrfee/mediabrowser" + "github.com/lithammer/shortuuid/v3" + "github.com/timshannon/badgerhold/v4" +) + +// @Summary Creates a new Jellyfin user without an invite. +// @Produce json +// @Param newUserDTO body newUserDTO true "New user request object" +// @Success 200 +// @Router /users [post] +// @Security Bearer +// @tags Users +func (app *appContext) NewUserAdmin(gc *gin.Context) { + respondUser := func(code int, user, email bool, msg string, gc *gin.Context) { + resp := newUserResponse{ + User: user, + Email: email, + Error: msg, + } + gc.JSON(code, resp) + gc.Abort() + } + var req newUserDTO + gc.BindJSON(&req) + existingUser, _, _ := app.jf.UserByName(req.Username, false) + if existingUser.Name != "" { + msg := lm.UserExists + app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, msg) + respondUser(401, false, false, msg, gc) + return + } + user, status, err := app.jf.NewUser(req.Username, req.Password) + if !(status == 200 || status == 204) || err != nil { + app.err.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, err) + respondUser(401, false, false, err.Error(), gc) + return + } + id := user.ID + + // Record activity + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityCreation, + UserID: id, + SourceType: ActivityAdmin, + Source: gc.GetString("jfId"), + Value: user.Name, + Time: time.Now(), + }, gc, false) + + profile := app.storage.GetDefaultProfile() + if req.Profile != "" && req.Profile != "none" { + if p, ok := app.storage.GetProfileKey(req.Profile); ok { + profile = p + } else { + app.debug.Printf(lm.FailedGetProfile+lm.FallbackToDefault, req.Profile) + } + + status, err = app.jf.SetPolicy(id, profile.Policy) + if !(status == 200 || status == 204 || err == nil) { + app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, req.Username, err) + } + status, err = app.jf.SetConfiguration(id, profile.Configuration) + if (status == 200 || status == 204) && err == nil { + status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs) + } + if !((status == 200 || status == 204) && err == nil) { + app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, req.Username, err) + } + } + app.jf.CacheExpiry = time.Now() + if emailEnabled { + app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true}) + } + if app.config.Section("ombi").Key("enabled").MustBool(false) { + if profile.Ombi == nil { + profile.Ombi = map[string]interface{}{} + } + errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi) + if err != nil || code != 200 { + app.err.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err) + app.debug.Println("Errors reported by Ombi: " + strings.Join(errors, ", ")) + } else { + app.info.Printf(lm.CreateUser, lm.Ombi, req.Username) + } + } + if app.config.Section("jellyseerr").Key("enabled").MustBool(false) { + // Gets existing user (not possible) or imports the given user. + _, err := app.js.MustGetUser(id) + if err != nil { + app.err.Printf(lm.FailedCreateUser, lm.Jellyseerr, req.Username, err) + } else { + app.info.Printf(lm.CreateUser, lm.Jellyseerr, req.Username) + } + err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User) + if err != nil { + app.err.Printf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, req.Username, err) + } + err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications) + if err != nil { + app.err.Printf(lm.FailedApplyTemplate, "notifications", lm.Jellyseerr, req.Username, err) + } + if emailEnabled { + err = app.js.ModifyUser(id, map[jellyseerr.UserField]any{jellyseerr.FieldEmail: req.Email}) + if err != nil { + app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, id, err) + } + } + } + if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" { + msg, err := app.email.constructWelcome(req.Username, time.Time{}, app, false) + if err != nil { + app.err.Printf(lm.FailedConstructWelcomeMessage, id, err) + respondUser(500, true, false, err.Error(), gc) + return + } else if err := app.email.send(msg, req.Email); err != nil { + app.err.Printf(lm.FailedSendWelcomeMessage, req.Username, req.Email, err) + respondUser(500, true, false, err.Error(), gc) + return + } else { + app.info.Printf(lm.SentWelcomeMessage, req.Username, req.Email) + } + } + respondUser(200, true, true, "", gc) +} + +type errorFunc func(gc *gin.Context) + +// Used on the form & when a users email has been confirmed. +func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) (f errorFunc, success bool) { + existingUser, _, _ := app.jf.UserByName(req.Username, false) + if existingUser.Name != "" { + f = func(gc *gin.Context) { + msg := lm.UserExists + app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, msg) + respond(401, "errorUserExists", gc) + } + success = false + return + } + var discordUser DiscordUser + discordVerified := false + if discordEnabled { + if req.DiscordPIN == "" { + if app.config.Section("discord").Key("required").MustBool(false) { + f = func(gc *gin.Context) { + app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, lm.AccountUnverified) + respond(401, "errorDiscordVerification", gc) + } + success = false + return + } + } else { + discordUser, discordVerified = app.discord.UserVerified(req.DiscordPIN) + if !discordVerified { + f = func(gc *gin.Context) { + app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, fmt.Sprintf(lm.InvalidPIN, req.DiscordPIN)) + respond(401, "errorInvalidPIN", gc) + } + success = false + return + } + if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(discordUser.ID) { + f = func(gc *gin.Context) { + app.debug.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, lm.AccountLinked) + respond(400, "errorAccountLinked", gc) + } + success = false + return + } + err := app.discord.ApplyRole(discordUser.ID) + if err != nil { + f = func(gc *gin.Context) { + app.err.Printf("%s: New user failed: Failed to set member role: %v", req.Code, err) + respond(401, "error", gc) + } + success = false + return + } + } + } + var matrixUser MatrixUser + matrixVerified := false + if matrixEnabled { + if req.MatrixPIN == "" { + if app.config.Section("matrix").Key("required").MustBool(false) { + f = func(gc *gin.Context) { + app.info.Printf(lm.FailedLinkUser, lm.Matrix, "?", req.Code, lm.AccountUnverified) + respond(401, "errorMatrixVerification", gc) + } + success = false + return + } + } else { + user, ok := app.matrix.tokens[req.MatrixPIN] + if !ok || !user.Verified { + matrixVerified = false + f = func(gc *gin.Context) { + app.info.Printf(lm.FailedLinkUser, lm.Matrix, "?", req.Code, fmt.Sprintf(lm.InvalidPIN, req.MatrixPIN)) + respond(401, "errorInvalidPIN", gc) + } + success = false + return + } + if app.config.Section("matrix").Key("require_unique").MustBool(false) && app.matrix.UserExists(user.User.UserID) { + f = func(gc *gin.Context) { + app.debug.Printf("%s: New user failed: Matrix user already linked", req.Code) + respond(400, "errorAccountLinked", gc) + } + success = false + return + } + matrixVerified = user.Verified + matrixUser = *user.User + + } + } + var tgToken TelegramVerifiedToken + telegramVerified := false + if telegramEnabled { + if req.TelegramPIN == "" { + if app.config.Section("telegram").Key("required").MustBool(false) { + f = func(gc *gin.Context) { + app.info.Printf(lm.FailedLinkUser, lm.Telegram, "?", req.Code, lm.AccountUnverified) + respond(401, "errorTelegramVerification", gc) + } + success = false + return + } + } else { + tgToken, telegramVerified = app.telegram.TokenVerified(req.TelegramPIN) + if !telegramVerified { + f = func(gc *gin.Context) { + app.info.Printf(lm.FailedLinkUser, lm.Telegram, "?", req.Code, fmt.Sprintf(lm.InvalidPIN, req.TelegramPIN)) + respond(401, "errorInvalidPIN", gc) + } + success = false + return + } + if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(tgToken.Username) { + f = func(gc *gin.Context) { + app.debug.Printf("%s: New user failed: Telegram user already linked", req.Code) + respond(400, "errorAccountLinked", gc) + } + success = false + return + } + } + } + if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed { + claims := jwt.MapClaims{ + "valid": true, + "invite": req.Code, + "exp": time.Now().Add(30 * time.Minute).Unix(), + "type": "confirmation", + } + tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) + if err != nil { + f = func(gc *gin.Context) { + app.info.Printf("Failed to generate confirmation token: %v", err) + respond(500, "errorUnknown", gc) + } + success = false + return + } + if app.ConfirmationKeys == nil { + app.ConfirmationKeys = map[string]map[string]newUserDTO{} + } + cKeys, ok := app.ConfirmationKeys[req.Code] + if !ok { + cKeys = map[string]newUserDTO{} + } + cKeys[key] = req + app.confirmationKeysLock.Lock() + app.ConfirmationKeys[req.Code] = cKeys + app.confirmationKeysLock.Unlock() + f = func(gc *gin.Context) { + app.debug.Printf("%s: Email confirmation required", req.Code) + respond(401, "confirmEmail", gc) + msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app, false) + if err != nil { + app.err.Printf("%s: Failed to construct confirmation email: %v", req.Code, err) + } else if err := app.email.send(msg, req.Email); err != nil { + app.err.Printf("%s: Failed to send user confirmation email: %v", req.Code, err) + } else { + app.info.Printf("%s: Sent user confirmation email to \"%s\"", req.Code, req.Email) + } + } + success = false + return + } + + user, status, err := app.jf.NewUser(req.Username, req.Password) + if !(status == 200 || status == 204) || err != nil { + f = func(gc *gin.Context) { + app.err.Printf("%s New user failed (%d): %v", req.Code, status, err) + respond(401, app.storage.lang.Admin[app.storage.lang.chosenAdminLang].Notifications.get("errorUnknown"), gc) + } + success = false + return + } + invite, _ := app.storage.GetInvitesKey(req.Code) + app.checkInvite(req.Code, true, req.Username) + if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) { + for address, settings := range invite.Notify { + if settings["notify-creation"] { + go func(addr string) { + msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app, false) + if err != nil { + app.err.Printf("%s: Failed to construct user creation notification: %v", req.Code, err) + } else { + // Check whether notify "addr" is an email address of Jellyfin ID + if strings.Contains(addr, "@") { + err = app.email.send(msg, addr) + } else { + err = app.sendByID(msg, addr) + } + if err != nil { + app.err.Printf("%s: Failed to send user creation notification: %v", req.Code, err) + } else { + app.info.Printf("Sent user creation notification to %s", addr) + } + } + }(address) + } + } + } + id := user.ID + + // Record activity + sourceType := ActivityAnon + source := "" + if invite.ReferrerJellyfinID != "" { + sourceType = ActivityUser + source = invite.ReferrerJellyfinID + } + + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityCreation, + UserID: id, + SourceType: sourceType, + Source: source, + InviteCode: invite.Code, + Value: user.Name, + Time: time.Now(), + }, gc, true) + + emailStore := EmailAddress{ + Addr: req.Email, + Contact: (req.Email != ""), + } + // Only allow disabling of email contact if some other method is available. + if req.DiscordContact || req.TelegramContact || req.MatrixContact { + emailStore.Contact = req.EmailContact + } + + if invite.UserLabel != "" { + emailStore.Label = invite.UserLabel + } + + var profile Profile + if invite.Profile != "" { + app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile) + var ok bool + profile, ok = app.storage.GetProfileKey(invite.Profile) + if !ok { + profile = app.storage.GetDefaultProfile() + } + app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile) + status, err = app.jf.SetPolicy(id, profile.Policy) + if !((status == 200 || status == 204) && err == nil) { + app.err.Printf("%s: Failed to set user policy (%d): %v", req.Code, status, err) + } + app.debug.Printf("Applying homescreen from profile \"%s\"", invite.Profile) + status, err = app.jf.SetConfiguration(id, profile.Configuration) + if (status == 200 || status == 204) && err == nil { + status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs) + } + if !((status == 200 || status == 204) && err == nil) { + app.err.Printf("%s: Failed to set configuration template (%d): %v", req.Code, status, err) + } + if app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false) && profile.ReferralTemplateKey != "" { + emailStore.ReferralTemplateKey = profile.ReferralTemplateKey + // Store here, just incase email are disabled (whether this is even possible, i don't know) + app.storage.SetEmailsKey(id, emailStore) + + // If UseReferralExpiry is enabled, create the ref now so the clock starts ticking + refInv := Invite{} + err = app.storage.db.Get(profile.ReferralTemplateKey, &refInv) + if refInv.UseReferralExpiry { + refInv.Code = GenerateInviteCode() + expiryDelta := refInv.ValidTill.Sub(refInv.Created) + refInv.Created = time.Now() + refInv.ValidTill = refInv.Created.Add(expiryDelta) + refInv.IsReferral = true + refInv.ReferrerJellyfinID = id + app.storage.SetInvitesKey(refInv.Code, refInv) + } + } + } + // if app.config.Section("password_resets").Key("enabled").MustBool(false) { + if req.Email != "" || invite.UserLabel != "" { + app.storage.SetEmailsKey(id, emailStore) + } + expiry := time.Time{} + if invite.UserExpiry { + expiry = time.Now().AddDate(0, invite.UserMonths, invite.UserDays).Add(time.Duration((60*invite.UserHours)+invite.UserMinutes) * time.Minute) + app.storage.SetUserExpiryKey(id, UserExpiry{Expiry: expiry}) + } + if discordVerified { + discordUser.Contact = req.DiscordContact + if app.storage.deprecatedDiscord == nil { + app.storage.deprecatedDiscord = discordStore{} + } + // Note we don't log an activity here, since it's part of creating a user. + app.storage.SetDiscordKey(user.ID, discordUser) + delete(app.discord.verifiedTokens, req.DiscordPIN) + } + if telegramVerified { + tgUser := TelegramUser{ + ChatID: tgToken.ChatID, + Username: tgToken.Username, + Contact: req.TelegramContact, + } + if lang, ok := app.telegram.languages[tgToken.ChatID]; ok { + tgUser.Lang = lang + } + if app.storage.deprecatedTelegram == nil { + app.storage.deprecatedTelegram = telegramStore{} + } + app.telegram.DeleteVerifiedToken(req.TelegramPIN) + app.storage.SetTelegramKey(user.ID, tgUser) + } + if invite.Profile != "" && app.config.Section("ombi").Key("enabled").MustBool(false) { + if profile.Ombi != nil && len(profile.Ombi) != 0 { + template := profile.Ombi + errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, template) + accountExists := false + var ombiUser map[string]interface{} + if err != nil || code != 200 { + // Check if on the off chance, Ombi's user importer has already added the account. + ombiUser, status, err = app.getOmbiImportedUser(req.Username) + if status == 200 && err == nil { + app.info.Println("Found existing Ombi user, applying changes") + accountExists = true + template["password"] = req.Password + status, err = app.applyOmbiProfile(ombiUser, template) + if status != 200 || err != nil { + app.err.Printf("Failed to modify existing Ombi user (%d): %v\n", status, err) + } + } else { + app.info.Printf("Failed to create Ombi user (%d): %s", code, err) + app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", ")) + } + } else { + ombiUser, status, err = app.getOmbiUser(id) + if status != 200 || err != nil { + app.err.Printf("Failed to get Ombi user (%d): %v", status, err) + } else { + app.info.Println("Created Ombi user") + accountExists = true + } + } + if accountExists { + if discordVerified || telegramVerified { + dID := "" + tUser := "" + if discordVerified { + dID = discordUser.ID + } + if telegramVerified { + u, _ := app.storage.GetTelegramKey(user.ID) + tUser = u.Username + } + resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser) + if !(status == 200 || status == 204) || err != nil { + app.err.Printf("Failed to link Telegram/Discord to Ombi (%d): %v", status, err) + app.debug.Printf("Response: %v", resp) + } + } + } + } else { + app.debug.Printf("Skipping Ombi: Profile \"%s\" was empty", invite.Profile) + } + } + if invite.Profile != "" && app.config.Section("jellyseerr").Key("enabled").MustBool(false) { + if profile.Jellyseerr.Enabled { + // Gets existing user (not possible) or imports the given user. + _, err := app.js.MustGetUser(id) + if err != nil { + app.err.Printf("Failed to create Jellyseerr user: %v", err) + } else { + app.info.Println("Created Jellyseerr user") + } + err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User) + if err != nil { + app.err.Printf("Failed to apply Jellyseerr user template: %v\n", err) + } + err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications) + if err != nil { + app.err.Printf("Failed to apply Jellyseerr notifications template: %v\n", err) + } + contactMethods := map[jellyseerr.NotificationsField]any{} + if emailEnabled { + err = app.js.ModifyMainUserSettings(id, jellyseerr.MainUserSettings{Email: req.Email}) + if err != nil { + app.err.Printf("Failed to set Jellyseerr email address: %v\n", err) + } else { + contactMethods[jellyseerr.FieldEmailEnabled] = req.EmailContact + } + } + if discordVerified { + contactMethods[jellyseerr.FieldDiscord] = discordUser.ID + contactMethods[jellyseerr.FieldDiscordEnabled] = req.DiscordContact + } + if telegramVerified { + u, _ := app.storage.GetTelegramKey(user.ID) + contactMethods[jellyseerr.FieldTelegram] = u.ChatID + contactMethods[jellyseerr.FieldTelegramEnabled] = req.TelegramContact + } + if emailEnabled || discordVerified || telegramVerified { + err := app.js.ModifyNotifications(id, contactMethods) + if err != nil { + app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + } + } + } else { + app.debug.Printf("Skipping Jellyseerr: Profile \"%s\" was empty", invite.Profile) + } + } + if matrixVerified { + matrixUser.Contact = req.MatrixContact + delete(app.matrix.tokens, req.MatrixPIN) + if app.storage.deprecatedMatrix == nil { + app.storage.deprecatedMatrix = matrixStore{} + } + app.storage.SetMatrixKey(user.ID, matrixUser) + } + if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramVerified || discordVerified || matrixVerified { + name := app.getAddressOrName(user.ID) + app.debug.Printf("%s: Sending welcome message to %s", req.Username, name) + msg, err := app.email.constructWelcome(req.Username, expiry, app, false) + if err != nil { + app.err.Printf("%s: Failed to construct welcome message: %v", req.Username, err) + } else if err := app.sendByID(msg, user.ID); err != nil { + app.err.Printf("%s: Failed to send welcome message: %v", req.Username, err) + } else { + app.info.Printf("%s: Sent welcome message to \"%s\"", req.Username, name) + } + } + app.jf.CacheExpiry = time.Now() + success = true + return +} + +// @Summary Creates a new Jellyfin user via invite code +// @Produce json +// @Param newUserDTO body newUserDTO true "New user request object" +// @Success 200 {object} PasswordValidation +// @Failure 400 {object} PasswordValidation +// @Router /newUser [post] +// @tags Users +func (app *appContext) NewUser(gc *gin.Context) { + var req newUserDTO + gc.BindJSON(&req) + app.debug.Printf("%s: New user attempt", req.Code) + if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText, false) { + app.info.Printf("%s: New user failed: Captcha Incorrect", req.Code) + respond(400, "errorCaptcha", gc) + return + } + if !app.checkInvite(req.Code, false, "") { + app.info.Printf("%s New user failed: invalid code", req.Code) + respond(401, "errorInvalidCode", gc) + return + } + validation := app.validator.validate(req.Password) + valid := true + for _, val := range validation { + if !val { + valid = false + break + } + } + if !valid { + // 200 bcs idk what i did in js + app.info.Printf("%s: New user failed: Invalid password", req.Code) + gc.JSON(200, validation) + return + } + if emailEnabled { + if app.config.Section("email").Key("required").MustBool(false) && !strings.Contains(req.Email, "@") { + app.info.Printf("%s: New user failed: Email Required", req.Code) + respond(400, "errorNoEmail", gc) + return + } + if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" && app.EmailAddressExists(req.Email) { + app.info.Printf("%s: New user failed: Email already in use", req.Code) + respond(400, "errorEmailLinked", gc) + return + } + } + f, success := app.newUser(req, false, gc) + if !success { + f(gc) + return + } + code := 200 + for _, val := range validation { + if !val { + code = 400 + } + } + gc.JSON(code, validation) +} + +// @Summary Enable/Disable a list of users, optionally notifying them why. +// @Produce json +// @Param enableDisableUserDTO body enableDisableUserDTO true "User enable/disable request object" +// @Success 200 {object} boolResponse +// @Failure 400 {object} stringResponse +// @Failure 500 {object} errorListDTO "List of errors" +// @Router /users/enable [post] +// @Security Bearer +// @tags Users +func (app *appContext) EnableDisableUsers(gc *gin.Context) { + var req enableDisableUserDTO + gc.BindJSON(&req) + errors := errorListDTO{ + "GetUser": map[string]string{}, + "SetPolicy": map[string]string{}, + } + sendMail := messagesEnabled + 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 + } + } + activityType := ActivityDisabled + if req.Enabled { + activityType = ActivityEnabled + } + for _, userID := range req.Users { + user, status, err := app.jf.UserByID(userID, false) + if status != 200 || err != nil { + errors["GetUser"][userID] = fmt.Sprintf("%d %v", status, err) + app.err.Printf("Failed to get user \"%s\" (%d): %v", userID, status, err) + continue + } + user.Policy.IsDisabled = !req.Enabled + status, err = app.jf.SetPolicy(userID, user.Policy) + if !(status == 200 || status == 204) || err != nil { + errors["SetPolicy"][userID] = fmt.Sprintf("%d %v", status, err) + app.err.Printf("Failed to set policy for user \"%s\" (%d): %v", userID, status, err) + continue + } + + // Record activity + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: activityType, + UserID: userID, + SourceType: ActivityAdmin, + Source: gc.GetString("jfId"), + Time: time.Now(), + }, gc, false) + + if sendMail && req.Notify { + if err := app.sendByID(msg, userID); err != nil { + app.err.Printf("Failed to send account enabled/disabled email: %v", err) + continue + } + } + } + app.jf.CacheExpiry = time.Now() + if len(errors["GetUser"]) != 0 || len(errors["SetPolicy"]) != 0 { + gc.JSON(500, errors) + return + } + respondBool(200, true, gc) +} + +// @Summary Delete a list of users, optionally notifying them why. +// @Produce json +// @Param deleteUserDTO body deleteUserDTO true "User deletion request object" +// @Success 200 {object} boolResponse +// @Failure 400 {object} stringResponse +// @Failure 500 {object} errorListDTO "List of errors" +// @Router /users [delete] +// @Security Bearer +// @tags Users +func (app *appContext) DeleteUsers(gc *gin.Context) { + var req deleteUserDTO + gc.BindJSON(&req) + errors := map[string]string{} + 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 { + if ombiEnabled { + ombiUser, code, err := app.getOmbiUser(userID) + if code == 200 && err == nil { + if id, ok := ombiUser["id"]; ok { + status, err := app.ombi.DeleteUser(id.(string)) + if err != nil || status != 200 { + app.err.Printf("Failed to delete ombi user (%d): %v", status, err) + errors[userID] = fmt.Sprintf("Ombi: %d %v, ", status, err) + } + } + } + } + + username := "" + if user, status, err := app.jf.UserByID(userID, false); status == 200 && err == nil { + username = user.Name + } + + status, err := app.jf.DeleteUser(userID) + if !(status == 200 || status == 204) || err != nil { + msg := fmt.Sprintf("%d: %v", status, err) + if _, ok := errors[userID]; !ok { + errors[userID] = msg + } else { + errors[userID] += msg + } + } + + // Record activity + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityDeletion, + UserID: userID, + SourceType: ActivityAdmin, + Source: gc.GetString("jfId"), + Value: username, + Time: time.Now(), + }, gc, false) + + if sendMail && req.Notify { + if err := app.sendByID(msg, userID); err != nil { + app.err.Printf("Failed to send account deletion email: %v", err) + } + } + } + app.jf.CacheExpiry = time.Now() + if len(errors) == len(req.Users) { + respondBool(500, false, gc) + app.err.Printf("Account deletion failed: %s", errors[req.Users[0]]) + return + } else if len(errors) != 0 { + gc.JSON(500, errors) + return + } + respondBool(200, true, gc) +} + +// @Summary Extend time before the user(s) expiry, or create an expiry if it doesn't exist. +// @Produce json +// @Param extendExpiryDTO body extendExpiryDTO true "Extend expiry object" +// @Success 200 {object} boolResponse +// @Failure 400 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /users/extend [post] +// @tags Users +func (app *appContext) ExtendExpiry(gc *gin.Context) { + var req extendExpiryDTO + gc.BindJSON(&req) + app.info.Printf("Expiry extension requested for %d user(s)", len(req.Users)) + if req.Months <= 0 && req.Days <= 0 && req.Hours <= 0 && req.Minutes <= 0 && req.Timestamp <= 0 { + respondBool(400, false, gc) + return + } + for _, id := range req.Users { + base := time.Now() + if expiry, ok := app.storage.GetUserExpiryKey(id); ok { + base = expiry.Expiry + app.debug.Printf("Expiry extended for \"%s\"", id) + } else { + app.debug.Printf("Created expiry for \"%s\"", id) + } + expiry := UserExpiry{} + if req.Timestamp != 0 { + expiry.Expiry = time.Unix(req.Timestamp, 0) + } else { + expiry.Expiry = base.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute) + } + app.storage.SetUserExpiryKey(id, expiry) + if messagesEnabled && req.Notify { + go func(uid string, exp time.Time) { + user, status, err := app.jf.UserByID(uid, false) + if status != 200 || err != nil { + return + } + msg, err := app.email.constructExpiryAdjusted(user.Name, exp, req.Reason, app, false) + if err != nil { + app.err.Printf("%s: Failed to construct expiry adjustment notification: %v", uid, err) + return + } + if err := app.sendByID(msg, uid); err != nil { + app.err.Printf("%s: Failed to send expiry adjustment notification: %v", uid, err) + } + }(id, expiry.Expiry) + } + } + respondBool(204, true, gc) +} + +// @Summary Remove an expiry from a user's account. +// @Produce json +// @Param id path string true "id of user to extend expiry of." +// @Success 200 {object} boolResponse +// @Router /users/{id}/expiry [delete] +// @tags Users +func (app *appContext) RemoveExpiry(gc *gin.Context) { + app.storage.DeleteUserExpiryKey(gc.Param("id")) + respondBool(200, true, gc) +} + +// @Summary Enable referrals for the given user(s) based on the rules set in the given invite code, or profile. +// @Produce json +// @Param EnableDisableReferralDTO body EnableDisableReferralDTO true "List of users" +// @Param mode path string true "mode of template sourcing from 'invite' or 'profile'." +// @Param source path string true "invite code or profile name, depending on what mode is." +// @Param useExpiry path string true "with-expiry or none." +// @Success 200 {object} boolResponse +// @Failure 400 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /users/referral/{mode}/{source}/{useExpiry} [post] +// @Security Bearer +// @tags Users +func (app *appContext) EnableReferralForUsers(gc *gin.Context) { + var req EnableDisableReferralDTO + gc.BindJSON(&req) + mode := gc.Param("mode") + + source := gc.Param("source") + useExpiry := gc.Param("useExpiry") == "with-expiry" + baseInv := Invite{} + if mode == "profile" { + profile, ok := app.storage.GetProfileKey(source) + err := app.storage.db.Get(profile.ReferralTemplateKey, &baseInv) + if !ok || profile.ReferralTemplateKey == "" || err != nil { + app.debug.Printf("Couldn't find template to source from") + respondBool(400, false, gc) + return + + } + app.debug.Printf("Found referral template in profile: %+v\n", profile.ReferralTemplateKey) + } else if mode == "invite" { + // Get the invite, and modify it to turn it into a referral + err := app.storage.db.Get(source, &baseInv) + if err != nil { + app.debug.Printf("Couldn't find invite to source from") + respondBool(400, false, gc) + return + } + } + for _, u := range req.Users { + // 1. Wipe out any existing referral codes. + app.storage.db.DeleteMatching(Invite{}, badgerhold.Where("ReferrerJellyfinID").Eq(u)) + + // 2. Generate referral invite. + inv := baseInv + inv.Code = GenerateInviteCode() + expiryDelta := inv.ValidTill.Sub(inv.Created) + inv.Created = time.Now() + if useExpiry { + inv.ValidTill = inv.Created.Add(expiryDelta) + } else { + inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour) + } + inv.IsReferral = true + inv.ReferrerJellyfinID = u + inv.UseReferralExpiry = useExpiry + app.storage.SetInvitesKey(inv.Code, inv) + } +} + +// @Summary Disable referrals for the given user(s). +// @Produce json +// @Param EnableDisableReferralDTO body EnableDisableReferralDTO true "List of users" +// @Success 200 {object} boolResponse +// @Router /users/referral [delete] +// @Security Bearer +// @tags Users +func (app *appContext) DisableReferralForUsers(gc *gin.Context) { + var req EnableDisableReferralDTO + gc.BindJSON(&req) + for _, u := range req.Users { + // 1. Delete directly bound template + app.storage.db.DeleteMatching(Invite{}, badgerhold.Where("ReferrerJellyfinID").Eq(u)) + // 2. Check for and delete profile-attached template + user, ok := app.storage.GetEmailsKey(u) + if !ok { + continue + } + user.ReferralTemplateKey = "" + app.storage.SetEmailsKey(u, user) + } + respondBool(200, true, gc) +} + +// @Summary Send an announcement via email to a given list of users. +// @Produce json +// @Param announcementDTO body announcementDTO true "Announcement request object" +// @Success 200 {object} boolResponse +// @Failure 400 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /users/announce [post] +// @Security Bearer +// @tags Users +func (app *appContext) Announce(gc *gin.Context) { + var req announcementDTO + gc.BindJSON(&req) + if !messagesEnabled { + respondBool(400, false, gc) + return + } + // Generally, we only need to construct once. If {username} is included, however, this needs to be done for each user. + unique := strings.Contains(req.Message, "{username}") + if unique { + for _, userID := range req.Users { + user, status, err := app.jf.UserByID(userID, false) + if status != 200 || err != nil { + app.err.Printf("Failed to get user with ID \"%s\" (%d): %v", userID, status, err) + continue + } + msg, err := app.email.constructTemplate(req.Subject, req.Message, app, user.Name) + if err != nil { + app.err.Printf("Failed to construct announcement message: %v", err) + respondBool(500, false, gc) + return + } else if err := app.sendByID(msg, userID); err != nil { + app.err.Printf("Failed to send announcement message: %v", err) + respondBool(500, false, gc) + return + } + } + } else { + msg, err := app.email.constructTemplate(req.Subject, req.Message, app) + if err != nil { + app.err.Printf("Failed to construct announcement messages: %v", err) + respondBool(500, false, gc) + return + } else if err := app.sendByID(msg, req.Users...); err != nil { + app.err.Printf("Failed to send announcement messages: %v", err) + respondBool(500, false, gc) + return + } + } + app.info.Println("Sent announcement messages") + respondBool(200, true, gc) +} + +// @Summary Save an announcement as a template for use or editing later. +// @Produce json +// @Param announcementTemplate body announcementTemplate true "Announcement request object" +// @Success 200 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /users/announce/template [post] +// @Security Bearer +// @tags Users +func (app *appContext) SaveAnnounceTemplate(gc *gin.Context) { + var req announcementTemplate + gc.BindJSON(&req) + if !messagesEnabled { + respondBool(400, false, gc) + return + } + + app.storage.SetAnnouncementsKey(req.Name, req) + respondBool(200, true, gc) +} + +// @Summary Save an announcement as a template for use or editing later. +// @Produce json +// @Success 200 {object} getAnnouncementsDTO +// @Router /users/announce [get] +// @Security Bearer +// @tags Users +func (app *appContext) GetAnnounceTemplates(gc *gin.Context) { + resp := &getAnnouncementsDTO{make([]string, len(app.storage.GetAnnouncements()))} + for i, a := range app.storage.GetAnnouncements() { + resp.Announcements[i] = a.Name + } + gc.JSON(200, resp) +} + +// @Summary Get an announcement template. +// @Produce json +// @Success 200 {object} announcementTemplate +// @Failure 400 {object} boolResponse +// @Param name path string true "name of template (url encoded if necessary)" +// @Router /users/announce/template/{name} [get] +// @Security Bearer +// @tags Users +func (app *appContext) GetAnnounceTemplate(gc *gin.Context) { + escapedName := gc.Param("name") + name, err := url.QueryUnescape(escapedName) + if err != nil { + respondBool(400, false, gc) + return + } + if announcement, ok := app.storage.GetAnnouncementsKey(name); ok { + gc.JSON(200, announcement) + return + } + respondBool(400, false, gc) +} + +// @Summary Delete an announcement template. +// @Produce json +// @Success 200 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Param name path string true "name of template" +// @Router /users/announce/template/{name} [delete] +// @Security Bearer +// @tags Users +func (app *appContext) DeleteAnnounceTemplate(gc *gin.Context) { + name := gc.Param("name") + app.storage.DeleteAnnouncementsKey(name) + respondBool(200, false, gc) +} + +// @Summary Generate password reset links for a list of users, sending the links to them if possible. +// @Produce json +// @Param AdminPasswordResetDTO body AdminPasswordResetDTO true "List of user IDs" +// @Success 204 {object} boolResponse +// @Success 200 {object} AdminPasswordResetRespDTO +// @Failure 400 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /users/password-reset [post] +// @Security Bearer +// @tags Users +func (app *appContext) AdminPasswordReset(gc *gin.Context) { + var req AdminPasswordResetDTO + gc.BindJSON(&req) + if req.Users == nil || len(req.Users) == 0 { + app.debug.Println("Ignoring empty request for PWR") + respondBool(400, false, gc) + return + } + linkCount := 0 + var pwr InternalPWR + var err error + resp := AdminPasswordResetRespDTO{} + for _, id := range req.Users { + pwr, err = app.GenInternalReset(id) + if err != nil { + app.err.Printf("Failed to get user from Jellyfin: %v", err) + respondBool(500, false, gc) + return + } + if app.internalPWRs == nil { + app.internalPWRs = map[string]InternalPWR{} + } + app.internalPWRs[pwr.PIN] = pwr + sendAddress := app.getAddressOrName(id) + if sendAddress == "" || len(req.Users) == 1 { + resp.Link, err = app.GenResetLink(pwr.PIN) + linkCount++ + if sendAddress == "" { + resp.Manual = true + } + } + if sendAddress != "" { + msg, err := app.email.constructReset( + PasswordReset{ + Pin: pwr.PIN, + Username: pwr.Username, + Expiry: pwr.Expiry, + Internal: true, + }, app, false, + ) + if err != nil { + app.err.Printf("Failed to construct password reset message for \"%s\": %v", pwr.Username, err) + respondBool(500, false, gc) + return + } else if err := app.sendByID(msg, id); err != nil { + app.err.Printf("Failed to send password reset message to \"%s\": %v", sendAddress, err) + } else { + app.info.Printf("Sent password reset message to \"%s\"", sendAddress) + } + } + } + if resp.Link != "" && linkCount == 1 { + gc.JSON(200, resp) + return + } + respondBool(204, true, gc) +} + +// @Summary Get a list of Jellyfin users. +// @Produce json +// @Success 200 {object} getUsersDTO +// @Failure 500 {object} stringResponse +// @Router /users [get] +// @Security Bearer +// @tags Users +func (app *appContext) GetUsers(gc *gin.Context) { + app.debug.Println("Users requested") + var resp getUsersDTO + users, status, err := app.jf.GetUsers(false) + resp.UserList = make([]respUser, len(users)) + if !(status == 200 || status == 204) || err != nil { + app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) + respond(500, "Couldn't get users", gc) + return + } + adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true) + allowAll := app.config.Section("ui").Key("allow_all").MustBool(false) + referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false) + i := 0 + for _, jfUser := range users { + user := respUser{ + ID: jfUser.ID, + Name: jfUser.Name, + Admin: jfUser.Policy.IsAdministrator, + Disabled: jfUser.Policy.IsDisabled, + ReferralsEnabled: false, + } + if !jfUser.LastActivityDate.IsZero() { + user.LastActive = jfUser.LastActivityDate.Unix() + } + if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok { + user.Email = email.Addr + user.NotifyThroughEmail = email.Contact + user.Label = email.Label + user.AccountsAdmin = (app.jellyfinLogin) && (email.Admin || (adminOnly && jfUser.Policy.IsAdministrator) || allowAll) + } + expiry, ok := app.storage.GetUserExpiryKey(jfUser.ID) + if ok { + user.Expiry = expiry.Expiry.Unix() + } + if tgUser, ok := app.storage.GetTelegramKey(jfUser.ID); ok { + user.Telegram = tgUser.Username + user.NotifyThroughTelegram = tgUser.Contact + } + if mxUser, ok := app.storage.GetMatrixKey(jfUser.ID); ok { + user.Matrix = mxUser.UserID + user.NotifyThroughMatrix = mxUser.Contact + } + if dcUser, ok := app.storage.GetDiscordKey(jfUser.ID); ok { + user.Discord = RenderDiscordUsername(dcUser) + // user.Discord = dcUser.Username + "#" + dcUser.Discriminator + user.DiscordID = dcUser.ID + user.NotifyThroughDiscord = dcUser.Contact + } + // FIXME: Send referral data + referrerInv := Invite{} + if referralsEnabled { + // 1. Directly attached invite. + err := app.storage.db.FindOne(&referrerInv, badgerhold.Where("ReferrerJellyfinID").Eq(jfUser.ID)) + if err == nil { + user.ReferralsEnabled = true + // 2. Referrals via profile template. Shallow check, doesn't look for the thing in the database. + } else if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok && email.ReferralTemplateKey != "" { + user.ReferralsEnabled = true + } + } + resp.UserList[i] = user + i++ + } + gc.JSON(200, resp) +} + +// @Summary Set whether or not a user can access jfa-go. Redundant if the user is a Jellyfin admin. +// @Produce json +// @Param setAccountsAdminDTO body setAccountsAdminDTO true "Map of userIDs to whether or not they have access." +// @Success 204 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /users/accounts-admin [post] +// @Security Bearer +// @tags Users +func (app *appContext) SetAccountsAdmin(gc *gin.Context) { + var req setAccountsAdminDTO + gc.BindJSON(&req) + app.debug.Println("Admin modification requested") + users, status, err := app.jf.GetUsers(false) + if !(status == 200 || status == 204) || err != nil { + app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) + respond(500, "Couldn't get users", gc) + return + } + for _, jfUser := range users { + id := jfUser.ID + if admin, ok := req[id]; ok { + var emailStore = EmailAddress{} + if oldEmail, ok := app.storage.GetEmailsKey(id); ok { + emailStore = oldEmail + } + emailStore.Admin = admin + app.storage.SetEmailsKey(id, emailStore) + } + } + app.info.Println("Email list modified") + respondBool(204, true, gc) +} + +// @Summary Modify user's labels, which show next to their name in the accounts tab. +// @Produce json +// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to labels" +// @Success 204 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /users/labels [post] +// @Security Bearer +// @tags Users +func (app *appContext) ModifyLabels(gc *gin.Context) { + var req modifyEmailsDTO + gc.BindJSON(&req) + app.debug.Println("Label modification requested") + users, status, err := app.jf.GetUsers(false) + if !(status == 200 || status == 204) || err != nil { + app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) + respond(500, "Couldn't get users", gc) + return + } + for _, jfUser := range users { + id := jfUser.ID + if label, ok := req[id]; ok { + var emailStore = EmailAddress{} + if oldEmail, ok := app.storage.GetEmailsKey(id); ok { + emailStore = oldEmail + } + emailStore.Label = label + app.storage.SetEmailsKey(id, emailStore) + } + } + app.info.Println("Email list modified") + respondBool(204, true, gc) +} + +func (app *appContext) modifyEmail(jfID string, addr string) { + contactPrefChanged := false + emailStore, ok := app.storage.GetEmailsKey(jfID) + // Auto enable contact by email for newly added addresses + if !ok || emailStore.Addr == "" { + emailStore = EmailAddress{ + Contact: true, + } + contactPrefChanged = true + } + emailStore.Addr = addr + app.storage.SetEmailsKey(jfID, emailStore) + if app.config.Section("ombi").Key("enabled").MustBool(false) { + ombiUser, code, err := app.getOmbiUser(jfID) + if code == 200 && err == nil { + ombiUser["emailAddress"] = addr + code, err = app.ombi.ModifyUser(ombiUser) + if code != 200 || err != nil { + app.err.Printf("%s: Failed to change ombi email address (%d): %v", ombiUser["userName"].(string), code, err) + } + } + } + if app.config.Section("jellyseerr").Key("enabled").MustBool(false) { + err := app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: addr}) + if err != nil { + app.err.Printf("Failed to set Jellyseerr email address: %v\n", err) + } else if contactPrefChanged { + contactMethods := map[jellyseerr.NotificationsField]any{ + jellyseerr.FieldEmailEnabled: true, + } + err := app.js.ModifyNotifications(jfID, contactMethods) + if err != nil { + app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + } + } + } +} + +// @Summary Modify user's email addresses. +// @Produce json +// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to email addresses" +// @Success 200 {object} boolResponse +// @Failure 500 {object} stringResponse +// @Router /users/emails [post] +// @Security Bearer +// @tags Users +func (app *appContext) ModifyEmails(gc *gin.Context) { + var req modifyEmailsDTO + gc.BindJSON(&req) + app.debug.Println("Email modification requested") + users, status, err := app.jf.GetUsers(false) + if !(status == 200 || status == 204) || err != nil { + app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) + respond(500, "Couldn't get users", gc) + return + } + for _, jfUser := range users { + id := jfUser.ID + if address, ok := req[id]; ok { + app.modifyEmail(id, address) + + activityType := ActivityContactLinked + if address == "" { + activityType = ActivityContactUnlinked + } + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: activityType, + UserID: id, + SourceType: ActivityAdmin, + Source: gc.GetString("jfId"), + Value: "email", + Time: time.Now(), + }, gc, false) + } + } + app.info.Println("Email list modified") + respondBool(200, true, gc) +} + +// @Summary Apply settings to a list of users, either from a profile or from another user. +// @Produce json +// @Param userSettingsDTO body userSettingsDTO true "Parameters for applying settings" +// @Success 200 {object} errorListDTO +// @Failure 500 {object} errorListDTO "Lists of errors that occurred while applying settings" +// @Router /users/settings [post] +// @Security Bearer +// @tags Profiles & Settings +func (app *appContext) ApplySettings(gc *gin.Context) { + app.info.Println("User settings change requested") + var req userSettingsDTO + gc.BindJSON(&req) + applyingFrom := "profile" + var policy mediabrowser.Policy + var configuration mediabrowser.Configuration + var displayprefs map[string]interface{} + var ombi map[string]interface{} + var jellyseerr JellyseerrTemplate + jellyseerr.Enabled = false + if req.From == "profile" { + // Check profile exists & isn't empty + profile, ok := app.storage.GetProfileKey(req.Profile) + if !ok { + app.err.Printf("Couldn't find profile \"%s\" or profile was empty", req.Profile) + respond(500, "Couldn't find profile", gc) + return + } + if req.Homescreen { + if !profile.Homescreen { + app.err.Printf("No homescreen saved in profile \"%s\"", req.Profile) + respond(500, "No homescreen template available", gc) + return + } + configuration = profile.Configuration + displayprefs = profile.Displayprefs + } + if req.Policy { + policy = profile.Policy + } + if req.Ombi && app.config.Section("ombi").Key("enabled").MustBool(false) { + if profile.Ombi != nil && len(profile.Ombi) != 0 { + ombi = profile.Ombi + } + } + if req.Jellyseerr && app.config.Section("jellyseerr").Key("enabled").MustBool(false) { + if profile.Jellyseerr.Enabled { + jellyseerr = profile.Jellyseerr + } + } + + } else if req.From == "user" { + applyingFrom = "user" + app.jf.CacheExpiry = time.Now() + user, status, err := app.jf.UserByID(req.ID, false) + if !(status == 200 || status == 204) || err != nil { + app.err.Printf("Failed to get user from Jellyfin (%d): %v", status, err) + respond(500, "Couldn't get user", gc) + return + } + applyingFrom = "\"" + user.Name + "\"" + if req.Policy { + policy = user.Policy + } + if req.Homescreen { + displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID) + if !(status == 200 || status == 204) || err != nil { + app.err.Printf("Failed to get DisplayPrefs (%d): %v", status, err) + respond(500, "Couldn't get displayprefs", gc) + return + } + configuration = user.Configuration + } + } + app.info.Printf("Applying settings to %d user(s) from %s", len(req.ApplyTo), applyingFrom) + errors := errorListDTO{ + "policy": map[string]string{}, + "homescreen": map[string]string{}, + "ombi": map[string]string{}, + "jellyseerr": map[string]string{}, + } + /* Jellyfin doesn't seem to like too many of these requests sent in succession + and can crash and mess up its database. Issue #160 says this occurs when more + than 100 users are modified. A delay totalling 500ms between requests is used + if so. */ + var shouldDelay bool = len(req.ApplyTo) >= 100 + if shouldDelay { + app.debug.Println("Adding delay between requests for large batch") + } + for _, id := range req.ApplyTo { + var status int + var err error + if req.Policy { + status, err = app.jf.SetPolicy(id, policy) + if !(status == 200 || status == 204) || err != nil { + errors["policy"][id] = fmt.Sprintf("%d: %s", status, err) + } + } + if shouldDelay { + time.Sleep(250 * time.Millisecond) + } + if req.Homescreen { + status, err = app.jf.SetConfiguration(id, configuration) + errorString := "" + if !(status == 200 || status == 204) || err != nil { + errorString += fmt.Sprintf("Configuration %d: %v ", status, err) + } else { + status, err = app.jf.SetDisplayPreferences(id, displayprefs) + if !(status == 200 || status == 204) || err != nil { + errorString += fmt.Sprintf("Displayprefs %d: %v ", status, err) + } + } + if errorString != "" { + errors["homescreen"][id] = errorString + } + } + if ombi != nil { + errorString := "" + user, status, err := app.getOmbiUser(id) + if status != 200 || err != nil { + errorString += fmt.Sprintf("Ombi GetUser %d: %v ", status, err) + } else { + // newUser := ombi + // newUser["id"] = user["id"] + // newUser["userName"] = user["userName"] + // newUser["alias"] = user["alias"] + // newUser["emailAddress"] = user["emailAddress"] + status, err = app.applyOmbiProfile(user, ombi) + if status != 200 || err != nil { + errorString += fmt.Sprintf("Apply %d: %v ", status, err) + } + } + if errorString != "" { + errors["ombi"][id] = errorString + } + } + if jellyseerr.Enabled { + errorString := "" + // newUser := ombi + // newUser["id"] = user["id"] + // newUser["userName"] = user["userName"] + // newUser["alias"] = user["alias"] + // newUser["emailAddress"] = user["emailAddress"] + err := app.js.ApplyTemplateToUser(id, jellyseerr.User) + if err != nil { + errorString += fmt.Sprintf("ApplyUser: %v ", err) + } + err = app.js.ApplyNotificationsTemplateToUser(id, jellyseerr.Notifications) + if err != nil { + errorString += fmt.Sprintf("ApplyNotifications: %v ", err) + } + if errorString != "" { + errors["jellyseerr"][id] = errorString + } + } + + if shouldDelay { + time.Sleep(250 * time.Millisecond) + } + } + code := 200 + if len(errors["policy"]) == len(req.ApplyTo) || len(errors["homescreen"]) == len(req.ApplyTo) { + code = 500 + } + gc.JSON(code, errors) +} diff --git a/api-activities.go b/api-activities.go index 17e24ad..8763f3d 100644 --- a/api-activities.go +++ b/api-activities.go @@ -2,6 +2,7 @@ package main import ( "github.com/gin-gonic/gin" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/timshannon/badgerhold/v4" ) @@ -120,7 +121,7 @@ func (app *appContext) GetActivities(gc *gin.Context) { err := app.storage.db.Find(&results, query) if err != nil { - app.err.Printf("Failed to read activities from DB: %v\n", err) + app.err.Printf(lm.FailedDBReadActivities, err) } resp := GetActivitiesRespDTO{ diff --git a/api-backups.go b/api-backups.go index 672fde2..109082e 100644 --- a/api-backups.go +++ b/api-backups.go @@ -8,6 +8,7 @@ import ( "time" "github.com/gin-gonic/gin" + lm "github.com/hrfee/jfa-go/logmessages" ) // @Summary Creates a backup of the database. @@ -35,7 +36,7 @@ func (app *appContext) GetBackup(gc *gin.Context) { ok := (strings.HasPrefix(fname, BACKUP_PREFIX) || strings.HasPrefix(fname, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX)) && strings.HasSuffix(fname, BACKUP_SUFFIX) t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(fname, BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX)) if !ok || err != nil || t.IsZero() { - app.debug.Printf("Ignoring backup DL request due to fname: %v\n", err) + app.debug.Printf(lm.IgnoreInvalidFilename, fname, err) respondBool(400, false, gc) return } @@ -83,7 +84,7 @@ func (app *appContext) RestoreLocalBackup(gc *gin.Context) { ok := strings.HasPrefix(fname, BACKUP_PREFIX) && strings.HasSuffix(fname, BACKUP_SUFFIX) t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(fname, BACKUP_PREFIX), BACKUP_SUFFIX)) if !ok || err != nil || t.IsZero() { - app.debug.Printf("Ignoring backup DL request due to fname: %v\n", err) + app.debug.Printf(lm.IgnoreInvalidFilename, fname, err) respondBool(400, false, gc) return } @@ -103,15 +104,15 @@ func (app *appContext) RestoreLocalBackup(gc *gin.Context) { func (app *appContext) RestoreBackup(gc *gin.Context) { file, err := gc.FormFile("backups-file") if err != nil { - app.err.Printf("Failed to get file from form data: %v\n", err) + app.err.Printf(lm.FailedGetUpload, err) respondBool(400, false, gc) return } - app.debug.Printf("Got uploaded file \"%s\"\n", file.Filename) + app.debug.Printf(lm.GetUpload, file.Filename) path := app.config.Section("backups").Key("path").String() fullpath := filepath.Join(path, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX+time.Now().Local().Format(BACKUP_DATEFMT)+BACKUP_SUFFIX) gc.SaveUploadedFile(file, fullpath) - app.debug.Printf("Saved to \"%s\"\n", fullpath) + app.debug.Printf(lm.Write, fullpath) LOADBAK = fullpath app.restart(gc) } diff --git a/api-invites.go b/api-invites.go index b8885ec..d8e53a1 100644 --- a/api-invites.go +++ b/api-invites.go @@ -8,6 +8,7 @@ import ( "time" "github.com/gin-gonic/gin" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/itchyny/timefmt-go" "github.com/lithammer/shortuuid/v3" "github.com/timshannon/badgerhold/v4" @@ -29,6 +30,7 @@ func GenerateInviteCode() string { return inviteCode } +// checkInvites performs general housekeeping on invites, i.e. deleting expired ones and cleaning captcha data. func (app *appContext) checkInvites() { currentTime := time.Now() for _, data := range app.storage.GetInvites() { @@ -52,60 +54,11 @@ func (app *appContext) checkInvites() { if !currentTime.After(expiry) { continue } - - app.debug.Printf("Housekeeping: Deleting old invite %s", data.Code) - - // Disable referrals for the user if UseReferralExpiry is enabled, so no new ones are made. - if data.IsReferral && data.UseReferralExpiry && data.ReferrerJellyfinID != "" { - user, ok := app.storage.GetEmailsKey(data.ReferrerJellyfinID) - if ok { - user.ReferralTemplateKey = "" - app.storage.SetEmailsKey(data.ReferrerJellyfinID, user) - } - } - notify := data.Notify - if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { - app.debug.Printf("%s: Expiry notification", data.Code) - var wait sync.WaitGroup - for address, settings := range notify { - if !settings["notify-expiry"] { - continue - } - wait.Add(1) - go func(addr string) { - defer wait.Done() - msg, err := app.email.constructExpiry(data.Code, data, app, false) - if err != nil { - app.err.Printf("%s: Failed to construct expiry notification: %v", data.Code, err) - } else { - // Check whether notify "address" is an email address of Jellyfin ID - if strings.Contains(addr, "@") { - err = app.email.send(msg, addr) - } else { - err = app.sendByID(msg, addr) - } - if err != nil { - app.err.Printf("%s: Failed to send expiry notification: %v", data.Code, err) - } else { - app.info.Printf("Sent expiry notification to %s", addr) - } - } - }(address) - } - wait.Wait() - } - app.storage.DeleteInvitesKey(data.Code) - - app.storage.SetActivityKey(shortuuid.New(), Activity{ - Type: ActivityDeleteInvite, - SourceType: ActivityDaemon, - InviteCode: data.Code, - Value: data.Label, - Time: time.Now(), - }, nil, false) + app.deleteExpiredInvite(data) } } +// checkInvite checks the validity of a specific invite, optionally removing it if invalid(ated). func (app *appContext) checkInvite(code string, used bool, username string) bool { currentTime := time.Now() inv, match := app.storage.GetInvitesKey(code) @@ -114,54 +67,8 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool } expiry := inv.ValidTill if currentTime.After(expiry) { - app.debug.Printf("Housekeeping: Deleting old invite %s", code) - notify := inv.Notify - if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { - app.debug.Printf("%s: Expiry notification", code) - var wait sync.WaitGroup - for address, settings := range notify { - if !settings["notify-expiry"] { - continue - } - wait.Add(1) - go func(addr string) { - defer wait.Done() - msg, err := app.email.constructExpiry(code, inv, app, false) - if err != nil { - app.err.Printf("%s: Failed to construct expiry notification: %v", code, err) - } else { - // Check whether notify "address" is an email address of Jellyfin ID - if strings.Contains(addr, "@") { - err = app.email.send(msg, addr) - } else { - err = app.sendByID(msg, addr) - } - if err != nil { - app.err.Printf("%s: Failed to send expiry notification: %v", code, err) - } else { - app.info.Printf("Sent expiry notification to %s", addr) - } - } - }(address) - } - wait.Wait() - } - if inv.IsReferral && inv.ReferrerJellyfinID != "" && inv.UseReferralExpiry { - user, ok := app.storage.GetEmailsKey(inv.ReferrerJellyfinID) - if ok { - user.ReferralTemplateKey = "" - app.storage.SetEmailsKey(inv.ReferrerJellyfinID, user) - } - } + app.deleteExpiredInvite(inv) match = false - app.storage.DeleteInvitesKey(code) - app.storage.SetActivityKey(shortuuid.New(), Activity{ - Type: ActivityDeleteInvite, - SourceType: ActivityDaemon, - InviteCode: code, - Value: inv.Label, - Time: time.Now(), - }, nil, false) } else if used { del := false newInv := inv @@ -187,6 +94,67 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool return match } +func (app *appContext) deleteExpiredInvite(data Invite) { + app.debug.Printf(lm.DeleteOldInvite, data.Code) + + // Disable referrals for the user if UseReferralExpiry is enabled, so no new ones are made. + if data.IsReferral && data.UseReferralExpiry && data.ReferrerJellyfinID != "" { + user, ok := app.storage.GetEmailsKey(data.ReferrerJellyfinID) + if ok { + user.ReferralTemplateKey = "" + app.storage.SetEmailsKey(data.ReferrerJellyfinID, user) + } + } + wait := app.sendAdminExpiryNotification(data) + app.storage.DeleteInvitesKey(data.Code) + + app.storage.SetActivityKey(shortuuid.New(), Activity{ + Type: ActivityDeleteInvite, + SourceType: ActivityDaemon, + InviteCode: data.Code, + Value: data.Label, + Time: time.Now(), + }, nil, false) + + if wait != nil { + wait.Wait() + } +} + +func (app *appContext) sendAdminExpiryNotification(data Invite) *sync.WaitGroup { + notify := data.Notify + if !emailEnabled || !app.config.Section("notifications").Key("enabled").MustBool(false) || len(notify) != 0 { + return nil + } + var wait sync.WaitGroup + for address, settings := range notify { + if !settings["notify-expiry"] { + continue + } + wait.Add(1) + go func(addr string) { + defer wait.Done() + msg, err := app.email.constructExpiry(data.Code, data, app, false) + if err != nil { + app.err.Printf(lm.FailedConstructExpiryAdmin, data.Code, err) + } else { + // Check whether notify "address" is an email address or Jellyfin ID + if strings.Contains(addr, "@") { + err = app.email.send(msg, addr) + } else { + err = app.sendByID(msg, addr) + } + if err != nil { + app.err.Printf(lm.FailedSendExpiryAdmin, data.Code, addr, err) + } else { + app.info.Printf(lm.SentExpiryAdmin, data.Code, addr) + } + } + }(address) + } + return &wait +} + // @Summary Create a new invite. // @Produce json // @Param generateInviteDTO body generateInviteDTO true "New invite request object" @@ -196,7 +164,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool // @tags Invites func (app *appContext) GenerateInvite(gc *gin.Context) { var req generateInviteDTO - app.debug.Println("Generating new invite") + app.debug.Println(lm.GenerateInvite) gc.BindJSON(&req) currentTime := time.Now() validTill := currentTime.AddDate(0, req.Months, req.Days) @@ -230,13 +198,12 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { addressValid := false discord := "" - app.debug.Printf("%s: Sending invite message", invite.Code) if discordEnabled && (!strings.Contains(req.SendTo, "@") || strings.HasPrefix(req.SendTo, "@")) { users := app.discord.GetUsers(req.SendTo) if len(users) == 0 { - invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo) + invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipNoUser, req.SendTo) } else if len(users) > 1 { - invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo) + invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipMultiUser, req.SendTo) } else { invite.SendTo = req.SendTo addressValid = true @@ -249,8 +216,10 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { if addressValid { msg, err := app.email.constructInvite(invite.Code, invite, app, false) if err != nil { - invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo) - app.err.Printf("%s: Failed to construct invite message: %v", invite.Code, err) + // Slight misuse of the template + invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, req.SendTo, err) + + app.err.Printf(lm.FailedConstructInviteMessage, invite.Code, err) } else { var err error if discord != "" { @@ -259,10 +228,10 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { err = app.email.send(msg, req.SendTo) } if err != nil { - invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo) - app.err.Printf("%s: %s: %v", invite.Code, invite.SendTo, err) + invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, req.SendTo, err) + app.err.Println(invite.SendTo) } else { - app.info.Printf("%s: Sent invite email to \"%s\"", invite.Code, req.SendTo) + app.info.Printf(lm.SentInviteMessage, invite.Code, req.SendTo) } } } @@ -297,7 +266,6 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { // @Security Bearer // @tags Invites func (app *appContext) GetInvites(gc *gin.Context) { - app.debug.Println("Invites requested") currentTime := time.Now() app.checkInvites() var invites []inviteDTO @@ -332,7 +300,7 @@ func (app *appContext) GetInvites(gc *gin.Context) { if err != nil { date, err := timefmt.Parse(pair[1], app.datePattern+" "+app.timePattern) if err != nil { - app.err.Printf("Failed to parse usedBy time: %v", err) + app.err.Printf(lm.FailedParseTime, err) } unix = date.Unix() } @@ -347,7 +315,6 @@ func (app *appContext) GetInvites(gc *gin.Context) { invite.SendTo = inv.SendTo } if len(inv.Notify) != 0 { - // app.err.Printf("%s has notify section: %+v, you are %s\n", inv.Code, inv.Notify, gc.GetString("jfId")) var addressOrID string if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { addressOrID = gc.GetString("jfId") @@ -397,10 +364,9 @@ func (app *appContext) GetInvites(gc *gin.Context) { func (app *appContext) SetProfile(gc *gin.Context) { var req inviteProfileDTO gc.BindJSON(&req) - app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile) // "" means "Don't apply profile" if _, ok := app.storage.GetProfileKey(req.Profile); !ok && req.Profile != "" { - app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile) + app.err.Printf(lm.FailedGetProfile, req.Profile) respond(500, "Profile not found", gc) return } @@ -424,11 +390,11 @@ func (app *appContext) SetNotify(gc *gin.Context) { gc.BindJSON(&req) changed := false for code, settings := range req { - app.debug.Printf("%s: Notification settings change requested", code) invite, ok := app.storage.GetInvitesKey(code) if !ok { - app.err.Printf("%s Notification setting change failed: Invalid code", code) - respond(400, "Invalid invite code", gc) + msg := fmt.Sprintf(lm.InvalidInviteCode, code) + app.err.Println(msg) + respond(400, msg, gc) return } var address string @@ -436,9 +402,8 @@ func (app *appContext) SetNotify(gc *gin.Context) { if jellyfinLogin { var addressAvailable bool = app.getAddressOrName(gc.GetString("jfId")) != "" if !addressAvailable { - app.err.Printf("%s: Couldn't find contact method for admin. Make sure one is set.", code) - app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId")) - respond(500, "Missing user contact method", gc) + app.err.Printf(lm.FailedGetContactMethod, gc.GetString("jfId")) + respond(500, fmt.Sprintf(lm.FailedGetContactMethod, "admin"), gc) return } address = gc.GetString("jfId") @@ -453,15 +418,12 @@ func (app *appContext) SetNotify(gc *gin.Context) { } /*else { if _, ok := invite.Notify[address]["notify-expiry"]; !ok { */ - if _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] { - invite.Notify[address]["notify-expiry"] = settings["notify-expiry"] - app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address) - changed = true - } - if _, ok := settings["notify-creation"]; ok && invite.Notify[address]["notify-creation"] != settings["notify-creation"] { - invite.Notify[address]["notify-creation"] = settings["notify-creation"] - app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings["notify-creation"], address) - changed = true + for _, notifyType := range []string{"notify-expiry", "notify-creation"} { + if _, ok := settings[notifyType]; ok && invite.Notify[address][notifyType] != settings[notifyType] { + invite.Notify[address][notifyType] = settings[notifyType] + app.debug.Printf(lm.SetAdminNotify, notifyType, settings[notifyType], address) + changed = true + } } if changed { app.storage.SetInvitesKey(code, invite) @@ -480,7 +442,6 @@ func (app *appContext) SetNotify(gc *gin.Context) { func (app *appContext) DeleteInvite(gc *gin.Context) { var req deleteInviteDTO gc.BindJSON(&req) - app.debug.Printf("%s: Deletion requested", req.Code) inv, ok := app.storage.GetInvitesKey(req.Code) if ok { app.storage.DeleteInvitesKey(req.Code) @@ -495,10 +456,10 @@ func (app *appContext) DeleteInvite(gc *gin.Context) { Time: time.Now(), }, gc, false) - app.info.Printf("%s: Invite deleted", req.Code) + app.info.Printf(lm.DeleteInvite, req.Code) respondBool(200, true, gc) return } - app.err.Printf("%s: Deletion failed: Invalid code", req.Code) + app.err.Printf(lm.FailedDeleteInvite, req.Code, "invalid code") respond(400, "Code doesn't exist", gc) } diff --git a/api-jellyseerr.go b/api-jellyseerr.go index 5ced607..31fecb5 100644 --- a/api-jellyseerr.go +++ b/api-jellyseerr.go @@ -5,6 +5,7 @@ import ( "strconv" "github.com/gin-gonic/gin" + lm "github.com/hrfee/jfa-go/logmessages" ) // @Summary Get a list of Jellyseerr users. @@ -15,14 +16,12 @@ import ( // @Security Bearer // @tags Jellyseerr func (app *appContext) JellyseerrUsers(gc *gin.Context) { - app.debug.Println("Jellyseerr users requested") users, err := app.js.GetUsers() if err != nil { - app.err.Printf("Failed to get users from Jellyseerr: %v", err) + app.err.Printf(lm.FailedGetUsers, lm.Jellyseerr, err) respond(500, "Couldn't get users", gc) return } - app.debug.Printf("Jellyseerr users retrieved: %d", len(users)) userlist := make([]ombiUser, len(users)) i := 0 for _, u := range users { @@ -60,14 +59,14 @@ func (app *appContext) SetJellyseerrProfile(gc *gin.Context) { } u, err := app.js.UserByID(jellyseerrID) if err != nil { - app.err.Printf("Couldn't get user from Jellyseerr: %v", err) + app.err.Printf(lm.FailedGetUsers, lm.Jellyseerr, err) respond(500, "Couldn't get user", gc) return } profile.Jellyseerr.User = u.UserTemplate n, err := app.js.GetNotificationPreferencesByID(jellyseerrID) if err != nil { - app.err.Printf("Couldn't get user's notification prefs from Jellyseerr: %v", err) + app.err.Printf(lm.FailedGetJellyseerrNotificationPrefs, err) respond(500, "Couldn't get user notification prefs", gc) return } diff --git a/api-messages.go b/api-messages.go index db26db0..e632c1e 100644 --- a/api-messages.go +++ b/api-messages.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/hrfee/jfa-go/jellyseerr" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/lithammer/shortuuid/v3" "gopkg.in/ini.v1" ) @@ -134,7 +135,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) { emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress") customMessage, ok := app.storage.GetCustomContentKey(id) if !ok && id != "Announcement" { - app.err.Printf("Failed to get custom message with ID \"%s\"", id) + app.err.Printf(lm.FailedGetCustomMessage, id) respondBool(400, false, gc) return } @@ -328,7 +329,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) { jellyseerr.FieldTelegram: tgUser.ChatID, jellyseerr.FieldTelegramEnabled: tgUser.Contact, }); err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } linkExistingOmbiDiscordTelegram(app) @@ -361,11 +362,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte tgUser.Contact = req.Telegram app.storage.SetTelegramKey(req.ID, tgUser) if change { - msg := "" - if !req.Telegram { - msg = " not" - } - app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg) + app.debug.Printf(lm.SetContactPrefForService, lm.Telegram, tgUser.Username, req.Telegram) jsPrefs[jellyseerr.FieldTelegramEnabled] = req.Telegram } } @@ -374,11 +371,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte dcUser.Contact = req.Discord app.storage.SetDiscordKey(req.ID, dcUser) if change { - msg := "" - if !req.Discord { - msg = " not" - } - app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg) + app.debug.Printf(lm.SetContactPrefForService, lm.Discord, dcUser.Username, req.Discord) jsPrefs[jellyseerr.FieldDiscordEnabled] = req.Discord } } @@ -387,11 +380,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte mxUser.Contact = req.Matrix app.storage.SetMatrixKey(req.ID, mxUser) if change { - msg := "" - if !req.Matrix { - msg = " not" - } - app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg) + app.debug.Printf(lm.SetContactPrefForService, lm.Matrix, mxUser.UserID, req.Matrix) } } if email, ok := app.storage.GetEmailsKey(req.ID); ok { @@ -399,18 +388,14 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte email.Contact = req.Email app.storage.SetEmailsKey(req.ID, email) if change { - msg := "" - if !req.Email { - msg = " not" - } - app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg) + app.debug.Printf(lm.SetContactPrefForService, lm.Email, email.Addr, req.Email) jsPrefs[jellyseerr.FieldEmailEnabled] = req.Email } } if app.config.Section("jellyseerr").Key("enabled").MustBool(false) { err := app.js.ModifyNotifications(req.ID, jsPrefs) if err != nil { - app.err.Printf("Failed to sync contact prefs with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } } respondBool(200, true, gc) @@ -555,7 +540,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) { func (app *appContext) MatrixCheckPIN(gc *gin.Context) { code := gc.Param("invCode") if _, ok := app.storage.GetInvitesKey(code); !ok { - app.debug.Println("Matrix: Invite code was invalid") + app.debug.Printf(lm.InvalidInviteCode, code) respondBool(401, false, gc) return } @@ -563,12 +548,12 @@ func (app *appContext) MatrixCheckPIN(gc *gin.Context) { pin := gc.Param("pin") user, ok := app.matrix.tokens[pin] if !ok { - app.debug.Println("Matrix: PIN not found") + app.debug.Printf(lm.InvalidPIN, pin) respondBool(200, false, gc) return } if user.User.UserID != userID { - app.debug.Println("Matrix: User ID of PIN didn't match") + app.debug.Printf(lm.UnauthorizedPIN, pin) respondBool(200, false, gc) return } @@ -596,7 +581,7 @@ func (app *appContext) MatrixLogin(gc *gin.Context) { } token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password) if err != nil { - app.err.Printf("Matrix: Failed to generate token: %v", err) + app.err.Printf(lm.FailedGenerateToken, err) respond(401, "Unauthorized", gc) return } @@ -607,7 +592,7 @@ func (app *appContext) MatrixLogin(gc *gin.Context) { matrix.Key("token").SetValue(token) matrix.Key("user_id").SetValue(req.Username) if err := tempConfig.SaveTo(app.configPath); err != nil { - app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err) + app.err.Printf(lm.FailedWriting, app.configPath, err) respondBool(500, false, gc) return } @@ -631,7 +616,7 @@ func (app *appContext) MatrixConnect(gc *gin.Context) { } roomID, encrypted, err := app.matrix.CreateRoom(req.UserID) if err != nil { - app.err.Printf("Matrix: Failed to create room: %v", err) + app.err.Printf(lm.FailedCreateRoom, err) respondBool(500, false, gc) return } @@ -701,7 +686,7 @@ func (app *appContext) DiscordConnect(gc *gin.Context) { jellyseerr.FieldDiscord: req.DiscordID, jellyseerr.FieldDiscordEnabled: true, }); err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } app.storage.SetActivityKey(shortuuid.New(), Activity{ @@ -739,7 +724,7 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) { jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier, jellyseerr.FieldDiscordEnabled: false, }); err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } app.storage.SetActivityKey(shortuuid.New(), Activity{ @@ -775,7 +760,7 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) { jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier, jellyseerr.FieldTelegramEnabled: false, }); err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } app.storage.SetActivityKey(shortuuid.New(), Activity{ diff --git a/api-ombi.go b/api-ombi.go index ab9b534..b9fc870 100644 --- a/api-ombi.go +++ b/api-ombi.go @@ -5,6 +5,7 @@ import ( "net/url" "github.com/gin-gonic/gin" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/hrfee/mediabrowser" ) @@ -66,10 +67,9 @@ func (app *appContext) getOmbiImportedUser(name string) (map[string]interface{}, // @Security Bearer // @tags Ombi func (app *appContext) OmbiUsers(gc *gin.Context) { - app.debug.Println("Ombi users requested") users, status, err := app.ombi.GetUsers() if err != nil || status != 200 { - app.err.Printf("Failed to get users from Ombi (%d): %v", status, err) + app.err.Printf(lm.FailedGetUsers, lm.Ombi, err) respond(500, "Couldn't get users", gc) return } @@ -105,7 +105,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) { } template, code, err := app.ombi.TemplateByID(req.ID) if err != nil || code != 200 || len(template) == 0 { - app.err.Printf("Couldn't get user from Ombi (%d): %v", code, err) + app.err.Printf(lm.FailedGetUsers, lm.Ombi, err) respond(500, "Couldn't get user", gc) return } diff --git a/api-profiles.go b/api-profiles.go index 482abbb..40367da 100644 --- a/api-profiles.go +++ b/api-profiles.go @@ -1,9 +1,11 @@ package main import ( + "fmt" "time" "github.com/gin-gonic/gin" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/timshannon/badgerhold/v4" ) @@ -14,7 +16,6 @@ import ( // @Security Bearer // @tags Profiles & Settings func (app *appContext) GetProfiles(gc *gin.Context) { - app.debug.Println("Profiles requested") out := getProfilesDTO{ DefaultProfile: app.storage.GetDefaultProfile().Name, Profiles: map[string]profileDTO{}, @@ -52,10 +53,11 @@ func (app *appContext) GetProfiles(gc *gin.Context) { func (app *appContext) SetDefaultProfile(gc *gin.Context) { req := profileChangeDTO{} gc.BindJSON(&req) - app.info.Printf("Setting default profile to \"%s\"", req.Name) + app.info.Printf(lm.SetDefaultProfile, req.Name) if _, ok := app.storage.GetProfileKey(req.Name); !ok { - app.err.Printf("Profile not found: \"%s\"", req.Name) - respond(500, "Profile not found", gc) + msg := fmt.Sprintf(lm.FailedGetProfile, req.Name) + app.err.Println(msg) + respond(500, msg, gc) return } app.storage.db.ForEach(&badgerhold.Query{}, func(profile *Profile) error { @@ -79,13 +81,12 @@ func (app *appContext) SetDefaultProfile(gc *gin.Context) { // @Security Bearer // @tags Profiles & Settings func (app *appContext) CreateProfile(gc *gin.Context) { - app.info.Println("Profile creation requested") var req newProfileDTO gc.BindJSON(&req) app.jf.CacheExpiry = time.Now() user, status, err := app.jf.UserByID(req.ID, false) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get user from Jellyfin (%d): %v", status, err) + app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) respond(500, "Couldn't get user", gc) return } @@ -94,12 +95,12 @@ func (app *appContext) CreateProfile(gc *gin.Context) { Policy: user.Policy, Homescreen: req.Homescreen, } - app.debug.Printf("Creating profile from user \"%s\"", user.Name) + app.debug.Printf(lm.CreateProfileFromUser, user.Name) if req.Homescreen { profile.Configuration = user.Configuration profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get DisplayPrefs (%d): %v", status, err) + app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err) respond(500, "Couldn't get displayprefs", gc) return } @@ -145,13 +146,13 @@ func (app *appContext) EnableReferralForProfile(gc *gin.Context) { inv, ok := app.storage.GetInvitesKey(invCode) if !ok { respond(400, "Invalid invite code", gc) - app.err.Printf("\"%s\": Failed to enable referrals: invite not found", profileName) + app.err.Printf(lm.InvalidInviteCode, invCode) return } profile, ok := app.storage.GetProfileKey(profileName) if !ok { respond(400, "Invalid profile", gc) - app.err.Printf("\"%s\": Failed to enable referrals: profile not found", profileName) + app.err.Printf(lm.FailedGetProfile, profileName) return } diff --git a/api-userpage.go b/api-userpage.go index 93d9490..28fc77a 100644 --- a/api-userpage.go +++ b/api-userpage.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "net/http" "os" "strings" @@ -9,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" "github.com/hrfee/jfa-go/jellyseerr" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/lithammer/shortuuid/v3" "github.com/timshannon/badgerhold/v4" ) @@ -29,7 +31,7 @@ func (app *appContext) MyDetails(gc *gin.Context) { user, status, err := app.jf.UserByID(resp.Id, false) if status != 200 || err != nil { - app.err.Printf("Failed to get Jellyfin user (%d): %+v\n", status, err) + app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) respond(500, "Failed to get user", gc) return } @@ -133,8 +135,9 @@ func (app *appContext) SetMyContactMethods(gc *gin.Context) { func (app *appContext) LogoutUser(gc *gin.Context) { cookie, err := gc.Cookie("user-refresh") if err != nil { - app.debug.Printf("Couldn't get cookies: %s", err) - respond(500, "Couldn't fetch cookies", gc) + msg := fmt.Sprintf(lm.FailedGetCookies, "user-refresh", err) + app.debug.Println(msg) + respond(500, msg, gc) return } app.invalidTokens = append(app.invalidTokens, cookie) @@ -174,21 +177,21 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) { } token, err := jwt.Parse(key, checkToken) if err != nil { - app.err.Printf("Failed to parse key: %s", err) + app.err.Printf(lm.FailedParseJWT, err) fail() // respond(500, "unknownError", gc) return } claims, ok := token.Claims.(jwt.MapClaims) if !ok { - app.err.Printf("Failed to parse key: %s", err) + app.err.Println(lm.FailedCastJWT) fail() // respond(500, "unknownError", gc) return } expiry := time.Unix(int64(claims["exp"].(float64)), 0) if !(ok && token.Valid && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) { - app.err.Printf("Invalid key") + app.err.Println(lm.InvalidJWT) fail() // respond(400, "invalidKey", gc) return @@ -212,7 +215,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) { Time: time.Now(), }, gc, true) - app.info.Println("Email list modified") + app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId")) gc.Redirect(http.StatusSeeOther, "/my/account") return } @@ -231,7 +234,6 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) { func (app *appContext) ModifyMyEmail(gc *gin.Context) { var req ModifyMyEmailDTO gc.BindJSON(&req) - app.debug.Println("Email modification requested") if !strings.ContainsRune(req.Email, '@') { respond(400, "Invalid Email Address", gc) return @@ -251,7 +253,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) { key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) if err != nil { - app.err.Printf("Failed to generate confirmation token: %v", err) + app.err.Printf(lm.FailedSignJWT, err) respond(500, "errorUnknown", gc) return } @@ -262,15 +264,15 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) { if status == 200 && err == nil { name = user.Name } - app.debug.Printf("%s: Email confirmation required", id) + app.debug.Printf(lm.EmailConfirmationRequired, id) respond(401, "confirmEmail", gc) msg, err := app.email.constructConfirmation("", name, key, app, false) if err != nil { - app.err.Printf("%s: Failed to construct confirmation email: %v", name, err) + app.err.Printf(lm.FailedConstructConfirmationEmail, id, err) } else if err := app.email.send(msg, req.Email); err != nil { - app.err.Printf("%s: Failed to send user confirmation email: %v", name, err) + app.err.Printf(lm.FailedSendConfirmationEmail, id, req.Email, err) } else { - app.info.Printf("%s: Sent user confirmation email to \"%s\"", name, req.Email) + app.err.Printf(lm.SentConfirmationEmail, id, req.Email) } return } @@ -358,7 +360,7 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) { jellyseerr.FieldDiscord: dcUser.ID, jellyseerr.FieldDiscordEnabled: dcUser.Contact, }); err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } app.storage.SetActivityKey(shortuuid.New(), Activity{ @@ -413,7 +415,7 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) { jellyseerr.FieldTelegram: tgUser.ChatID, jellyseerr.FieldTelegramEnabled: tgUser.Contact, }); err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } app.storage.SetActivityKey(shortuuid.New(), Activity{ @@ -477,12 +479,12 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) { pin := gc.Param("pin") user, ok := app.matrix.tokens[pin] if !ok { - app.debug.Println("Matrix: PIN not found") + app.debug.Printf(lm.InvalidPIN, pin) respondBool(200, false, gc) return } if user.User.UserID != userID { - app.debug.Println("Matrix: User ID of PIN didn't match") + app.debug.Printf(lm.UnauthorizedPIN, pin) respondBool(200, false, gc) return } @@ -523,7 +525,7 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) { jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier, jellyseerr.FieldDiscordEnabled: false, }); err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } app.storage.SetActivityKey(shortuuid.New(), Activity{ @@ -551,7 +553,7 @@ func (app *appContext) UnlinkMyTelegram(gc *gin.Context) { jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier, jellyseerr.FieldTelegramEnabled: false, }); err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } app.storage.SetActivityKey(shortuuid.New(), Activity{ @@ -606,7 +608,6 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) { contactMethodAllowed := app.config.Section("user_page").Key("allow_pwr_contact_method").MustBool(true) address := gc.Param("address") if address == "" { - app.debug.Println("Ignoring empty request for PWR") cancel.Stop() respondBool(400, false, gc) return @@ -616,7 +617,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) { jfUser, ok := app.ReverseUserSearch(address, usernameAllowed, emailAllowed, contactMethodAllowed) if !ok { - app.debug.Printf("Ignoring PWR request: User not found") + app.debug.Printf(lm.FailedGetUsers, lm.Jellyfin, "no results") for range timerWait { respondBool(204, true, gc) @@ -626,7 +627,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) { } pwr, err = app.GenInternalReset(jfUser.ID) if err != nil { - app.err.Printf("Failed to get user from Jellyfin: %v", err) + app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) for range timerWait { respondBool(204, true, gc) return @@ -647,16 +648,16 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) { }, app, false, ) if err != nil { - app.err.Printf("Failed to construct password reset message for \"%s\": %v", pwr.Username, err) + app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err) for range timerWait { respondBool(204, true, gc) return } return } else if err := app.sendByID(msg, jfUser.ID); err != nil { - app.err.Printf("Failed to send password reset message to \"%s\": %v", address, err) + app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, "?", err) } else { - app.info.Printf("Sent password reset message to \"%s\"", address) + app.info.Printf(lm.SentPWRMessage, pwr.Username, "?") } for range timerWait { respondBool(204, true, gc) @@ -683,14 +684,13 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) { validation := app.validator.validate(req.New) for _, val := range validation { if !val { - app.debug.Printf("%s: Change password failed: Invalid password", gc.GetString("jfId")) gc.JSON(400, validation) return } } user, status, err := app.jf.UserByID(gc.GetString("jfId"), false) if status != 200 || err != nil { - app.err.Printf("Failed to change password: couldn't find user (%d): %+v", status, err) + app.err.Printf(lm.FailedGetUser, gc.GetString("jfId"), lm.Jellyfin, err) respondBool(500, false, gc) return } @@ -718,16 +718,16 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) { func() { ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId")) if status != 200 || err != nil { - app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", user.Name, status, err) + app.err.Printf(lm.FailedGetUser, user.Name, lm.Ombi, err) return } ombiUser["password"] = req.New status, err = app.ombi.ModifyUser(ombiUser) if status != 200 || err != nil { - app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err) + app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err) return } - app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"]) + app.debug.Printf(lm.ChangePassword, lm.Ombi, ombiUser["userName"]) }() } cookie, err := gc.Cookie("user-refresh") @@ -735,7 +735,7 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) { app.invalidTokens = append(app.invalidTokens, cookie) gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true) } else { - app.debug.Printf("Couldn't get cookies: %s", err) + app.debug.Printf(lm.FailedGetCookies, "user-refresh", err) } respondBool(204, true, gc) } @@ -761,7 +761,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) { user, ok := app.storage.GetEmailsKey(gc.GetString("jfId")) err = app.storage.db.Get(user.ReferralTemplateKey, &inv) if !ok || err != nil || user.ReferralTemplateKey == "" { - app.debug.Printf("Ignoring referral request, couldn't find template.") + app.debug.Printf(lm.FailedGetReferralTemplate, user.ReferralTemplateKey, err) respondBool(400, false, gc) return } @@ -782,6 +782,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) { // If UseReferralExpiry is enabled, we delete it and return nothing. app.storage.DeleteInvitesKey(inv.Code) if inv.UseReferralExpiry { + app.debug.Printf(lm.DeleteOldReferral, inv.Code) user, ok := app.storage.GetEmailsKey(gc.GetString("jfId")) if ok { user.ReferralTemplateKey = "" @@ -791,6 +792,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) { respondBool(400, false, gc) return } + app.debug.Printf(lm.RenewOldReferral, inv.Code) inv.Code = GenerateInviteCode() inv.Created = time.Now() inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour) diff --git a/api-users.go b/api-users.go index ec41a94..d36ef66 100644 --- a/api-users.go +++ b/api-users.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" "github.com/hrfee/jfa-go/jellyseerr" + lm "github.com/hrfee/jfa-go/logmessages" "github.com/hrfee/mediabrowser" "github.com/lithammer/shortuuid/v3" "github.com/timshannon/badgerhold/v4" @@ -36,14 +37,14 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { gc.BindJSON(&req) existingUser, _, _ := app.jf.UserByName(req.Username, false) if existingUser.Name != "" { - msg := fmt.Sprintf("User already exists named %s", req.Username) - app.info.Printf("%s New user failed: %s", req.Username, msg) + msg := lm.UserExists + app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, msg) respondUser(401, false, false, msg, gc) return } user, status, err := app.jf.NewUser(req.Username, req.Password) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("%s New user failed (%d): %v", req.Username, status, err) + app.err.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, err) respondUser(401, false, false, err.Error(), gc) return } @@ -64,19 +65,19 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { if p, ok := app.storage.GetProfileKey(req.Profile); ok { profile = p } else { - app.debug.Printf("Couldn't find profile \"%s\", using default", req.Profile) + app.debug.Printf(lm.FailedGetProfile+lm.FallbackToDefault, req.Profile) } status, err = app.jf.SetPolicy(id, profile.Policy) if !(status == 200 || status == 204 || err == nil) { - app.err.Printf("%s: Failed to set user policy (%d): %v", req.Username, status, err) + app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, req.Username, err) } status, err = app.jf.SetConfiguration(id, profile.Configuration) if (status == 200 || status == 204) && err == nil { status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs) } if !((status == 200 || status == 204) && err == nil) { - app.err.Printf("%s: Failed to set configuration template (%d): %v", req.Username, status, err) + app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, req.Username, err) } } app.jf.CacheExpiry = time.Now() @@ -89,48 +90,47 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { } errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi) if err != nil || code != 200 { - app.err.Printf("Failed to create Ombi user (%d): %v", code, err) - app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", ")) + app.err.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err) + app.debug.Printf(lm.AdditionalOmbiErrors, strings.Join(errors, ", ")) } else { - app.info.Println("Created Ombi user") + app.info.Printf(lm.CreateUser, lm.Ombi, req.Username) } } if app.config.Section("jellyseerr").Key("enabled").MustBool(false) { // Gets existing user (not possible) or imports the given user. _, err := app.js.MustGetUser(id) if err != nil { - app.err.Printf("Failed to create Jellyseerr user: %v", err) + app.err.Printf(lm.FailedCreateUser, lm.Jellyseerr, req.Username, err) } else { - app.info.Println("Created Jellyseerr user") + app.info.Printf(lm.CreateUser, lm.Jellyseerr, req.Username) } err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User) if err != nil { - app.err.Printf("Failed to apply Jellyseerr user template: %v\n", err) + app.err.Printf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, req.Username, err) } err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications) if err != nil { - app.err.Printf("Failed to apply Jellyseerr notifications template: %v\n", err) + app.err.Printf(lm.FailedApplyTemplate, "notifications", lm.Jellyseerr, req.Username, err) } if emailEnabled { err = app.js.ModifyUser(id, map[jellyseerr.UserField]any{jellyseerr.FieldEmail: req.Email}) if err != nil { - app.err.Printf("Failed to set Jellyseerr email address: %v\n", err) + app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, id, err) } } } if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" { - app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email) msg, err := app.email.constructWelcome(req.Username, time.Time{}, app, false) if err != nil { - app.err.Printf("%s: Failed to construct welcome email: %v", req.Username, err) + app.err.Printf(lm.FailedConstructWelcomeMessage, id, err) respondUser(500, true, false, err.Error(), gc) return } else if err := app.email.send(msg, req.Email); err != nil { - app.err.Printf("%s: Failed to send welcome email: %v", req.Username, err) + app.err.Printf(lm.FailedSendWelcomeMessage, req.Username, req.Email, err) respondUser(500, true, false, err.Error(), gc) return } else { - app.info.Printf("%s: Sent welcome email to %s", req.Username, req.Email) + app.info.Printf(lm.SentWelcomeMessage, req.Username, req.Email) } } respondUser(200, true, true, "", gc) @@ -143,8 +143,8 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) existingUser, _, _ := app.jf.UserByName(req.Username, false) if existingUser.Name != "" { f = func(gc *gin.Context) { - msg := fmt.Sprintf("User %s already exists", req.Username) - app.info.Printf("%s: New user failed: %s", req.Code, msg) + msg := lm.UserExists + app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, msg) respond(401, "errorUserExists", gc) } success = false @@ -156,7 +156,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) if req.DiscordPIN == "" { if app.config.Section("discord").Key("required").MustBool(false) { f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Discord verification not completed", req.Code) + app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, lm.AccountUnverified) respond(401, "errorDiscordVerification", gc) } success = false @@ -166,7 +166,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) discordUser, discordVerified = app.discord.UserVerified(req.DiscordPIN) if !discordVerified { f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Discord PIN was invalid", req.Code) + app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, fmt.Sprintf(lm.InvalidPIN, req.DiscordPIN)) respond(401, "errorInvalidPIN", gc) } success = false @@ -174,7 +174,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) } if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(discordUser.ID) { f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Discord user already linked", req.Code) + app.debug.Printf(lm.FailedLinkUser, lm.Discord, discordUser.ID, req.Code, lm.AccountLinked) respond(400, "errorAccountLinked", gc) } success = false @@ -183,7 +183,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) err := app.discord.ApplyRole(discordUser.ID) if err != nil { f = func(gc *gin.Context) { - app.err.Printf("%s: New user failed: Failed to set member role: %v", req.Code, err) + app.err.Printf(lm.FailedLinkUser, lm.Discord, discordUser.ID, req.Code, fmt.Sprintf(lm.FailedSetDiscordMemberRole, err)) respond(401, "error", gc) } success = false @@ -197,7 +197,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) if req.MatrixPIN == "" { if app.config.Section("matrix").Key("required").MustBool(false) { f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Matrix verification not completed", req.Code) + app.info.Printf(lm.FailedLinkUser, lm.Matrix, "?", req.Code, lm.AccountUnverified) respond(401, "errorMatrixVerification", gc) } success = false @@ -208,7 +208,11 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) if !ok || !user.Verified { matrixVerified = false f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Matrix PIN was invalid", req.Code) + uid := "" + if ok { + uid = user.User.UserID + } + app.info.Printf(lm.FailedLinkUser, lm.Matrix, uid, req.Code, fmt.Sprintf(lm.InvalidPIN, req.MatrixPIN)) respond(401, "errorInvalidPIN", gc) } success = false @@ -216,7 +220,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) } if app.config.Section("matrix").Key("require_unique").MustBool(false) && app.matrix.UserExists(user.User.UserID) { f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Matrix user already linked", req.Code) + app.debug.Printf(lm.FailedLinkUser, lm.Matrix, user.User.UserID, req.Code, lm.AccountLinked) respond(400, "errorAccountLinked", gc) } success = false @@ -233,7 +237,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) if req.TelegramPIN == "" { if app.config.Section("telegram").Key("required").MustBool(false) { f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Telegram verification not completed", req.Code) + app.info.Printf(lm.FailedLinkUser, lm.Telegram, "?", req.Code, lm.AccountUnverified) respond(401, "errorTelegramVerification", gc) } success = false @@ -243,7 +247,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) tgToken, telegramVerified = app.telegram.TokenVerified(req.TelegramPIN) if !telegramVerified { f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Telegram PIN was invalid", req.Code) + app.info.Printf(lm.FailedLinkUser, lm.Telegram, tgToken.Username, req.Code, fmt.Sprintf(lm.InvalidPIN, req.TelegramPIN)) respond(401, "errorInvalidPIN", gc) } success = false @@ -251,7 +255,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) } if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(tgToken.Username) { f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Telegram user already linked", req.Code) + app.debug.Printf(lm.FailedLinkUser, lm.Telegram, tgToken.Username, req.Code, lm.AccountLinked) respond(400, "errorAccountLinked", gc) } success = false @@ -270,7 +274,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) if err != nil { f = func(gc *gin.Context) { - app.info.Printf("Failed to generate confirmation token: %v", err) + app.info.Printf(lm.FailedSignJWT, err) respond(500, "errorUnknown", gc) } success = false @@ -288,15 +292,15 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) app.ConfirmationKeys[req.Code] = cKeys app.confirmationKeysLock.Unlock() f = func(gc *gin.Context) { - app.debug.Printf("%s: Email confirmation required", req.Code) + app.debug.Printf(lm.EmailConfirmationRequired, req.Username) respond(401, "confirmEmail", gc) msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app, false) if err != nil { - app.err.Printf("%s: Failed to construct confirmation email: %v", req.Code, err) + app.err.Printf(lm.FailedConstructConfirmationEmail, req.Code, err) } else if err := app.email.send(msg, req.Email); err != nil { - app.err.Printf("%s: Failed to send user confirmation email: %v", req.Code, err) + app.err.Printf(lm.FailedSendConfirmationEmail, req.Code, req.Email, err) } else { - app.info.Printf("%s: Sent user confirmation email to \"%s\"", req.Code, req.Email) + app.err.Printf(lm.SentConfirmationEmail, req.Code, req.Email) } } success = false @@ -306,7 +310,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) user, status, err := app.jf.NewUser(req.Username, req.Password) if !(status == 200 || status == 204) || err != nil { f = func(gc *gin.Context) { - app.err.Printf("%s New user failed (%d): %v", req.Code, status, err) + app.err.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, err) respond(401, app.storage.lang.Admin[app.storage.lang.chosenAdminLang].Notifications.get("errorUnknown"), gc) } success = false @@ -320,7 +324,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) go func(addr string) { msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app, false) if err != nil { - app.err.Printf("%s: Failed to construct user creation notification: %v", req.Code, err) + app.err.Printf(lm.FailedConstructCreationAdmin, req.Code, err) } else { // Check whether notify "addr" is an email address of Jellyfin ID if strings.Contains(addr, "@") { @@ -329,9 +333,9 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) err = app.sendByID(msg, addr) } if err != nil { - app.err.Printf("%s: Failed to send user creation notification: %v", req.Code, err) + app.err.Printf(lm.FailedSendCreationAdmin, req.Code, addr, err) } else { - app.info.Printf("Sent user creation notification to %s", addr) + app.info.Printf(lm.SentCreationAdmin, req.Code, addr) } } }(address) @@ -373,24 +377,22 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) var profile Profile if invite.Profile != "" { - app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile) + app.debug.Printf(lm.ApplyProfile, invite.Profile) var ok bool profile, ok = app.storage.GetProfileKey(invite.Profile) if !ok { profile = app.storage.GetDefaultProfile() } - app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile) status, err = app.jf.SetPolicy(id, profile.Policy) if !((status == 200 || status == 204) && err == nil) { - app.err.Printf("%s: Failed to set user policy (%d): %v", req.Code, status, err) + app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, id, err) } - app.debug.Printf("Applying homescreen from profile \"%s\"", invite.Profile) status, err = app.jf.SetConfiguration(id, profile.Configuration) if (status == 200 || status == 204) && err == nil { status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs) } if !((status == 200 || status == 204) && err == nil) { - app.err.Printf("%s: Failed to set configuration template (%d): %v", req.Code, status, err) + app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, id, err) } if app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false) && profile.ReferralTemplateKey != "" { emailStore.ReferralTemplateKey = profile.ReferralTemplateKey @@ -454,23 +456,23 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) // Check if on the off chance, Ombi's user importer has already added the account. ombiUser, status, err = app.getOmbiImportedUser(req.Username) if status == 200 && err == nil { - app.info.Println("Found existing Ombi user, applying changes") + app.info.Println(lm.Ombi + " " + lm.UserExists) accountExists = true template["password"] = req.Password status, err = app.applyOmbiProfile(ombiUser, template) if status != 200 || err != nil { - app.err.Printf("Failed to modify existing Ombi user (%d): %v\n", status, err) + app.err.Printf(lm.FailedApplyProfile, lm.Ombi, req.Username, err) } } else { - app.info.Printf("Failed to create Ombi user (%d): %s", code, err) - app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", ")) + app.info.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err) + app.debug.Printf(lm.AdditionalOmbiErrors, strings.Join(errors, ", ")) } } else { ombiUser, status, err = app.getOmbiUser(id) if status != 200 || err != nil { - app.err.Printf("Failed to get Ombi user (%d): %v", status, err) + app.err.Printf(lm.FailedGetUser, id, lm.Ombi, err) } else { - app.info.Println("Created Ombi user") + app.info.Println(lm.CreateUser, lm.Ombi, id) accountExists = true } } @@ -487,13 +489,11 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) } resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to link Telegram/Discord to Ombi (%d): %v", status, err) - app.debug.Printf("Response: %v", resp) + app.err.Printf(lm.FailedSyncContactMethods, lm.Ombi, err) + app.debug.Printf(lm.AdditionalOmbiErrors, resp) } } } - } else { - app.debug.Printf("Skipping Ombi: Profile \"%s\" was empty", invite.Profile) } } if invite.Profile != "" && app.config.Section("jellyseerr").Key("enabled").MustBool(false) { @@ -501,23 +501,23 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) // Gets existing user (not possible) or imports the given user. _, err := app.js.MustGetUser(id) if err != nil { - app.err.Printf("Failed to create Jellyseerr user: %v", err) + app.err.Printf(lm.FailedCreateUser, lm.Jellyseerr, id, err) } else { - app.info.Println("Created Jellyseerr user") + app.info.Printf(lm.CreateUser, lm.Jellyseerr, id) } err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User) if err != nil { - app.err.Printf("Failed to apply Jellyseerr user template: %v\n", err) + app.err.Printf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, id, err) } err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications) if err != nil { - app.err.Printf("Failed to apply Jellyseerr notifications template: %v\n", err) + app.err.Printf(lm.FailedApplyTemplate, "notifications", lm.Jellyseerr, id, err) } contactMethods := map[jellyseerr.NotificationsField]any{} if emailEnabled { err = app.js.ModifyMainUserSettings(id, jellyseerr.MainUserSettings{Email: req.Email}) if err != nil { - app.err.Printf("Failed to set Jellyseerr email address: %v\n", err) + app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, id, err) } else { contactMethods[jellyseerr.FieldEmailEnabled] = req.EmailContact } @@ -534,11 +534,9 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) if emailEnabled || discordVerified || telegramVerified { err := app.js.ModifyNotifications(id, contactMethods) if err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } } - } else { - app.debug.Printf("Skipping Jellyseerr: Profile \"%s\" was empty", invite.Profile) } } if matrixVerified { @@ -551,14 +549,13 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) } if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramVerified || discordVerified || matrixVerified { name := app.getAddressOrName(user.ID) - app.debug.Printf("%s: Sending welcome message to %s", req.Username, name) msg, err := app.email.constructWelcome(req.Username, expiry, app, false) if err != nil { - app.err.Printf("%s: Failed to construct welcome message: %v", req.Username, err) + app.err.Printf(lm.FailedConstructWelcomeMessage, id, err) } else if err := app.sendByID(msg, user.ID); err != nil { - app.err.Printf("%s: Failed to send welcome message: %v", req.Username, err) + app.err.Printf(lm.FailedSendWelcomeMessage, id, req.Email, err) } else { - app.info.Printf("%s: Sent welcome message to \"%s\"", req.Username, name) + app.info.Printf(lm.SentWelcomeMessage, id, req.Email) } } app.jf.CacheExpiry = time.Now() @@ -576,14 +573,13 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) func (app *appContext) NewUser(gc *gin.Context) { var req newUserDTO gc.BindJSON(&req) - app.debug.Printf("%s: New user attempt", req.Code) if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText, false) { - app.info.Printf("%s: New user failed: Captcha Incorrect", req.Code) + app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, lm.IncorrectCaptcha) respond(400, "errorCaptcha", gc) return } if !app.checkInvite(req.Code, false, "") { - app.info.Printf("%s New user failed: invalid code", req.Code) + app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, fmt.Sprintf(lm.InvalidInviteCode, req.Code)) respond(401, "errorInvalidCode", gc) return } @@ -597,18 +593,15 @@ func (app *appContext) NewUser(gc *gin.Context) { } if !valid { // 200 bcs idk what i did in js - app.info.Printf("%s: New user failed: Invalid password", req.Code) gc.JSON(200, validation) return } if emailEnabled { if app.config.Section("email").Key("required").MustBool(false) && !strings.Contains(req.Email, "@") { - app.info.Printf("%s: New user failed: Email Required", req.Code) respond(400, "errorNoEmail", gc) return } if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" && app.EmailAddressExists(req.Email) { - app.info.Printf("%s: New user failed: Email already in use", req.Code) respond(400, "errorEmailLinked", gc) return } @@ -653,7 +646,7 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) { msg, err = app.email.constructDisabled(req.Reason, app, false) } if err != nil { - app.err.Printf("Failed to construct account enabled/disabled emails: %v", err) + app.err.Printf(lm.FailedConstructEnableDisableMessage, "?", err) sendMail = false } } @@ -665,14 +658,14 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) { user, status, err := app.jf.UserByID(userID, false) if status != 200 || err != nil { errors["GetUser"][userID] = fmt.Sprintf("%d %v", status, err) - app.err.Printf("Failed to get user \"%s\" (%d): %v", userID, status, err) + app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err) continue } user.Policy.IsDisabled = !req.Enabled status, err = app.jf.SetPolicy(userID, user.Policy) if !(status == 200 || status == 204) || err != nil { errors["SetPolicy"][userID] = fmt.Sprintf("%d %v", status, err) - app.err.Printf("Failed to set policy for user \"%s\" (%d): %v", userID, status, err) + app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, userID, err) continue } @@ -687,7 +680,7 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) { if sendMail && req.Notify { if err := app.sendByID(msg, userID); err != nil { - app.err.Printf("Failed to send account enabled/disabled email: %v", err) + app.err.Printf(lm.FailedSendEnableDisableMessage, userID, "?", err) continue } } @@ -720,7 +713,7 @@ func (app *appContext) DeleteUsers(gc *gin.Context) { 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) + app.err.Printf(lm.FailedConstructDeletionMessage, "?", err) sendMail = false } } @@ -731,7 +724,7 @@ func (app *appContext) DeleteUsers(gc *gin.Context) { if id, ok := ombiUser["id"]; ok { status, err := app.ombi.DeleteUser(id.(string)) if err != nil || status != 200 { - app.err.Printf("Failed to delete ombi user (%d): %v", status, err) + app.err.Printf(lm.FailedDeleteUser, lm.Ombi, userID, err) errors[userID] = fmt.Sprintf("Ombi: %d %v, ", status, err) } } @@ -765,14 +758,14 @@ func (app *appContext) DeleteUsers(gc *gin.Context) { if sendMail && req.Notify { if err := app.sendByID(msg, userID); err != nil { - app.err.Printf("Failed to send account deletion email: %v", err) + app.err.Printf(lm.FailedSendDeletionMessage, userID, "?", err) } } } app.jf.CacheExpiry = time.Now() if len(errors) == len(req.Users) { respondBool(500, false, gc) - app.err.Printf("Account deletion failed: %s", errors[req.Users[0]]) + app.err.Printf(lm.FailedDeleteUsers, lm.Jellyfin, errors[req.Users[0]]) return } else if len(errors) != 0 { gc.JSON(500, errors) @@ -801,10 +794,8 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) { base := time.Now() if expiry, ok := app.storage.GetUserExpiryKey(id); ok { base = expiry.Expiry - app.debug.Printf("Expiry extended for \"%s\"", id) - } else { - app.debug.Printf("Created expiry for \"%s\"", id) } + app.debug.Printf(lm.ExtendCreateExpiry, id) expiry := UserExpiry{} if req.Timestamp != 0 { expiry.Expiry = time.Unix(req.Timestamp, 0) @@ -820,11 +811,11 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) { } msg, err := app.email.constructExpiryAdjusted(user.Name, exp, req.Reason, app, false) if err != nil { - app.err.Printf("%s: Failed to construct expiry adjustment notification: %v", uid, err) + app.err.Printf(lm.FailedConstructExpiryAdjustmentMessage, uid, err) return } if err := app.sendByID(msg, uid); err != nil { - app.err.Printf("%s: Failed to send expiry adjustment notification: %v", uid, err) + app.err.Printf(lm.FailedSendExpiryAdjustmentMessage, uid, "?", err) } }(id, expiry.Expiry) } @@ -867,17 +858,17 @@ func (app *appContext) EnableReferralForUsers(gc *gin.Context) { profile, ok := app.storage.GetProfileKey(source) err := app.storage.db.Get(profile.ReferralTemplateKey, &baseInv) if !ok || profile.ReferralTemplateKey == "" || err != nil { - app.debug.Printf("Couldn't find template to source from") + app.debug.Printf(lm.FailedGetReferralTemplate, profile.ReferralTemplateKey, err) respondBool(400, false, gc) return } - app.debug.Printf("Found referral template in profile: %+v\n", profile.ReferralTemplateKey) + app.debug.Printf(lm.GetReferralTemplate, profile.ReferralTemplateKey) } else if mode == "invite" { // Get the invite, and modify it to turn it into a referral err := app.storage.db.Get(source, &baseInv) if err != nil { - app.debug.Printf("Couldn't find invite to source from") + app.debug.Printf(lm.InvalidInviteCode, source) respondBool(400, false, gc) return } @@ -949,33 +940,34 @@ func (app *appContext) Announce(gc *gin.Context) { for _, userID := range req.Users { user, status, err := app.jf.UserByID(userID, false) if status != 200 || err != nil { - app.err.Printf("Failed to get user with ID \"%s\" (%d): %v", userID, status, err) + app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err) continue } msg, err := app.email.constructTemplate(req.Subject, req.Message, app, user.Name) if err != nil { - app.err.Printf("Failed to construct announcement message: %v", err) + app.err.Printf(lm.FailedConstructAnnouncementMessage, userID, err) respondBool(500, false, gc) return } else if err := app.sendByID(msg, userID); err != nil { - app.err.Printf("Failed to send announcement message: %v", err) + app.err.Printf(lm.FailedSendAnnouncementMessage, userID, "?", err) respondBool(500, false, gc) return } } + app.info.Printf(lm.SentAnnouncementMessage, userID, "?") } else { msg, err := app.email.constructTemplate(req.Subject, req.Message, app) if err != nil { - app.err.Printf("Failed to construct announcement messages: %v", err) + app.err.Printf(lm.FailedConstructAnnouncementMessage, "*", err) respondBool(500, false, gc) return } else if err := app.sendByID(msg, req.Users...); err != nil { - app.err.Printf("Failed to send announcement messages: %v", err) + app.err.Printf(lm.FailedSendAnnouncementMessage, "*", "?", err) respondBool(500, false, gc) return } + app.info.Printf(lm.SentAnnouncementMessage, "*", "?") } - app.info.Println("Sent announcement messages") respondBool(200, true, gc) } @@ -1063,7 +1055,6 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) { var req AdminPasswordResetDTO gc.BindJSON(&req) if req.Users == nil || len(req.Users) == 0 { - app.debug.Println("Ignoring empty request for PWR") respondBool(400, false, gc) return } @@ -1074,7 +1065,7 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) { for _, id := range req.Users { pwr, err = app.GenInternalReset(id) if err != nil { - app.err.Printf("Failed to get user from Jellyfin: %v", err) + app.err.Printf(lm.FailedGetUser, id, lm.Jellyfin, err) respondBool(500, false, gc) return } @@ -1100,13 +1091,13 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) { }, app, false, ) if err != nil { - app.err.Printf("Failed to construct password reset message for \"%s\": %v", pwr.Username, err) + app.err.Printf(lm.FailedConstructPWRMessage, id, err) respondBool(500, false, gc) return } else if err := app.sendByID(msg, id); err != nil { - app.err.Printf("Failed to send password reset message to \"%s\": %v", sendAddress, err) + app.err.Printf(lm.FailedSendPWRMessage, id, sendAddress, err) } else { - app.info.Printf("Sent password reset message to \"%s\"", sendAddress) + app.info.Printf(lm.SentPWRMessage, id, sendAddress) } } } @@ -1125,12 +1116,11 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) { // @Security Bearer // @tags Users func (app *appContext) GetUsers(gc *gin.Context) { - app.debug.Println("Users requested") var resp getUsersDTO users, status, err := app.jf.GetUsers(false) resp.UserList = make([]respUser, len(users)) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) + app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) respond(500, "Couldn't get users", gc) return } @@ -1202,10 +1192,9 @@ func (app *appContext) GetUsers(gc *gin.Context) { func (app *appContext) SetAccountsAdmin(gc *gin.Context) { var req setAccountsAdminDTO gc.BindJSON(&req) - app.debug.Println("Admin modification requested") users, status, err := app.jf.GetUsers(false) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) + app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) respond(500, "Couldn't get users", gc) return } @@ -1218,9 +1207,9 @@ func (app *appContext) SetAccountsAdmin(gc *gin.Context) { } emailStore.Admin = admin app.storage.SetEmailsKey(id, emailStore) + app.info.Printf(lm.UserAdminAdjusted, id, admin) } } - app.info.Println("Email list modified") respondBool(204, true, gc) } @@ -1235,10 +1224,9 @@ func (app *appContext) SetAccountsAdmin(gc *gin.Context) { func (app *appContext) ModifyLabels(gc *gin.Context) { var req modifyEmailsDTO gc.BindJSON(&req) - app.debug.Println("Label modification requested") users, status, err := app.jf.GetUsers(false) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) + app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) respond(500, "Couldn't get users", gc) return } @@ -1250,10 +1238,10 @@ func (app *appContext) ModifyLabels(gc *gin.Context) { emailStore = oldEmail } emailStore.Label = label + app.debug.Println(lm.UserLabelAdjusted, id, label) app.storage.SetEmailsKey(id, emailStore) } } - app.info.Println("Email list modified") respondBool(204, true, gc) } @@ -1275,21 +1263,21 @@ func (app *appContext) modifyEmail(jfID string, addr string) { ombiUser["emailAddress"] = addr code, err = app.ombi.ModifyUser(ombiUser) if code != 200 || err != nil { - app.err.Printf("%s: Failed to change ombi email address (%d): %v", ombiUser["userName"].(string), code, err) + app.err.Printf(lm.FailedSetEmailAddress, lm.Ombi, jfID, err) } } } if app.config.Section("jellyseerr").Key("enabled").MustBool(false) { err := app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: addr}) if err != nil { - app.err.Printf("Failed to set Jellyseerr email address: %v\n", err) + app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err) } else if contactPrefChanged { contactMethods := map[jellyseerr.NotificationsField]any{ jellyseerr.FieldEmailEnabled: true, } err := app.js.ModifyNotifications(jfID, contactMethods) if err != nil { - app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err) } } } @@ -1306,10 +1294,9 @@ func (app *appContext) modifyEmail(jfID string, addr string) { func (app *appContext) ModifyEmails(gc *gin.Context) { var req modifyEmailsDTO gc.BindJSON(&req) - app.debug.Println("Email modification requested") users, status, err := app.jf.GetUsers(false) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) + app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err) respond(500, "Couldn't get users", gc) return } @@ -1318,6 +1305,8 @@ func (app *appContext) ModifyEmails(gc *gin.Context) { if address, ok := req[id]; ok { app.modifyEmail(id, address) + app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId")) + activityType := ActivityContactLinked if address == "" { activityType = ActivityContactUnlinked @@ -1332,7 +1321,6 @@ func (app *appContext) ModifyEmails(gc *gin.Context) { }, gc, false) } } - app.info.Println("Email list modified") respondBool(200, true, gc) } @@ -1348,7 +1336,8 @@ func (app *appContext) ApplySettings(gc *gin.Context) { app.info.Println("User settings change requested") var req userSettingsDTO gc.BindJSON(&req) - applyingFrom := "profile" + applyingFromType := lm.Profile + applyingFromSource := "?" var policy mediabrowser.Policy var configuration mediabrowser.Configuration var displayprefs map[string]interface{} @@ -1359,18 +1348,21 @@ func (app *appContext) ApplySettings(gc *gin.Context) { // Check profile exists & isn't empty profile, ok := app.storage.GetProfileKey(req.Profile) if !ok { - app.err.Printf("Couldn't find profile \"%s\" or profile was empty", req.Profile) + app.err.Printf(lm.FailedGetProfile, req.Profile) respond(500, "Couldn't find profile", gc) return } + applyingFromSource = req.Profile if req.Homescreen { - if !profile.Homescreen { - app.err.Printf("No homescreen saved in profile \"%s\"", req.Profile) + if profile.Homescreen { + configuration = profile.Configuration + displayprefs = profile.Displayprefs + } else { + req.Homescreen = false + app.err.Printf(lm.ProfileNoHomescreen, req.Profile) respond(500, "No homescreen template available", gc) return } - configuration = profile.Configuration - displayprefs = profile.Displayprefs } if req.Policy { policy = profile.Policy @@ -1387,29 +1379,29 @@ func (app *appContext) ApplySettings(gc *gin.Context) { } } else if req.From == "user" { - applyingFrom = "user" + applyingFromType = lm.User app.jf.CacheExpiry = time.Now() user, status, err := app.jf.UserByID(req.ID, false) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get user from Jellyfin (%d): %v", status, err) + app.err.Printf(lm.FailedGetUser, req.ID, lm.Jellyfin, err) respond(500, "Couldn't get user", gc) return } - applyingFrom = "\"" + user.Name + "\"" + applyingFromSource = user.Name if req.Policy { policy = user.Policy } if req.Homescreen { displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID) if !(status == 200 || status == 204) || err != nil { - app.err.Printf("Failed to get DisplayPrefs (%d): %v", status, err) + app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err) respond(500, "Couldn't get displayprefs", gc) return } configuration = user.Configuration } } - app.info.Printf("Applying settings to %d user(s) from %s", len(req.ApplyTo), applyingFrom) + app.info.Printf(lm.ApplyingTemplatesFrom, applyingFromType, applyingFromSource, len(req.ApplyTo)) errors := errorListDTO{ "policy": map[string]string{}, "homescreen": map[string]string{}, @@ -1420,9 +1412,10 @@ func (app *appContext) ApplySettings(gc *gin.Context) { and can crash and mess up its database. Issue #160 says this occurs when more than 100 users are modified. A delay totalling 500ms between requests is used if so. */ - var shouldDelay bool = len(req.ApplyTo) >= 100 + const requestDelayThreshold = 100 + var shouldDelay bool = len(req.ApplyTo) >= requestDelayThreshold if shouldDelay { - app.debug.Println("Adding delay between requests for large batch") + app.debug.Printf(lm.DelayingRequests, requestDelayThreshold) } for _, id := range req.ApplyTo { var status int diff --git a/logmessages/logmessages.go b/logmessages/logmessages.go index cd7e9bd..70764a2 100644 --- a/logmessages/logmessages.go +++ b/logmessages/logmessages.go @@ -1,9 +1,19 @@ package logmessages const ( + Jellyseerr = "Jellyseerr" + Jellyfin = "Jellyfin" + Ombi = "Ombi" + Discord = "Discord" + Telegram = "Telegram" + Matrix = "Matrix" + Email = "Email" + + // main.go FailedLogging = "Failed to start log wrapper: %v\n" NoConfig = "Couldn't find default config file" + Write = "Wrote to \"%s\"" FailedWriting = "Failed to write to \"%s\": %v" FailedReading = "Failed to read from \"%s\": %v" FailedOpen = "Failed to open \"%s\": %v" @@ -24,15 +34,15 @@ const ( UsingTLS = "Using TLS/HTTP2" - UsingOmbi = "Starting Ombi client" - UsingJellyseerr = "Starting Jellyseerr client" + UsingOmbi = "Starting " + " + Ombi + " + " client" + UsingJellyseerr = "Starting " + Jellyseerr + " client" UsingEmby = "Using Emby server type (EXPERIMENTAL: PWRs are not available, and support is limited.)" - UsingJellyfin = "Using Jellyfin server type" - UsingJellyfinAuth = "Using Jellyfin for authentication" + UsingJellyfin = "Using " + Jellyfin + " server type" + UsingJellyfinAuth = "Using " + Jellyfin + " for authentication" UsingLocalAuth = "Using local username/pw authentication (NOT RECOMMENDED)" - AuthJellyfin = "Authenticated with Jellyfin @ \"%s\"" - FailedAuthJellyfin = "Failed to authenticate with Jellyfin @ \"%s\" (code %d): %v" + AuthJellyfin = "Authenticated with " + Jellyfin + " @ \"%s\"" + FailedAuthJellyfin = "Failed to authenticate with " + Jellyfin + " @ \"%s\" (code %d): %v" InitDiscord = "Initialized Discord daemon" FailedInitDiscord = "Failed to initialize Discord daemon: %v" @@ -59,4 +69,147 @@ const ( Quitting = "Shutting down..." Quit = "Server shut down." FailedQuit = "Server shutdown failed: %v" + + // api-activities.go + FailedDBReadActivities = "Failed to read activities from DB: %v" + + // api-backups.go + IgnoreInvalidFilename = "Invalid filename \"%s\", ignoring: %v" + GetUpload = "Retrieved uploaded file \"%s\"" + FailedGetUpload = "Failed to retrieve file from form data: %v" + + // api-invites.go + DeleteOldInvite = "Deleting old invite \"%s\"" + DeleteInvite = "Deleting invite \"%s\"" + FailedDeleteInvite = "Failed to delete invite \"%s\": %v" + GenerateInvite = "Generating new invite" + InvalidInviteCode = "Invalid invite code \"%s\"" + + FailedSendToTooltipNoUser = "Failed: \"%s\" not found" + FailedSendToTooltipMultiUser = "Failed: \"%s\" linked to multiple users" + + FailedParseTime = "Failed to parse time value: %v" + + FailedGetContactMethod = "Failed to get contact method for \"%s\", make sure one is set." + + SetAdminNotify = "Set \"%s\" to %t for admin address \"%s\"" + + // api-jellyseerr.go + FailedGetUsers = "Failed to get user(s) from %s: %v" + // FIXME: Once done, look back at uses of FailedGetUsers for places where this would make more sense. + FailedGetUser = "Failed to get user \"%s\" from %s: %v" + FailedGetJellyseerrNotificationPrefs = "Failed to get user's notification prefs from " + Jellyseerr + ": %v" + FailedSyncContactMethods = "Failed to sync contact methods with %s: %v" + + // api-messages.go + FailedGetCustomMessage = "Failed to get custom message \"%s\"" + SetContactPrefForService = "Set contact preference for %s (\"%s\"): %t" + + // Matrix + InvalidPIN = "Invalid PIN \"%s\"" + UnauthorizedPIN = "Unauthorized PIN \"%s\"" + FailedCreateRoom = "Failed to create room: %v" + FailedGenerateToken = "Failed to generate token: %v" + + // api-profiles.go + SetDefaultProfile = "Setting default profile to \"%s\"" + + FailedApplyProfile = "Failed to apply profile for %s user \"%s\": %v" + ApplyProfile = "Applying settings from profile \"%s\"" + FailedGetProfile = "Failed to find profile \"%s\"" + FailedApplyTemplate = "Failed to apply %s template for %s user \"%s\": %v" + FallbackToDefault = ", using default" + CreateProfileFromUser = "Creating profile from user \"%s\"" + FailedGetJellyfinDisplayPrefs = "Failed to get DisplayPreferences for user \"%s\" from " + Jellyfin + ": %v" + ProfileNoHomescreen = "No homescreen template in profile \"%s\"" + Profile = "profile" + User = "user" + ApplyingTemplatesFrom = "Applying templates from %s: \"%s\" to %d users" + DelayingRequests = "Delay will be added between requests (count = %d)" + + // api-userpage.go + EmailConfirmationRequired = "User \"%s\" requires email confirmation" + + ChangePassword = "Changed password for %s user \"%s\"" + FailedChangePassword = "Failed to change password for %s user \"%s\": %v" + + GetReferralTemplate = "Found referral template \"%s\"" + FailedGetReferralTemplate = "Failed to find referral template \"%s\": %v" + DeleteOldReferral = "Deleting old referral \"%s\"" + RenewOldReferral = "Renewing old referral \"%s\"" + + // api-users.go + CreateUser = "Created %s user \"%s\"" + FailedCreateUser = "Failed to create new %s user \"%s\": %v" + LinkUser = "Linked %s user \"%s\"" + FailedLinkUser = "Failed to link %s user \"%s\" with \"%s\": %v" + DeleteUser = "Deleted %s user \"%s\"" + FailedDeleteUser = "Failed to delete %s user \"%s\": %v" + FailedDeleteUsers = "Failed to delete %s user(s): %v" + UserExists = "user already exists" + AccountLinked = "account already linked and require_unique enabled" + AccountUnverified = "unverified" + FailedSetDiscordMemberRole = "Failed to set " + Discord + " member role: %v" + + FailedSetEmailAddress = "Failed to set email address for %s user \"%s\": %v" + + AdditionalOmbiErrors = "Additional errors from " + Ombi + ": %v" + + IncorrectCaptcha = "captcha incorrect" + + ExtendCreateExpiry = "Extended or created expiry for user \"%s\"" + + UserEmailAdjusted = "Email for user \"%s\" adjusted" + UserAdminAdjusted = "Admin state for user \"%s\" set to %t" + UserLabelAdjusted = "Label for user \"%s\" set to \"%s\"" +) + +const ( + FailedGetCookies = "Failed to get cookie(s) \"%s\": %v" + FailedParseJWT = "Failed to parse JWT: %v" + FailedCastJWT = "JWT claims unreadable" + InvalidJWT = "JWT was invalidated, of incorrect type or has expired" + FailedSignJWT = "Failed to sign JWT: %v" +) + +const ( + FailedConstructExpiryAdmin = "Failed to construct expiry notification for \"%s\": %v" + FailedSendExpiryAdmin = "Failed to send expiry notification for \"%s\" to \"%s\": %v" + SentExpiryAdmin = "Sent expiry notification for \"%s\" to \"%s\"" + + FailedConstructCreationAdmin = "Failed to construct creation notification for \"%s\": %v" + FailedSendCreationAdmin = "Failed to send creation notification for \"%s\" to \"%s\": %v" + SentCreationAdmin = "Sent creation notification for \"%s\" to \"%s\"" + + FailedConstructInviteMessage = "Failed to construct invite message for \"%s\": %v" + FailedSendInviteMessage = "Failed to send invite message for \"%s\" to \"%s\": %v" + SentInviteMessage = "Sent invite message for \"%s\" to \"%s\"" + + FailedConstructConfirmationEmail = "Failed to construct confirmation email for \"%s\": %v" + FailedSendConfirmationEmail = "Failed to send confirmation email for \"%s\" to \"%s\": %v" + SentConfirmationEmail = "Sent confirmation email for \"%s\" to \"%s\"" + + FailedConstructPWRMessage = "Failed to construct PWR message for \"%s\": %v" + FailedSendPWRMessage = "Failed to send PWR message for \"%s\" to \"%s\": %v" + SentPWRMessage = "Sent PWR message for \"%s\" to \"%s\"" + + FailedConstructWelcomeMessage = "Failed to construct welcome message for \"%s\": %v" + FailedSendWelcomeMessage = "Failed to send welcome message for \"%s\" to \"%s\": %v" + SentWelcomeMessage = "Sent welcome message for \"%s\" to \"%s\"" + + FailedConstructEnableDisableMessage = "Failed to construct enable/disable message for \"%s\": %v" + FailedSendEnableDisableMessage = "Failed to send enable/disable message for \"%s\" to \"%s\": %v" + SentEnableDisableMessage = "Sent enable/disable message for \"%s\" to \"%s\"" + + FailedConstructDeletionMessage = "Failed to construct account deletion message for \"%s\": %v" + FailedSendDeletionMessage = "Failed to send account deletion message for \"%s\" to \"%s\": %v" + SentDeletionMessage = "Sent account deletion message for \"%s\" to \"%s\"" + + FailedConstructExpiryAdjustmentMessage = "Failed to construct expiry adjustment message for \"%s\": %v" + FailedSendExpiryAdjustmentMessage = "Failed to send expiry adjustment message for \"%s\" to \"%s\": %v" + SentExpiryAdjustmentMessage = "Sent expiry adjustment message for \"%s\" to \"%s\"" + + FailedConstructAnnouncementMessage = "Failed to construct announcement message for \"%s\": %v" + FailedSendAnnouncementMessage = "Failed to send announcement message for \"%s\" to \"%s\": %v" + SentAnnouncementMessage = "Sent announcement message for \"%s\" to \"%s\"" )