From 385953b0cbc3b9eed6759ce1f0dd6885c8ab70d0 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 30 Jul 2024 20:55:45 +0100 Subject: [PATCH] jellyseerr: fix email setting, cover all contact adjustment areas hopefully all places where contact methods can be adjusted should sync with jellyseerr. --- api-messages.go | 41 +++++++++++++++++++++++++ api-userpage.go | 49 ++++++++++++++++++------------ api-users.go | 65 ++++++++++++++++++++++++---------------- config/config-base.json | 8 +++++ jellyseerr/jellyseerr.go | 24 ++++++++++++++- jellyseerr/models.go | 15 ++++++++++ 6 files changed, 157 insertions(+), 45 deletions(-) diff --git a/api-messages.go b/api-messages.go index a78ab85..db26db0 100644 --- a/api-messages.go +++ b/api-messages.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/hrfee/jfa-go/jellyseerr" "github.com/lithammer/shortuuid/v3" "gopkg.in/ini.v1" ) @@ -322,6 +323,14 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) { tgUser.Lang = lang } app.storage.SetTelegramKey(req.ID, tgUser) + + if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{ + jellyseerr.FieldTelegram: tgUser.ChatID, + jellyseerr.FieldTelegramEnabled: tgUser.Contact, + }); err != nil { + app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + } + linkExistingOmbiDiscordTelegram(app) respondBool(200, true, gc) } @@ -346,6 +355,7 @@ func (app *appContext) SetContactMethods(gc *gin.Context) { } func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Context) { + jsPrefs := map[jellyseerr.NotificationsField]any{} if tgUser, ok := app.storage.GetTelegramKey(req.ID); ok { change := tgUser.Contact != req.Telegram tgUser.Contact = req.Telegram @@ -356,6 +366,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte msg = " not" } app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg) + jsPrefs[jellyseerr.FieldTelegramEnabled] = req.Telegram } } if dcUser, ok := app.storage.GetDiscordKey(req.ID); ok { @@ -368,6 +379,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte msg = " not" } app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg) + jsPrefs[jellyseerr.FieldDiscordEnabled] = req.Discord } } if mxUser, ok := app.storage.GetMatrixKey(req.ID); ok { @@ -392,6 +404,13 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte msg = " not" } app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg) + 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) } } respondBool(200, true, gc) @@ -678,6 +697,13 @@ func (app *appContext) DiscordConnect(gc *gin.Context) { app.storage.SetDiscordKey(req.JellyfinID, user) + if err := app.js.ModifyNotifications(req.JellyfinID, map[jellyseerr.NotificationsField]any{ + jellyseerr.FieldDiscord: req.DiscordID, + jellyseerr.FieldDiscordEnabled: true, + }); err != nil { + app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + } + app.storage.SetActivityKey(shortuuid.New(), Activity{ Type: ActivityContactLinked, UserID: req.JellyfinID, @@ -708,6 +734,14 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) { } */ app.storage.DeleteDiscordKey(req.ID) + // May not actually remove Discord ID, but should disable interaction. + if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{ + jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier, + jellyseerr.FieldDiscordEnabled: false, + }); err != nil { + app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + } + app.storage.SetActivityKey(shortuuid.New(), Activity{ Type: ActivityContactUnlinked, UserID: req.ID, @@ -737,6 +771,13 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) { } */ app.storage.DeleteTelegramKey(req.ID) + if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{ + jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier, + jellyseerr.FieldTelegramEnabled: false, + }); err != nil { + app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + } + app.storage.SetActivityKey(shortuuid.New(), Activity{ Type: ActivityContactUnlinked, UserID: req.ID, diff --git a/api-userpage.go b/api-userpage.go index ee7eb33..93d9490 100644 --- a/api-userpage.go +++ b/api-userpage.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" + "github.com/hrfee/jfa-go/jellyseerr" "github.com/lithammer/shortuuid/v3" "github.com/timshannon/badgerhold/v4" ) @@ -200,14 +201,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) { gc.Redirect(http.StatusSeeOther, "/my/account") return } else if target == UserEmailChange { - emailStore, ok := app.storage.GetEmailsKey(id) - if !ok { - emailStore = EmailAddress{ - Contact: true, - } - } - emailStore.Addr = claims["email"].(string) - app.storage.SetEmailsKey(id, emailStore) + app.modifyEmail(id, claims["email"].(string)) app.storage.SetActivityKey(shortuuid.New(), Activity{ Type: ActivityContactLinked, @@ -218,17 +212,6 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) { Time: time.Now(), }, gc, true) - if app.config.Section("ombi").Key("enabled").MustBool(false) { - ombiUser, code, err := app.getOmbiUser(id) - if code == 200 && err == nil { - ombiUser["emailAddress"] = claims["email"].(string) - 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.info.Println("Email list modified") gc.Redirect(http.StatusSeeOther, "/my/account") return @@ -371,6 +354,13 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) { } app.storage.SetDiscordKey(gc.GetString("jfId"), dcUser) + if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{ + jellyseerr.FieldDiscord: dcUser.ID, + jellyseerr.FieldDiscordEnabled: dcUser.Contact, + }); err != nil { + app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + } + app.storage.SetActivityKey(shortuuid.New(), Activity{ Type: ActivityContactLinked, UserID: gc.GetString("jfId"), @@ -419,6 +409,13 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) { } app.storage.SetTelegramKey(gc.GetString("jfId"), tgUser) + if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{ + jellyseerr.FieldTelegram: tgUser.ChatID, + jellyseerr.FieldTelegramEnabled: tgUser.Contact, + }); err != nil { + app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + } + app.storage.SetActivityKey(shortuuid.New(), Activity{ Type: ActivityContactLinked, UserID: gc.GetString("jfId"), @@ -522,6 +519,13 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) { func (app *appContext) UnlinkMyDiscord(gc *gin.Context) { app.storage.DeleteDiscordKey(gc.GetString("jfId")) + if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{ + jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier, + jellyseerr.FieldDiscordEnabled: false, + }); err != nil { + app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + } + app.storage.SetActivityKey(shortuuid.New(), Activity{ Type: ActivityContactUnlinked, UserID: gc.GetString("jfId"), @@ -543,6 +547,13 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) { func (app *appContext) UnlinkMyTelegram(gc *gin.Context) { app.storage.DeleteTelegramKey(gc.GetString("jfId")) + if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{ + jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier, + jellyseerr.FieldTelegramEnabled: false, + }); err != nil { + app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err) + } + app.storage.SetActivityKey(shortuuid.New(), Activity{ Type: ActivityContactUnlinked, UserID: gc.GetString("jfId"), diff --git a/api-users.go b/api-users.go index ddafe08..3a4891d 100644 --- a/api-users.go +++ b/api-users.go @@ -516,7 +516,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) } contactMethods := map[jellyseerr.NotificationsField]any{} if emailEnabled { - err = app.js.ModifyUser(id, map[jellyseerr.UserField]any{jellyseerr.FieldEmail: req.Email}) + 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 { @@ -1258,6 +1258,44 @@ func (app *appContext) ModifyLabels(gc *gin.Context) { 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" @@ -1276,22 +1314,10 @@ func (app *appContext) ModifyEmails(gc *gin.Context) { respond(500, "Couldn't get users", gc) return } - ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false) for _, jfUser := range users { id := jfUser.ID if address, ok := req[id]; ok { - var emailStore = EmailAddress{} - oldEmail, ok := app.storage.GetEmailsKey(id) - if ok { - emailStore = oldEmail - } - // Auto enable contact by email for newly added addresses - if !ok || oldEmail.Addr == "" { - emailStore.Contact = true - } - - emailStore.Addr = address - app.storage.SetEmailsKey(id, emailStore) + app.modifyEmail(id, address) activityType := ActivityContactLinked if address == "" { @@ -1305,17 +1331,6 @@ func (app *appContext) ModifyEmails(gc *gin.Context) { Value: "email", Time: time.Now(), }, gc, false) - - if ombiEnabled { - ombiUser, code, err := app.getOmbiUser(id) - if code == 200 && err == nil { - ombiUser["emailAddress"] = address - 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.info.Println("Email list modified") diff --git a/config/config-base.json b/config/config-base.json index e18224a..e5434ac 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -1595,6 +1595,14 @@ "value": false, "description": "Enable the Jellyseerr integration." }, + "usertype_note": { + "name": "Password Changes:", + "type": "note", + "value": "", + "depends_true": "enabled", + "required": "false", + "description": "Ensure existing users on Jellyseerr are \"Jellyfin User\"s not \"Local User\"s, as password changes are not synced with Jellyseerr." + }, "server": { "name": "URL", "required": false, diff --git a/jellyseerr/jellyseerr.go b/jellyseerr/jellyseerr.go index cddb86b..6161c5b 100644 --- a/jellyseerr/jellyseerr.go +++ b/jellyseerr/jellyseerr.go @@ -16,7 +16,8 @@ import ( ) const ( - API_SUFFIX = "/api/v1" + API_SUFFIX = "/api/v1" + BogusIdentifier = "123412341234123456" ) // Jellyseerr represents a running Jellyseerr instance. @@ -300,6 +301,9 @@ func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error } func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error { + if _, ok := conf[FieldEmail]; ok { + return fmt.Errorf("email is read only, set with ModifyMainUserSettings instead") + } u, err := js.MustGetUser(jfID) if err != nil { return err @@ -408,3 +412,21 @@ func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) { err = json.Unmarshal([]byte(resp), &data) return data, err } + +func (js *Jellyseerr) ModifyMainUserSettings(jfID string, conf MainUserSettings) error { + u, err := js.MustGetUser(jfID) + if err != nil { + return err + } + + _, status, err := js.put(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false) + if err != nil { + return err + } + if status != 200 && status != 201 { + return fmt.Errorf("failed (error %d)", status) + } + // Lazily just invalidate the cache. + js.cacheExpiry = time.Now() + return nil +} diff --git a/jellyseerr/models.go b/jellyseerr/models.go index 5de1c5b..c418439 100644 --- a/jellyseerr/models.go +++ b/jellyseerr/models.go @@ -115,3 +115,18 @@ type NotificationsTemplate struct { WebPushEnabled bool `json:"webPushEnabled,omitempty"` NotifTypes NotificationTypes `json:"notificationTypes"` } + +type MainUserSettings struct { + Username string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + DiscordID string `json:"discordId,omitempty"` + Locale string `json:"locale,omitempty"` + Region string `json:"region,omitempty"` + OriginalLanguage any `json:"originalLanguage,omitempty"` + MovieQuotaLimit any `json:"movieQuotaLimit,omitempty"` + MovieQuotaDays any `json:"movieQuotaDays,omitempty"` + TvQuotaLimit any `json:"tvQuotaLimit,omitempty"` + TvQuotaDays any `json:"tvQuotaDays,omitempty"` + WatchlistSyncMovies any `json:"watchlistSyncMovies,omitempty"` + WatchlistSyncTv any `json:"watchlistSyncTv,omitempty"` +}