jellyseerr: fix email setting, cover all contact adjustment areas

hopefully all places where contact methods can be adjusted should sync
with jellyseerr.
jellyseerr
Harvey Tindall 4 months ago
parent 35f8337a36
commit 385953b0cb
No known key found for this signature in database
GPG Key ID: BBC65952848FB1A2

@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/jellyseerr"
"github.com/lithammer/shortuuid/v3" "github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
) )
@ -322,6 +323,14 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
tgUser.Lang = lang tgUser.Lang = lang
} }
app.storage.SetTelegramKey(req.ID, tgUser) 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) linkExistingOmbiDiscordTelegram(app)
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -346,6 +355,7 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
} }
func (app *appContext) setContactMethods(req SetContactMethodsDTO, 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 { if tgUser, ok := app.storage.GetTelegramKey(req.ID); ok {
change := tgUser.Contact != req.Telegram change := tgUser.Contact != req.Telegram
tgUser.Contact = req.Telegram tgUser.Contact = req.Telegram
@ -356,6 +366,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
msg = " not" msg = " not"
} }
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg) 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 { if dcUser, ok := app.storage.GetDiscordKey(req.ID); ok {
@ -368,6 +379,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
msg = " not" msg = " not"
} }
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg) 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 { if mxUser, ok := app.storage.GetMatrixKey(req.ID); ok {
@ -392,6 +404,13 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
msg = " not" msg = " not"
} }
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg) 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) respondBool(200, true, gc)
@ -678,6 +697,13 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
app.storage.SetDiscordKey(req.JellyfinID, user) 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{ app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked, Type: ActivityContactLinked,
UserID: req.JellyfinID, UserID: req.JellyfinID,
@ -708,6 +734,14 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
} */ } */
app.storage.DeleteDiscordKey(req.ID) 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{ app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactUnlinked, Type: ActivityContactUnlinked,
UserID: req.ID, UserID: req.ID,
@ -737,6 +771,13 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
} */ } */
app.storage.DeleteTelegramKey(req.ID) 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{ app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactUnlinked, Type: ActivityContactUnlinked,
UserID: req.ID, UserID: req.ID,

@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/hrfee/jfa-go/jellyseerr"
"github.com/lithammer/shortuuid/v3" "github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
) )
@ -200,14 +201,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
gc.Redirect(http.StatusSeeOther, "/my/account") gc.Redirect(http.StatusSeeOther, "/my/account")
return return
} else if target == UserEmailChange { } else if target == UserEmailChange {
emailStore, ok := app.storage.GetEmailsKey(id) app.modifyEmail(id, claims["email"].(string))
if !ok {
emailStore = EmailAddress{
Contact: true,
}
}
emailStore.Addr = claims["email"].(string)
app.storage.SetEmailsKey(id, emailStore)
app.storage.SetActivityKey(shortuuid.New(), Activity{ app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked, Type: ActivityContactLinked,
@ -218,17 +212,6 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
Time: time.Now(), Time: time.Now(),
}, gc, true) }, 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") app.info.Println("Email list modified")
gc.Redirect(http.StatusSeeOther, "/my/account") gc.Redirect(http.StatusSeeOther, "/my/account")
return return
@ -371,6 +354,13 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
} }
app.storage.SetDiscordKey(gc.GetString("jfId"), dcUser) 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{ app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked, Type: ActivityContactLinked,
UserID: gc.GetString("jfId"), UserID: gc.GetString("jfId"),
@ -419,6 +409,13 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
} }
app.storage.SetTelegramKey(gc.GetString("jfId"), tgUser) 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{ app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked, Type: ActivityContactLinked,
UserID: gc.GetString("jfId"), UserID: gc.GetString("jfId"),
@ -522,6 +519,13 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
func (app *appContext) UnlinkMyDiscord(gc *gin.Context) { func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
app.storage.DeleteDiscordKey(gc.GetString("jfId")) 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{ app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactUnlinked, Type: ActivityContactUnlinked,
UserID: gc.GetString("jfId"), UserID: gc.GetString("jfId"),
@ -543,6 +547,13 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
func (app *appContext) UnlinkMyTelegram(gc *gin.Context) { func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
app.storage.DeleteTelegramKey(gc.GetString("jfId")) 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{ app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactUnlinked, Type: ActivityContactUnlinked,
UserID: gc.GetString("jfId"), UserID: gc.GetString("jfId"),

@ -516,7 +516,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
} }
contactMethods := map[jellyseerr.NotificationsField]any{} contactMethods := map[jellyseerr.NotificationsField]any{}
if emailEnabled { 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 { if err != nil {
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err) app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
} else { } else {
@ -1258,6 +1258,44 @@ func (app *appContext) ModifyLabels(gc *gin.Context) {
respondBool(204, true, gc) 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. // @Summary Modify user's email addresses.
// @Produce json // @Produce json
// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to email addresses" // @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) respond(500, "Couldn't get users", gc)
return return
} }
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
for _, jfUser := range users { for _, jfUser := range users {
id := jfUser.ID id := jfUser.ID
if address, ok := req[id]; ok { if address, ok := req[id]; ok {
var emailStore = EmailAddress{} app.modifyEmail(id, address)
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)
activityType := ActivityContactLinked activityType := ActivityContactLinked
if address == "" { if address == "" {
@ -1305,17 +1331,6 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
Value: "email", Value: "email",
Time: time.Now(), Time: time.Now(),
}, gc, false) }, 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") app.info.Println("Email list modified")

@ -1595,6 +1595,14 @@
"value": false, "value": false,
"description": "Enable the Jellyseerr integration." "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": { "server": {
"name": "URL", "name": "URL",
"required": false, "required": false,

@ -17,6 +17,7 @@ import (
const ( const (
API_SUFFIX = "/api/v1" API_SUFFIX = "/api/v1"
BogusIdentifier = "123412341234123456"
) )
// Jellyseerr represents a running Jellyseerr instance. // 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 { 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) u, err := js.MustGetUser(jfID)
if err != nil { if err != nil {
return err return err
@ -408,3 +412,21 @@ func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
err = json.Unmarshal([]byte(resp), &data) err = json.Unmarshal([]byte(resp), &data)
return data, err 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
}

@ -115,3 +115,18 @@ type NotificationsTemplate struct {
WebPushEnabled bool `json:"webPushEnabled,omitempty"` WebPushEnabled bool `json:"webPushEnabled,omitempty"`
NotifTypes NotificationTypes `json:"notificationTypes"` 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"`
}

Loading…
Cancel
Save