1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-26 02:50:12 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
2a6937228c
accounts: make ombi/jellyseerr appliction optional on "modify settings"
Checkboxes added when applying from a profile.
2024-07-30 21:36:06 +01:00
785395dd20
disable request logging 2024-07-30 20:56:48 +01:00
385953b0cb
jellyseerr: fix email setting, cover all contact adjustment areas
hopefully all places where contact methods can be adjusted should sync
with jellyseerr.
2024-07-30 20:55:45 +01:00
12 changed files with 219 additions and 70 deletions

View File

@ -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,

View File

@ -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"),

View File

@ -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")
@ -1359,12 +1374,12 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
displayprefs = profile.Displayprefs
}
policy = profile.Policy
if app.config.Section("ombi").Key("enabled").MustBool(false) {
if req.Ombi && app.config.Section("ombi").Key("enabled").MustBool(false) {
if profile.Ombi != nil && len(profile.Ombi) != 0 {
ombi = profile.Ombi
}
}
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
if req.Jellyseerr && app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
if profile.Jellyseerr.Enabled {
jellyseerr = profile.Jellyseerr
}

View File

@ -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,

View File

@ -84,30 +84,40 @@
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-modify-user" href="">
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.modifySettingsDescription }}</p>
<div class="flex flex-row mb-4">
<label class="grow mr-2">
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-profile" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
<div class="flex flex-col gap-4 my-2">
<div class="flex flex-row gap-2">
<label class="grow">
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-profile" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
</label>
<label class="grow">
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-user">
<span class="button ~neutral @low supra full-width center">{{ .strings.user }}</span>
</label>
</div>
<div class="select ~neutral @low">
<select id="modify-user-profiles"></select>
</div>
<div class="select ~neutral @low unfocused">
<select id="modify-user-users"></select>
</div>
<label class="switch">
<input type="checkbox" id="modify-user-homescreen" checked>
<span>{{ .strings.applyHomescreenLayout }}</span>
</label>
<label class="grow ml-2">
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-user">
<span class="button ~neutral @low supra full-width center">{{ .strings.user }}</span>
<label class="switch">
<input type="checkbox" id="modify-user-ombi" checked>
<span>{{ .strings.applyOmbi }}</span>
</label>
<label class="switch">
<input type="checkbox" id="modify-user-jellyseerr" checked>
<span>{{ .strings.applyJellyseerr }}</span>
</label>
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.apply }}</span>
</label>
</div>
<div class="select ~neutral @low mb-4">
<select id="modify-user-profiles"></select>
</div>
<div class="select ~neutral @low mb-4 unfocused">
<select id="modify-user-users"></select>
</div>
<label class="switch mb-4">
<input type="checkbox" id="modify-user-homescreen" checked>
<span>{{ .strings.applyHomescreenLayout }}</span>
</label>
<label>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.apply }}</span>
</label>
</form>
</div>
{{ if .referralsEnabled }}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -81,6 +81,8 @@
"useInviteExpiry": "Set expiry from profile/invite",
"useInviteExpiryNote": "By default, invites expire after 90 days but can be renewed by the user. Enable for the referral to be disabled after the time set.",
"applyHomescreenLayout": "Apply homescreen layout",
"applyOmbi": "Apply Ombi profile (if available)",
"applyJellyseerr": "Apply Jellyseerr profile (if available)",
"sendDeleteNotificationEmail": "Send notification message",
"sendDeleteNotifiationExample": "Your account has been deleted.",
"settingsRestart": "Restart",

View File

@ -369,7 +369,7 @@ func start(asDaemon, firstCall bool) {
app.config.Section("jellyseerr").Key("api_key").String(),
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
)
app.js.LogRequestBodies = true
// app.js.LogRequestBodies = true
}

View File

@ -179,6 +179,8 @@ type userSettingsDTO struct {
ApplyTo []string `json:"apply_to"` // Users to apply settings to
ID string `json:"id"` // ID of user (if from = "user")
Homescreen bool `json:"homescreen"` // Whether to apply homescreen layout or not
Ombi bool `json:"ombi"` // Whether to apply ombi profile or not
Jellyseerr bool `json:"jellyseerr"` // Whether to apply jellyseerr profile or not
}
type announcementDTO struct {

View File

@ -187,6 +187,7 @@ login.onLogin = () => {
console.log("Logged in.");
window.updater = new Updater();
// FIXME: Decide whether to autoload activity or not
window.invites.reload()
setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000);
const currentTab = window.tabs.current;
switch (currentTab) {

View File

@ -795,6 +795,10 @@ export class accountsList {
private _searchBox = document.getElementById("accounts-search") as HTMLInputElement;
private _search: Search;
private _applyHomesreen = document.getElementById("modify-user-homescreen") as HTMLInputElement;
private _applyOmbi = document.getElementById("modify-user-ombi") as HTMLInputElement;
private _applyJellyseerr = document.getElementById("modify-user-jellyseerr") as HTMLInputElement;
private _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement;
private _users: { [id: string]: user };
private _ordering: string[] = [];
@ -1459,6 +1463,7 @@ export class accountsList {
const modalHeader = document.getElementById("header-modify-user");
modalHeader.textContent = window.lang.quantity("modifySettingsFor", this._collectUsers().length)
let list = this._collectUsers();
(() => {
let innerHTML = "";
for (const profile of window.availableProfiles) {
@ -1477,6 +1482,7 @@ export class accountsList {
const form = document.getElementById("form-modify-user") as HTMLFormElement;
const button = form.querySelector("span.submit") as HTMLSpanElement;
this._modifySettingsProfile.checked = true;
this._modifySettingsUser.checked = false;
form.onsubmit = (event: Event) => {
@ -1484,7 +1490,9 @@ export class accountsList {
toggleLoader(button);
let send = {
"apply_to": list,
"homescreen": (document.getElementById("modify-user-homescreen") as HTMLInputElement).checked
"homescreen": this._applyHomesreen.checked,
"ombi": this._applyOmbi.checked,
"jellyseerr": this._applyJellyseerr.checked
};
if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) {
send["from"] = "profile";
@ -1821,6 +1829,16 @@ export class accountsList {
};
this._modifySettings.onclick = this.modifyUsers;
this._modifySettings.classList.add("unfocused");
if (window.ombiEnabled)
this._applyOmbi.parentElement.classList.remove("unfocused");
else
this._applyOmbi.parentElement.classList.add("unfocused");
if (window.jellyseerrEnabled)
this._applyJellyseerr.parentElement.classList.remove("unfocused");
else
this._applyJellyseerr.parentElement.classList.add("unfocused");
const checkSource = () => {
const profileSpan = this._modifySettingsProfile.nextElementSibling as HTMLSpanElement;
const userSpan = this._modifySettingsUser.nextElementSibling as HTMLSpanElement;
@ -1831,6 +1849,8 @@ export class accountsList {
profileSpan.classList.remove("@low");
userSpan.classList.remove("@high");
userSpan.classList.add("@low");
this._applyOmbi.parentElement.classList.remove("unfocused");
this._applyJellyseerr.parentElement.classList.remove("unfocused");
} else {
this._userSelect.parentElement.classList.remove("unfocused");
this._profileSelect.parentElement.classList.add("unfocused");
@ -1838,6 +1858,8 @@ export class accountsList {
userSpan.classList.remove("@low");
profileSpan.classList.remove("@high");
profileSpan.classList.add("@low");
this._applyOmbi.parentElement.classList.add("unfocused");
this._applyJellyseerr.parentElement.classList.add("unfocused");
}
};
this._modifySettingsProfile.onchange = checkSource;