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-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-users.go b/api-users.go index 64d1997..ddafe08 100644 --- a/api-users.go +++ b/api-users.go @@ -4,11 +4,13 @@ import ( "fmt" "net/url" "os" + "strconv" "strings" "time" "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 +96,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 +363,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 +497,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.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) + } 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] = strconv.FormatInt(u.ChatID, 10) + 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) @@ -1265,6 +1339,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) @@ -1288,6 +1364,11 @@ func (app *appContext) ApplySettings(gc *gin.Context) { ombi = profile.Ombi } } + if app.config.Section("jellyseerr").Key("enabled").MustBool(false) { + if profile.Jellyseerr.Enabled { + jellyseerr = profile.Jellyseerr + } + } } else if req.From == "user" { applyingFrom = "user" @@ -1315,6 +1396,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 @@ -1367,6 +1449,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/config/config-base.json b/config/config-base.json index 1b3bdd2..e18224a 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -1580,6 +1580,41 @@ } } }, + "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." + }, + "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." + } + } + }, "backups": { "order": [], "meta": { 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..f7de13e 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 }}; @@ -396,6 +397,19 @@ +