From a97bccc88f4b6ac96c6e645c045131ae790333a8 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Tue, 30 Jul 2024 16:36:59 +0100 Subject: [PATCH] jellyseerr: use in profiles, apply on user creation and modification added in the same way as ombi profiles. Most code is copy-pasted and adjusted from ombi (especially on web), so maybe this can be merged in the future. Also, profile names are url-escaped like announcement template names were not too long ago. API client has "LogRequestBodies" option which just dumps the request body when enabled (useful for recreating reqs in the jellyseerr swagger UI). User.Name() helper returns a name from all three possible values in the struct. --- api-jellyseerr.go | 100 +++++++++++++++++++++++++++++++++ api-ombi.go | 7 ++- api-profiles.go | 1 + api-users.go | 102 ++++++++++++++++++++++++++++++++++ config/config-base.json | 35 ++++++++++++ go.mod | 5 +- html/admin.html | 17 ++++++ jellyseerr/jellyseerr.go | 61 ++++++++++++++------ jellyseerr/models.go | 117 ++++++++++++++++++++++----------------- lang/admin/en-us.json | 4 ++ main.go | 13 +++++ models.go | 2 + router.go | 5 ++ storage.go | 8 +++ ts/admin.ts | 3 + ts/form.ts | 8 ++- ts/modules/profiles.ts | 109 ++++++++++++++++++++++++++++++++++-- ts/typings/d.ts | 2 + views.go | 61 ++++++++++---------- 19 files changed, 555 insertions(+), 105 deletions(-) create mode 100644 api-jellyseerr.go 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 @@ +