diff --git a/api-jellyseerr.go b/api-jellyseerr.go new file mode 100644 index 0000000..5ced607 --- /dev/null +++ b/api-jellyseerr.go @@ -0,0 +1,100 @@ +package main + +import ( + "net/url" + "strconv" + + "github.com/gin-gonic/gin" +) + +// @Summary Get a list of Jellyseerr users. +// @Produce json +// @Success 200 {object} ombiUsersDTO +// @Failure 500 {object} stringResponse +// @Router /jellyseerr/users [get] +// @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) + 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 { + userlist[i] = ombiUser{ + Name: u.Name(), + ID: strconv.FormatInt(u.ID, 10), + } + i++ + } + gc.JSON(200, ombiUsersDTO{Users: userlist}) +} + +// @Summary Store Jellyseerr user template in an existing profile. +// @Produce json +// @Param id path string true "Jellyseerr ID of user to source from" +// @Param profile path string true "Name of profile to store in" +// @Success 200 {object} boolResponse +// @Failure 400 {object} boolResponse +// @Failure 500 {object} stringResponse +// @Router /profiles/jellyseerr/{profile}/{id} [post] +// @Security Bearer +// @tags Jellyseerr +func (app *appContext) SetJellyseerrProfile(gc *gin.Context) { + jellyseerrID, err := strconv.ParseInt(gc.Param("id"), 10, 64) + if err != nil { + respondBool(400, false, gc) + return + } + escapedProfileName := gc.Param("profile") + profileName, _ := url.QueryUnescape(escapedProfileName) + profile, ok := app.storage.GetProfileKey(profileName) + if !ok { + respondBool(400, false, gc) + return + } + u, err := app.js.UserByID(jellyseerrID) + if err != nil { + app.err.Printf("Couldn't get user from Jellyseerr: %v", 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) + respond(500, "Couldn't get user notification prefs", gc) + return + } + profile.Jellyseerr.Notifications = n.NotificationsTemplate + profile.Jellyseerr.Enabled = true + app.storage.SetProfileKey(profileName, profile) + respondBool(204, true, gc) +} + +// @Summary Remove jellyseerr user template from a profile. +// @Produce json +// @Param profile path string true "Name of profile to store in" +// @Success 200 {object} boolResponse +// @Failure 400 {object} boolResponse +// @Failure 500 {object} stringResponse +// @Router /profiles/jellyseerr/{profile} [delete] +// @Security Bearer +// @tags Jellyseerr +func (app *appContext) DeleteJellyseerrProfile(gc *gin.Context) { + escapedProfileName := gc.Param("profile") + profileName, _ := url.QueryUnescape(escapedProfileName) + profile, ok := app.storage.GetProfileKey(profileName) + if !ok { + respondBool(400, false, gc) + return + } + profile.Jellyseerr.Enabled = false + app.storage.SetProfileKey(profileName, profile) + respondBool(204, true, gc) +} 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-ombi.go b/api-ombi.go index ca148aa..ab9b534 100644 --- a/api-ombi.go +++ b/api-ombi.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "net/url" "github.com/gin-gonic/gin" "github.com/hrfee/mediabrowser" @@ -95,7 +96,8 @@ func (app *appContext) OmbiUsers(gc *gin.Context) { func (app *appContext) SetOmbiProfile(gc *gin.Context) { var req ombiUser gc.BindJSON(&req) - profileName := gc.Param("profile") + escapedProfileName := gc.Param("profile") + profileName, _ := url.QueryUnescape(escapedProfileName) profile, ok := app.storage.GetProfileKey(profileName) if !ok { respondBool(400, false, gc) @@ -122,7 +124,8 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) { // @Security Bearer // @tags Ombi func (app *appContext) DeleteOmbiProfile(gc *gin.Context) { - profileName := gc.Param("profile") + escapedProfileName := gc.Param("profile") + profileName, _ := url.QueryUnescape(escapedProfileName) profile, ok := app.storage.GetProfileKey(profileName) if !ok { respondBool(400, false, gc) diff --git a/api-profiles.go b/api-profiles.go index ccbac7d..482abbb 100644 --- a/api-profiles.go +++ b/api-profiles.go @@ -27,6 +27,7 @@ func (app *appContext) GetProfiles(gc *gin.Context) { LibraryAccess: p.LibraryAccess, FromUser: p.FromUser, Ombi: p.Ombi != nil, + Jellyseerr: p.Jellyseerr.Enabled, ReferralsEnabled: false, } if referralsEnabled { 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 64d1997..ec41a94 100644 --- a/api-users.go +++ b/api-users.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" + "github.com/hrfee/jfa-go/jellyseerr" "github.com/hrfee/mediabrowser" "github.com/lithammer/shortuuid/v3" "github.com/timshannon/badgerhold/v4" @@ -94,6 +95,29 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { app.info.Println("Created Ombi user") } } + 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) + } 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) + } + 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) + } + } + } 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) @@ -338,6 +362,10 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) 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 @@ -468,6 +496,51 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) 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) @@ -1184,6 +1257,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" @@ -1202,22 +1313,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 == "" { @@ -1231,17 +1330,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") @@ -1265,6 +1353,8 @@ func (app *appContext) ApplySettings(gc *gin.Context) { 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) @@ -1282,12 +1372,19 @@ func (app *appContext) ApplySettings(gc *gin.Context) { configuration = profile.Configuration displayprefs = profile.Displayprefs } - policy = profile.Policy - if app.config.Section("ombi").Key("enabled").MustBool(false) { + 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" @@ -1299,7 +1396,9 @@ func (app *appContext) ApplySettings(gc *gin.Context) { return } applyingFrom = "\"" + user.Name + "\"" - policy = user.Policy + if req.Policy { + policy = user.Policy + } if req.Homescreen { displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID) if !(status == 200 || status == 204) || err != nil { @@ -1315,6 +1414,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) { "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 @@ -1325,9 +1425,13 @@ func (app *appContext) ApplySettings(gc *gin.Context) { app.debug.Println("Adding delay between requests for large batch") } for _, id := range req.ApplyTo { - status, err := app.jf.SetPolicy(id, policy) - if !(status == 200 || status == 204) || err != nil { - errors["policy"][id] = fmt.Sprintf("%d: %s", status, err) + 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) @@ -1367,6 +1471,26 @@ func (app *appContext) ApplySettings(gc *gin.Context) { 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) } diff --git a/backups.go b/backups.go index 95e2fad..676bbcb 100644 --- a/backups.go +++ b/backups.go @@ -161,20 +161,13 @@ func (app *appContext) loadPendingBackup() { LOADBAK = "" } -func newBackupDaemon(app *appContext) *housekeepingDaemon { +func newBackupDaemon(app *appContext) *GenericDaemon { interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute - daemon := housekeepingDaemon{ - Stopped: false, - ShutdownChannel: make(chan string), - Interval: interval, - period: interval, - app: app, - } - daemon.jobs = []func(app *appContext){ + d := NewGenericDaemon(interval, app, func(app *appContext) { app.debug.Println("Backups: Creating backup") app.makeBackup() }, - } - return &daemon + ) + return d } diff --git a/config/config-base.json b/config/config-base.json index 1b3bdd2..1d12d88 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -1580,6 +1580,66 @@ } } }, + "jellyseerr": { + "order": [], + "meta": { + "name": "Jellyseerr Integration", + "description": "Connect to Jellyseerr to automatically trigger the import of users on account creation, and to automatically link contact methods (email, discord and telegram). A template must be added to a User Profile for accounts to be created." + }, + "settings": { + "enabled": { + "name": "Enabled", + "required": false, + "requires_restart": true, + "type": "bool", + "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, + "requires_restart": true, + "type": "text", + "value": "localhost:5000", + "depends_true": "enabled", + "description": "Jellyseerr server URL." + }, + "api_key": { + "name": "API Key", + "required": false, + "requires_restart": true, + "type": "text", + "value": "", + "depends_true": "enabled", + "description": "API Key. Get this from the first tab in Jellyseerr's settings." + }, + "import_existing": { + "name": "Import existing users to Jellyseerr", + "required": false, + "requires_restart": true, + "type": "bool", + "value": false, + "depends_true": "enabled", + "description": "Existing users (and those created outside jfa-go) will have their contact info imported to Jellyseerr." + }, + "constraints_note": { + "name": "Unique Emails:", + "type": "note", + "value": "", + "depends_true": "import_existing", + "required": "false", + "description": "Jellyseerr requires email addresses to be unique. If this is not the case, you may see errors in jfa-go's logs. You can require unique addresses in Settings > Email." + } + } + }, "backups": { "order": [], "meta": { diff --git a/daemon.go b/daemon.go index 19a3f97..e7c8efe 100644 --- a/daemon.go +++ b/daemon.go @@ -116,32 +116,16 @@ func (app *appContext) clearActivities() { } } -// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS - -type housekeepingDaemon struct { - Stopped bool - ShutdownChannel chan string - Interval time.Duration - period time.Duration - jobs []func(app *appContext) - app *appContext -} - -func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemon { - daemon := housekeepingDaemon{ - Stopped: false, - ShutdownChannel: make(chan string), - Interval: interval, - period: interval, - app: app, - } - daemon.jobs = []func(app *appContext){ +func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon { + d := NewGenericDaemon(interval, app, func(app *appContext) { app.debug.Println("Housekeeping: Checking for expired invites") app.checkInvites() }, func(app *appContext) { app.clearActivities() }, - } + ) + + d.Name("Housekeeping daemon") clearEmail := app.config.Section("email").Key("require_unique").MustBool(false) clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false) @@ -150,53 +134,24 @@ func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemo clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false) if clearEmail || clearDiscord || clearTelegram || clearMatrix { - daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() }) + d.appendJobs(func(app *appContext) { app.jf.CacheExpiry = time.Now() }) } if clearEmail { - daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearEmails() }) + d.appendJobs(func(app *appContext) { app.clearEmails() }) } if clearDiscord { - daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearDiscord() }) + d.appendJobs(func(app *appContext) { app.clearDiscord() }) } if clearTelegram { - daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearTelegram() }) + d.appendJobs(func(app *appContext) { app.clearTelegram() }) } if clearMatrix { - daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() }) + d.appendJobs(func(app *appContext) { app.clearMatrix() }) } if clearPWR { - daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearPWRCaptchas() }) + d.appendJobs(func(app *appContext) { app.clearPWRCaptchas() }) } - return &daemon -} - -func (rt *housekeepingDaemon) run() { - rt.app.info.Println("Invite daemon started") - for { - select { - case <-rt.ShutdownChannel: - rt.ShutdownChannel <- "Down" - return - case <-time.After(rt.period): - break - } - started := time.Now() - - for _, job := range rt.jobs { - job(rt.app) - } - - finished := time.Now() - duration := finished.Sub(started) - rt.period = rt.Interval - duration - } -} - -func (rt *housekeepingDaemon) Shutdown() { - rt.Stopped = true - rt.ShutdownChannel <- "Down" - <-rt.ShutdownChannel - close(rt.ShutdownChannel) + return d } diff --git a/genericdaemon.go b/genericdaemon.go new file mode 100644 index 0000000..f16dd68 --- /dev/null +++ b/genericdaemon.go @@ -0,0 +1,65 @@ +package main + +import "time" + +// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS + +type GenericDaemon struct { + Stopped bool + ShutdownChannel chan string + Interval time.Duration + period time.Duration + jobs []func(app *appContext) + app *appContext + name string +} + +func (d *GenericDaemon) appendJobs(jobs ...func(app *appContext)) { + d.jobs = append(d.jobs, jobs...) +} + +// NewGenericDaemon returns a daemon which can be given jobs that utilize appContext. +func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app *appContext)) *GenericDaemon { + d := GenericDaemon{ + Stopped: false, + ShutdownChannel: make(chan string), + Interval: interval, + period: interval, + app: app, + name: "Generic Daemon", + } + d.jobs = jobs + return &d + +} + +func (d *GenericDaemon) Name(name string) { d.name = name } + +func (d *GenericDaemon) run() { + d.app.info.Printf("%s started", d.name) + for { + select { + case <-d.ShutdownChannel: + d.ShutdownChannel <- "Down" + return + case <-time.After(d.period): + break + } + started := time.Now() + + for _, job := range d.jobs { + job(d.app) + } + + finished := time.Now() + duration := finished.Sub(started) + d.period = d.Interval - duration + } +} + +func (d *GenericDaemon) Shutdown() { + d.Stopped = true + d.ShutdownChannel <- "Down" + <-d.ShutdownChannel + close(d.ShutdownChannel) +} diff --git a/go.mod b/go.mod index e9969af..de1d06d 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,8 @@ replace github.com/hrfee/jfa-go/api => ./api replace github.com/hrfee/jfa-go/easyproxy => ./easyproxy +replace github.com/hrfee/jfa-go/jellyseerr => ./jellyseerr + require ( github.com/bwmarrin/discordgo v0.27.1 github.com/dgraph-io/badger/v3 v3.2103.5 @@ -29,7 +31,7 @@ require ( github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/golang-jwt/jwt v3.2.2+incompatible github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a - github.com/hrfee/jfa-go/common v0.0.0-20230626224816-f72960635dc3 + github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769 github.com/hrfee/jfa-go/docs v0.0.0-20230626224816-f72960635dc3 github.com/hrfee/jfa-go/easyproxy v0.0.0-00010101000000-000000000000 github.com/hrfee/jfa-go/linecache v0.0.0-20230626224816-f72960635dc3 @@ -88,6 +90,7 @@ require ( github.com/google/uuid v1.3.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect + github.com/hrfee/jfa-go/jellyseerr v0.0.0-00010101000000-000000000000 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.6 // indirect diff --git a/html/admin.html b/html/admin.html index 1dc3ad9..cd72885 100644 --- a/html/admin.html +++ b/html/admin.html @@ -10,6 +10,7 @@ window.discordEnabled = {{ .discordEnabled }}; window.matrixEnabled = {{ .matrixEnabled }}; window.ombiEnabled = {{ .ombiEnabled }}; + window.jellyseerrEnabled = {{ .jellyseerrEnabled }}; window.usernameEnabled = {{ .username }}; window.langFile = JSON.parse({{ .language }}); window.linkResetEnabled = {{ .linkResetEnabled }}; @@ -83,30 +84,44 @@
×

{{ .strings.modifySettingsDescription }}

-
-
{{ if .referralsEnabled }} @@ -396,6 +411,19 @@ +