1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-09-19 19:00:11 +00:00
jfa-go/api-users.go
Harvey Tindall f348262f88
logmessages: finish up to api-users (alphabetically), refactor
.go files done in alphabetical order. Some refactoring done to
checkInvite(s) so they share most code. Also removed some useless debug
lines.
2024-08-01 13:59:24 +01:00

1497 lines
48 KiB
Go

package main
import (
"fmt"
"net/url"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/hrfee/jfa-go/jellyseerr"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4"
)
// @Summary Creates a new Jellyfin user without an invite.
// @Produce json
// @Param newUserDTO body newUserDTO true "New user request object"
// @Success 200
// @Router /users [post]
// @Security Bearer
// @tags Users
func (app *appContext) NewUserAdmin(gc *gin.Context) {
respondUser := func(code int, user, email bool, msg string, gc *gin.Context) {
resp := newUserResponse{
User: user,
Email: email,
Error: msg,
}
gc.JSON(code, resp)
gc.Abort()
}
var req newUserDTO
gc.BindJSON(&req)
existingUser, _, _ := app.jf.UserByName(req.Username, false)
if existingUser.Name != "" {
msg := lm.UserExists
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, msg)
respondUser(401, false, false, msg, gc)
return
}
user, status, err := app.jf.NewUser(req.Username, req.Password)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, err)
respondUser(401, false, false, err.Error(), gc)
return
}
id := user.ID
// Record activity
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityCreation,
UserID: id,
SourceType: ActivityAdmin,
Source: gc.GetString("jfId"),
Value: user.Name,
Time: time.Now(),
}, gc, false)
profile := app.storage.GetDefaultProfile()
if req.Profile != "" && req.Profile != "none" {
if p, ok := app.storage.GetProfileKey(req.Profile); ok {
profile = p
} else {
app.debug.Printf(lm.FailedGetProfile+lm.FallbackToDefault, req.Profile)
}
status, err = app.jf.SetPolicy(id, profile.Policy)
if !(status == 200 || status == 204 || err == nil) {
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, req.Username, err)
}
status, err = app.jf.SetConfiguration(id, profile.Configuration)
if (status == 200 || status == 204) && err == nil {
status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs)
}
if !((status == 200 || status == 204) && err == nil) {
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, req.Username, err)
}
}
app.jf.CacheExpiry = time.Now()
if emailEnabled {
app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true})
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
if profile.Ombi == nil {
profile.Ombi = map[string]interface{}{}
}
errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi)
if err != nil || code != 200 {
app.err.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err)
app.debug.Printf(lm.AdditionalOmbiErrors, strings.Join(errors, ", "))
} else {
app.info.Printf(lm.CreateUser, lm.Ombi, req.Username)
}
}
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(lm.FailedCreateUser, lm.Jellyseerr, req.Username, err)
} else {
app.info.Printf(lm.CreateUser, lm.Jellyseerr, req.Username)
}
err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User)
if err != nil {
app.err.Printf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, req.Username, err)
}
err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications)
if err != nil {
app.err.Printf(lm.FailedApplyTemplate, "notifications", lm.Jellyseerr, req.Username, err)
}
if emailEnabled {
err = app.js.ModifyUser(id, map[jellyseerr.UserField]any{jellyseerr.FieldEmail: req.Email})
if err != nil {
app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, id, err)
}
}
}
if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" {
msg, err := app.email.constructWelcome(req.Username, time.Time{}, app, false)
if err != nil {
app.err.Printf(lm.FailedConstructWelcomeMessage, id, err)
respondUser(500, true, false, err.Error(), gc)
return
} else if err := app.email.send(msg, req.Email); err != nil {
app.err.Printf(lm.FailedSendWelcomeMessage, req.Username, req.Email, err)
respondUser(500, true, false, err.Error(), gc)
return
} else {
app.info.Printf(lm.SentWelcomeMessage, req.Username, req.Email)
}
}
respondUser(200, true, true, "", gc)
}
type errorFunc func(gc *gin.Context)
// Used on the form & when a users email has been confirmed.
func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context) (f errorFunc, success bool) {
existingUser, _, _ := app.jf.UserByName(req.Username, false)
if existingUser.Name != "" {
f = func(gc *gin.Context) {
msg := lm.UserExists
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, msg)
respond(401, "errorUserExists", gc)
}
success = false
return
}
var discordUser DiscordUser
discordVerified := false
if discordEnabled {
if req.DiscordPIN == "" {
if app.config.Section("discord").Key("required").MustBool(false) {
f = func(gc *gin.Context) {
app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, lm.AccountUnverified)
respond(401, "errorDiscordVerification", gc)
}
success = false
return
}
} else {
discordUser, discordVerified = app.discord.UserVerified(req.DiscordPIN)
if !discordVerified {
f = func(gc *gin.Context) {
app.info.Printf(lm.FailedLinkUser, lm.Discord, "?", req.Code, fmt.Sprintf(lm.InvalidPIN, req.DiscordPIN))
respond(401, "errorInvalidPIN", gc)
}
success = false
return
}
if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(discordUser.ID) {
f = func(gc *gin.Context) {
app.debug.Printf(lm.FailedLinkUser, lm.Discord, discordUser.ID, req.Code, lm.AccountLinked)
respond(400, "errorAccountLinked", gc)
}
success = false
return
}
err := app.discord.ApplyRole(discordUser.ID)
if err != nil {
f = func(gc *gin.Context) {
app.err.Printf(lm.FailedLinkUser, lm.Discord, discordUser.ID, req.Code, fmt.Sprintf(lm.FailedSetDiscordMemberRole, err))
respond(401, "error", gc)
}
success = false
return
}
}
}
var matrixUser MatrixUser
matrixVerified := false
if matrixEnabled {
if req.MatrixPIN == "" {
if app.config.Section("matrix").Key("required").MustBool(false) {
f = func(gc *gin.Context) {
app.info.Printf(lm.FailedLinkUser, lm.Matrix, "?", req.Code, lm.AccountUnverified)
respond(401, "errorMatrixVerification", gc)
}
success = false
return
}
} else {
user, ok := app.matrix.tokens[req.MatrixPIN]
if !ok || !user.Verified {
matrixVerified = false
f = func(gc *gin.Context) {
uid := ""
if ok {
uid = user.User.UserID
}
app.info.Printf(lm.FailedLinkUser, lm.Matrix, uid, req.Code, fmt.Sprintf(lm.InvalidPIN, req.MatrixPIN))
respond(401, "errorInvalidPIN", gc)
}
success = false
return
}
if app.config.Section("matrix").Key("require_unique").MustBool(false) && app.matrix.UserExists(user.User.UserID) {
f = func(gc *gin.Context) {
app.debug.Printf(lm.FailedLinkUser, lm.Matrix, user.User.UserID, req.Code, lm.AccountLinked)
respond(400, "errorAccountLinked", gc)
}
success = false
return
}
matrixVerified = user.Verified
matrixUser = *user.User
}
}
var tgToken TelegramVerifiedToken
telegramVerified := false
if telegramEnabled {
if req.TelegramPIN == "" {
if app.config.Section("telegram").Key("required").MustBool(false) {
f = func(gc *gin.Context) {
app.info.Printf(lm.FailedLinkUser, lm.Telegram, "?", req.Code, lm.AccountUnverified)
respond(401, "errorTelegramVerification", gc)
}
success = false
return
}
} else {
tgToken, telegramVerified = app.telegram.TokenVerified(req.TelegramPIN)
if !telegramVerified {
f = func(gc *gin.Context) {
app.info.Printf(lm.FailedLinkUser, lm.Telegram, tgToken.Username, req.Code, fmt.Sprintf(lm.InvalidPIN, req.TelegramPIN))
respond(401, "errorInvalidPIN", gc)
}
success = false
return
}
if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(tgToken.Username) {
f = func(gc *gin.Context) {
app.debug.Printf(lm.FailedLinkUser, lm.Telegram, tgToken.Username, req.Code, lm.AccountLinked)
respond(400, "errorAccountLinked", gc)
}
success = false
return
}
}
}
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) && !confirmed {
claims := jwt.MapClaims{
"valid": true,
"invite": req.Code,
"exp": time.Now().Add(30 * time.Minute).Unix(),
"type": "confirmation",
}
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
if err != nil {
f = func(gc *gin.Context) {
app.info.Printf(lm.FailedSignJWT, err)
respond(500, "errorUnknown", gc)
}
success = false
return
}
if app.ConfirmationKeys == nil {
app.ConfirmationKeys = map[string]map[string]newUserDTO{}
}
cKeys, ok := app.ConfirmationKeys[req.Code]
if !ok {
cKeys = map[string]newUserDTO{}
}
cKeys[key] = req
app.confirmationKeysLock.Lock()
app.ConfirmationKeys[req.Code] = cKeys
app.confirmationKeysLock.Unlock()
f = func(gc *gin.Context) {
app.debug.Printf(lm.EmailConfirmationRequired, req.Username)
respond(401, "confirmEmail", gc)
msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app, false)
if err != nil {
app.err.Printf(lm.FailedConstructConfirmationEmail, req.Code, err)
} else if err := app.email.send(msg, req.Email); err != nil {
app.err.Printf(lm.FailedSendConfirmationEmail, req.Code, req.Email, err)
} else {
app.err.Printf(lm.SentConfirmationEmail, req.Code, req.Email)
}
}
success = false
return
}
user, status, err := app.jf.NewUser(req.Username, req.Password)
if !(status == 200 || status == 204) || err != nil {
f = func(gc *gin.Context) {
app.err.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, err)
respond(401, app.storage.lang.Admin[app.storage.lang.chosenAdminLang].Notifications.get("errorUnknown"), gc)
}
success = false
return
}
invite, _ := app.storage.GetInvitesKey(req.Code)
app.checkInvite(req.Code, true, req.Username)
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) {
for address, settings := range invite.Notify {
if settings["notify-creation"] {
go func(addr string) {
msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app, false)
if err != nil {
app.err.Printf(lm.FailedConstructCreationAdmin, req.Code, err)
} else {
// Check whether notify "addr" is an email address of Jellyfin ID
if strings.Contains(addr, "@") {
err = app.email.send(msg, addr)
} else {
err = app.sendByID(msg, addr)
}
if err != nil {
app.err.Printf(lm.FailedSendCreationAdmin, req.Code, addr, err)
} else {
app.info.Printf(lm.SentCreationAdmin, req.Code, addr)
}
}
}(address)
}
}
}
id := user.ID
// Record activity
sourceType := ActivityAnon
source := ""
if invite.ReferrerJellyfinID != "" {
sourceType = ActivityUser
source = invite.ReferrerJellyfinID
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityCreation,
UserID: id,
SourceType: sourceType,
Source: source,
InviteCode: invite.Code,
Value: user.Name,
Time: time.Now(),
}, gc, true)
emailStore := EmailAddress{
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
}
var profile Profile
if invite.Profile != "" {
app.debug.Printf(lm.ApplyProfile, invite.Profile)
var ok bool
profile, ok = app.storage.GetProfileKey(invite.Profile)
if !ok {
profile = app.storage.GetDefaultProfile()
}
status, err = app.jf.SetPolicy(id, profile.Policy)
if !((status == 200 || status == 204) && err == nil) {
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, id, err)
}
status, err = app.jf.SetConfiguration(id, profile.Configuration)
if (status == 200 || status == 204) && err == nil {
status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs)
}
if !((status == 200 || status == 204) && err == nil) {
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, id, err)
}
if app.config.Section("user_page").Key("enabled").MustBool(false) && app.config.Section("user_page").Key("referrals").MustBool(false) && profile.ReferralTemplateKey != "" {
emailStore.ReferralTemplateKey = profile.ReferralTemplateKey
// Store here, just incase email are disabled (whether this is even possible, i don't know)
app.storage.SetEmailsKey(id, emailStore)
// If UseReferralExpiry is enabled, create the ref now so the clock starts ticking
refInv := Invite{}
err = app.storage.db.Get(profile.ReferralTemplateKey, &refInv)
if refInv.UseReferralExpiry {
refInv.Code = GenerateInviteCode()
expiryDelta := refInv.ValidTill.Sub(refInv.Created)
refInv.Created = time.Now()
refInv.ValidTill = refInv.Created.Add(expiryDelta)
refInv.IsReferral = true
refInv.ReferrerJellyfinID = id
app.storage.SetInvitesKey(refInv.Code, refInv)
}
}
}
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
if req.Email != "" || invite.UserLabel != "" {
app.storage.SetEmailsKey(id, emailStore)
}
expiry := time.Time{}
if invite.UserExpiry {
expiry = time.Now().AddDate(0, invite.UserMonths, invite.UserDays).Add(time.Duration((60*invite.UserHours)+invite.UserMinutes) * time.Minute)
app.storage.SetUserExpiryKey(id, UserExpiry{Expiry: expiry})
}
if discordVerified {
discordUser.Contact = req.DiscordContact
if app.storage.deprecatedDiscord == nil {
app.storage.deprecatedDiscord = discordStore{}
}
// Note we don't log an activity here, since it's part of creating a user.
app.storage.SetDiscordKey(user.ID, discordUser)
delete(app.discord.verifiedTokens, req.DiscordPIN)
}
if telegramVerified {
tgUser := TelegramUser{
ChatID: tgToken.ChatID,
Username: tgToken.Username,
Contact: req.TelegramContact,
}
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
tgUser.Lang = lang
}
if app.storage.deprecatedTelegram == nil {
app.storage.deprecatedTelegram = telegramStore{}
}
app.telegram.DeleteVerifiedToken(req.TelegramPIN)
app.storage.SetTelegramKey(user.ID, tgUser)
}
if invite.Profile != "" && app.config.Section("ombi").Key("enabled").MustBool(false) {
if profile.Ombi != nil && len(profile.Ombi) != 0 {
template := profile.Ombi
errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, template)
accountExists := false
var ombiUser map[string]interface{}
if err != nil || code != 200 {
// Check if on the off chance, Ombi's user importer has already added the account.
ombiUser, status, err = app.getOmbiImportedUser(req.Username)
if status == 200 && err == nil {
app.info.Println(lm.Ombi + " " + lm.UserExists)
accountExists = true
template["password"] = req.Password
status, err = app.applyOmbiProfile(ombiUser, template)
if status != 200 || err != nil {
app.err.Printf(lm.FailedApplyProfile, lm.Ombi, req.Username, err)
}
} else {
app.info.Printf(lm.FailedCreateUser, lm.Ombi, req.Username, err)
app.debug.Printf(lm.AdditionalOmbiErrors, strings.Join(errors, ", "))
}
} else {
ombiUser, status, err = app.getOmbiUser(id)
if status != 200 || err != nil {
app.err.Printf(lm.FailedGetUser, id, lm.Ombi, err)
} else {
app.info.Println(lm.CreateUser, lm.Ombi, id)
accountExists = true
}
}
if accountExists {
if discordVerified || telegramVerified {
dID := ""
tUser := ""
if discordVerified {
dID = discordUser.ID
}
if telegramVerified {
u, _ := app.storage.GetTelegramKey(user.ID)
tUser = u.Username
}
resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Ombi, err)
app.debug.Printf(lm.AdditionalOmbiErrors, resp)
}
}
}
}
}
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(lm.FailedCreateUser, lm.Jellyseerr, id, err)
} else {
app.info.Printf(lm.CreateUser, lm.Jellyseerr, id)
}
err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User)
if err != nil {
app.err.Printf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, id, err)
}
err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications)
if err != nil {
app.err.Printf(lm.FailedApplyTemplate, "notifications", lm.Jellyseerr, id, err)
}
contactMethods := map[jellyseerr.NotificationsField]any{}
if emailEnabled {
err = app.js.ModifyMainUserSettings(id, jellyseerr.MainUserSettings{Email: req.Email})
if err != nil {
app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, id, 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(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
}
}
}
}
if matrixVerified {
matrixUser.Contact = req.MatrixContact
delete(app.matrix.tokens, req.MatrixPIN)
if app.storage.deprecatedMatrix == nil {
app.storage.deprecatedMatrix = matrixStore{}
}
app.storage.SetMatrixKey(user.ID, matrixUser)
}
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramVerified || discordVerified || matrixVerified {
name := app.getAddressOrName(user.ID)
msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
if err != nil {
app.err.Printf(lm.FailedConstructWelcomeMessage, id, err)
} else if err := app.sendByID(msg, user.ID); err != nil {
app.err.Printf(lm.FailedSendWelcomeMessage, id, req.Email, err)
} else {
app.info.Printf(lm.SentWelcomeMessage, id, req.Email)
}
}
app.jf.CacheExpiry = time.Now()
success = true
return
}
// @Summary Creates a new Jellyfin user via invite code
// @Produce json
// @Param newUserDTO body newUserDTO true "New user request object"
// @Success 200 {object} PasswordValidation
// @Failure 400 {object} PasswordValidation
// @Router /newUser [post]
// @tags Users
func (app *appContext) NewUser(gc *gin.Context) {
var req newUserDTO
gc.BindJSON(&req)
if app.config.Section("captcha").Key("enabled").MustBool(false) && !app.verifyCaptcha(req.Code, req.CaptchaID, req.CaptchaText, false) {
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, lm.IncorrectCaptcha)
respond(400, "errorCaptcha", gc)
return
}
if !app.checkInvite(req.Code, false, "") {
app.info.Printf(lm.FailedCreateUser, lm.Jellyfin, req.Username, fmt.Sprintf(lm.InvalidInviteCode, req.Code))
respond(401, "errorInvalidCode", gc)
return
}
validation := app.validator.validate(req.Password)
valid := true
for _, val := range validation {
if !val {
valid = false
break
}
}
if !valid {
// 200 bcs idk what i did in js
gc.JSON(200, validation)
return
}
if emailEnabled {
if app.config.Section("email").Key("required").MustBool(false) && !strings.Contains(req.Email, "@") {
respond(400, "errorNoEmail", gc)
return
}
if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" && app.EmailAddressExists(req.Email) {
respond(400, "errorEmailLinked", gc)
return
}
}
f, success := app.newUser(req, false, gc)
if !success {
f(gc)
return
}
code := 200
for _, val := range validation {
if !val {
code = 400
}
}
gc.JSON(code, validation)
}
// @Summary Enable/Disable a list of users, optionally notifying them why.
// @Produce json
// @Param enableDisableUserDTO body enableDisableUserDTO true "User enable/disable request object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Failure 500 {object} errorListDTO "List of errors"
// @Router /users/enable [post]
// @Security Bearer
// @tags Users
func (app *appContext) EnableDisableUsers(gc *gin.Context) {
var req enableDisableUserDTO
gc.BindJSON(&req)
errors := errorListDTO{
"GetUser": map[string]string{},
"SetPolicy": map[string]string{},
}
sendMail := messagesEnabled
var msg *Message
var err error
if sendMail {
if req.Enabled {
msg, err = app.email.constructEnabled(req.Reason, app, false)
} else {
msg, err = app.email.constructDisabled(req.Reason, app, false)
}
if err != nil {
app.err.Printf(lm.FailedConstructEnableDisableMessage, "?", err)
sendMail = false
}
}
activityType := ActivityDisabled
if req.Enabled {
activityType = ActivityEnabled
}
for _, userID := range req.Users {
user, status, err := app.jf.UserByID(userID, false)
if status != 200 || err != nil {
errors["GetUser"][userID] = fmt.Sprintf("%d %v", status, err)
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
continue
}
user.Policy.IsDisabled = !req.Enabled
status, err = app.jf.SetPolicy(userID, user.Policy)
if !(status == 200 || status == 204) || err != nil {
errors["SetPolicy"][userID] = fmt.Sprintf("%d %v", status, err)
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, userID, err)
continue
}
// Record activity
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: activityType,
UserID: userID,
SourceType: ActivityAdmin,
Source: gc.GetString("jfId"),
Time: time.Now(),
}, gc, false)
if sendMail && req.Notify {
if err := app.sendByID(msg, userID); err != nil {
app.err.Printf(lm.FailedSendEnableDisableMessage, userID, "?", err)
continue
}
}
}
app.jf.CacheExpiry = time.Now()
if len(errors["GetUser"]) != 0 || len(errors["SetPolicy"]) != 0 {
gc.JSON(500, errors)
return
}
respondBool(200, true, gc)
}
// @Summary Delete a list of users, optionally notifying them why.
// @Produce json
// @Param deleteUserDTO body deleteUserDTO true "User deletion request object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} stringResponse
// @Failure 500 {object} errorListDTO "List of errors"
// @Router /users [delete]
// @Security Bearer
// @tags Users
func (app *appContext) DeleteUsers(gc *gin.Context) {
var req deleteUserDTO
gc.BindJSON(&req)
errors := map[string]string{}
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
sendMail := messagesEnabled
var msg *Message
var err error
if sendMail {
msg, err = app.email.constructDeleted(req.Reason, app, false)
if err != nil {
app.err.Printf(lm.FailedConstructDeletionMessage, "?", err)
sendMail = false
}
}
for _, userID := range req.Users {
if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(userID)
if code == 200 && err == nil {
if id, ok := ombiUser["id"]; ok {
status, err := app.ombi.DeleteUser(id.(string))
if err != nil || status != 200 {
app.err.Printf(lm.FailedDeleteUser, lm.Ombi, userID, err)
errors[userID] = fmt.Sprintf("Ombi: %d %v, ", status, err)
}
}
}
}
username := ""
if user, status, err := app.jf.UserByID(userID, false); status == 200 && err == nil {
username = user.Name
}
status, err := app.jf.DeleteUser(userID)
if !(status == 200 || status == 204) || err != nil {
msg := fmt.Sprintf("%d: %v", status, err)
if _, ok := errors[userID]; !ok {
errors[userID] = msg
} else {
errors[userID] += msg
}
}
// Record activity
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityDeletion,
UserID: userID,
SourceType: ActivityAdmin,
Source: gc.GetString("jfId"),
Value: username,
Time: time.Now(),
}, gc, false)
if sendMail && req.Notify {
if err := app.sendByID(msg, userID); err != nil {
app.err.Printf(lm.FailedSendDeletionMessage, userID, "?", err)
}
}
}
app.jf.CacheExpiry = time.Now()
if len(errors) == len(req.Users) {
respondBool(500, false, gc)
app.err.Printf(lm.FailedDeleteUsers, lm.Jellyfin, errors[req.Users[0]])
return
} else if len(errors) != 0 {
gc.JSON(500, errors)
return
}
respondBool(200, true, gc)
}
// @Summary Extend time before the user(s) expiry, or create an expiry if it doesn't exist.
// @Produce json
// @Param extendExpiryDTO body extendExpiryDTO true "Extend expiry object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/extend [post]
// @tags Users
func (app *appContext) ExtendExpiry(gc *gin.Context) {
var req extendExpiryDTO
gc.BindJSON(&req)
app.info.Printf("Expiry extension requested for %d user(s)", len(req.Users))
if req.Months <= 0 && req.Days <= 0 && req.Hours <= 0 && req.Minutes <= 0 && req.Timestamp <= 0 {
respondBool(400, false, gc)
return
}
for _, id := range req.Users {
base := time.Now()
if expiry, ok := app.storage.GetUserExpiryKey(id); ok {
base = expiry.Expiry
}
app.debug.Printf(lm.ExtendCreateExpiry, id)
expiry := UserExpiry{}
if req.Timestamp != 0 {
expiry.Expiry = time.Unix(req.Timestamp, 0)
} else {
expiry.Expiry = base.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute)
}
app.storage.SetUserExpiryKey(id, expiry)
if messagesEnabled && req.Notify {
go func(uid string, exp time.Time) {
user, status, err := app.jf.UserByID(uid, false)
if status != 200 || err != nil {
return
}
msg, err := app.email.constructExpiryAdjusted(user.Name, exp, req.Reason, app, false)
if err != nil {
app.err.Printf(lm.FailedConstructExpiryAdjustmentMessage, uid, err)
return
}
if err := app.sendByID(msg, uid); err != nil {
app.err.Printf(lm.FailedSendExpiryAdjustmentMessage, uid, "?", err)
}
}(id, expiry.Expiry)
}
}
respondBool(204, true, gc)
}
// @Summary Remove an expiry from a user's account.
// @Produce json
// @Param id path string true "id of user to extend expiry of."
// @Success 200 {object} boolResponse
// @Router /users/{id}/expiry [delete]
// @tags Users
func (app *appContext) RemoveExpiry(gc *gin.Context) {
app.storage.DeleteUserExpiryKey(gc.Param("id"))
respondBool(200, true, gc)
}
// @Summary Enable referrals for the given user(s) based on the rules set in the given invite code, or profile.
// @Produce json
// @Param EnableDisableReferralDTO body EnableDisableReferralDTO true "List of users"
// @Param mode path string true "mode of template sourcing from 'invite' or 'profile'."
// @Param source path string true "invite code or profile name, depending on what mode is."
// @Param useExpiry path string true "with-expiry or none."
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/referral/{mode}/{source}/{useExpiry} [post]
// @Security Bearer
// @tags Users
func (app *appContext) EnableReferralForUsers(gc *gin.Context) {
var req EnableDisableReferralDTO
gc.BindJSON(&req)
mode := gc.Param("mode")
source := gc.Param("source")
useExpiry := gc.Param("useExpiry") == "with-expiry"
baseInv := Invite{}
if mode == "profile" {
profile, ok := app.storage.GetProfileKey(source)
err := app.storage.db.Get(profile.ReferralTemplateKey, &baseInv)
if !ok || profile.ReferralTemplateKey == "" || err != nil {
app.debug.Printf(lm.FailedGetReferralTemplate, profile.ReferralTemplateKey, err)
respondBool(400, false, gc)
return
}
app.debug.Printf(lm.GetReferralTemplate, profile.ReferralTemplateKey)
} else if mode == "invite" {
// Get the invite, and modify it to turn it into a referral
err := app.storage.db.Get(source, &baseInv)
if err != nil {
app.debug.Printf(lm.InvalidInviteCode, source)
respondBool(400, false, gc)
return
}
}
for _, u := range req.Users {
// 1. Wipe out any existing referral codes.
app.storage.db.DeleteMatching(Invite{}, badgerhold.Where("ReferrerJellyfinID").Eq(u))
// 2. Generate referral invite.
inv := baseInv
inv.Code = GenerateInviteCode()
expiryDelta := inv.ValidTill.Sub(inv.Created)
inv.Created = time.Now()
if useExpiry {
inv.ValidTill = inv.Created.Add(expiryDelta)
} else {
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
}
inv.IsReferral = true
inv.ReferrerJellyfinID = u
inv.UseReferralExpiry = useExpiry
app.storage.SetInvitesKey(inv.Code, inv)
}
}
// @Summary Disable referrals for the given user(s).
// @Produce json
// @Param EnableDisableReferralDTO body EnableDisableReferralDTO true "List of users"
// @Success 200 {object} boolResponse
// @Router /users/referral [delete]
// @Security Bearer
// @tags Users
func (app *appContext) DisableReferralForUsers(gc *gin.Context) {
var req EnableDisableReferralDTO
gc.BindJSON(&req)
for _, u := range req.Users {
// 1. Delete directly bound template
app.storage.db.DeleteMatching(Invite{}, badgerhold.Where("ReferrerJellyfinID").Eq(u))
// 2. Check for and delete profile-attached template
user, ok := app.storage.GetEmailsKey(u)
if !ok {
continue
}
user.ReferralTemplateKey = ""
app.storage.SetEmailsKey(u, user)
}
respondBool(200, true, gc)
}
// @Summary Send an announcement via email to a given list of users.
// @Produce json
// @Param announcementDTO body announcementDTO true "Announcement request object"
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/announce [post]
// @Security Bearer
// @tags Users
func (app *appContext) Announce(gc *gin.Context) {
var req announcementDTO
gc.BindJSON(&req)
if !messagesEnabled {
respondBool(400, false, gc)
return
}
// Generally, we only need to construct once. If {username} is included, however, this needs to be done for each user.
unique := strings.Contains(req.Message, "{username}")
if unique {
for _, userID := range req.Users {
user, status, err := app.jf.UserByID(userID, false)
if status != 200 || err != nil {
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
continue
}
msg, err := app.email.constructTemplate(req.Subject, req.Message, app, user.Name)
if err != nil {
app.err.Printf(lm.FailedConstructAnnouncementMessage, userID, err)
respondBool(500, false, gc)
return
} else if err := app.sendByID(msg, userID); err != nil {
app.err.Printf(lm.FailedSendAnnouncementMessage, userID, "?", err)
respondBool(500, false, gc)
return
}
}
app.info.Printf(lm.SentAnnouncementMessage, userID, "?")
} else {
msg, err := app.email.constructTemplate(req.Subject, req.Message, app)
if err != nil {
app.err.Printf(lm.FailedConstructAnnouncementMessage, "*", err)
respondBool(500, false, gc)
return
} else if err := app.sendByID(msg, req.Users...); err != nil {
app.err.Printf(lm.FailedSendAnnouncementMessage, "*", "?", err)
respondBool(500, false, gc)
return
}
app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
}
respondBool(200, true, gc)
}
// @Summary Save an announcement as a template for use or editing later.
// @Produce json
// @Param announcementTemplate body announcementTemplate true "Announcement request object"
// @Success 200 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/announce/template [post]
// @Security Bearer
// @tags Users
func (app *appContext) SaveAnnounceTemplate(gc *gin.Context) {
var req announcementTemplate
gc.BindJSON(&req)
if !messagesEnabled {
respondBool(400, false, gc)
return
}
app.storage.SetAnnouncementsKey(req.Name, req)
respondBool(200, true, gc)
}
// @Summary Save an announcement as a template for use or editing later.
// @Produce json
// @Success 200 {object} getAnnouncementsDTO
// @Router /users/announce [get]
// @Security Bearer
// @tags Users
func (app *appContext) GetAnnounceTemplates(gc *gin.Context) {
resp := &getAnnouncementsDTO{make([]string, len(app.storage.GetAnnouncements()))}
for i, a := range app.storage.GetAnnouncements() {
resp.Announcements[i] = a.Name
}
gc.JSON(200, resp)
}
// @Summary Get an announcement template.
// @Produce json
// @Success 200 {object} announcementTemplate
// @Failure 400 {object} boolResponse
// @Param name path string true "name of template (url encoded if necessary)"
// @Router /users/announce/template/{name} [get]
// @Security Bearer
// @tags Users
func (app *appContext) GetAnnounceTemplate(gc *gin.Context) {
escapedName := gc.Param("name")
name, err := url.QueryUnescape(escapedName)
if err != nil {
respondBool(400, false, gc)
return
}
if announcement, ok := app.storage.GetAnnouncementsKey(name); ok {
gc.JSON(200, announcement)
return
}
respondBool(400, false, gc)
}
// @Summary Delete an announcement template.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param name path string true "name of template"
// @Router /users/announce/template/{name} [delete]
// @Security Bearer
// @tags Users
func (app *appContext) DeleteAnnounceTemplate(gc *gin.Context) {
name := gc.Param("name")
app.storage.DeleteAnnouncementsKey(name)
respondBool(200, false, gc)
}
// @Summary Generate password reset links for a list of users, sending the links to them if possible.
// @Produce json
// @Param AdminPasswordResetDTO body AdminPasswordResetDTO true "List of user IDs"
// @Success 204 {object} boolResponse
// @Success 200 {object} AdminPasswordResetRespDTO
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/password-reset [post]
// @Security Bearer
// @tags Users
func (app *appContext) AdminPasswordReset(gc *gin.Context) {
var req AdminPasswordResetDTO
gc.BindJSON(&req)
if req.Users == nil || len(req.Users) == 0 {
respondBool(400, false, gc)
return
}
linkCount := 0
var pwr InternalPWR
var err error
resp := AdminPasswordResetRespDTO{}
for _, id := range req.Users {
pwr, err = app.GenInternalReset(id)
if err != nil {
app.err.Printf(lm.FailedGetUser, id, lm.Jellyfin, err)
respondBool(500, false, gc)
return
}
if app.internalPWRs == nil {
app.internalPWRs = map[string]InternalPWR{}
}
app.internalPWRs[pwr.PIN] = pwr
sendAddress := app.getAddressOrName(id)
if sendAddress == "" || len(req.Users) == 1 {
resp.Link, err = app.GenResetLink(pwr.PIN)
linkCount++
if sendAddress == "" {
resp.Manual = true
}
}
if sendAddress != "" {
msg, err := app.email.constructReset(
PasswordReset{
Pin: pwr.PIN,
Username: pwr.Username,
Expiry: pwr.Expiry,
Internal: true,
}, app, false,
)
if err != nil {
app.err.Printf(lm.FailedConstructPWRMessage, id, err)
respondBool(500, false, gc)
return
} else if err := app.sendByID(msg, id); err != nil {
app.err.Printf(lm.FailedSendPWRMessage, id, sendAddress, err)
} else {
app.info.Printf(lm.SentPWRMessage, id, sendAddress)
}
}
}
if resp.Link != "" && linkCount == 1 {
gc.JSON(200, resp)
return
}
respondBool(204, true, gc)
}
// @Summary Get a list of Jellyfin users.
// @Produce json
// @Success 200 {object} getUsersDTO
// @Failure 500 {object} stringResponse
// @Router /users [get]
// @Security Bearer
// @tags Users
func (app *appContext) GetUsers(gc *gin.Context) {
var resp getUsersDTO
users, status, err := app.jf.GetUsers(false)
resp.UserList = make([]respUser, len(users))
if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Couldn't get users", gc)
return
}
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
allowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
i := 0
for _, jfUser := range users {
user := respUser{
ID: jfUser.ID,
Name: jfUser.Name,
Admin: jfUser.Policy.IsAdministrator,
Disabled: jfUser.Policy.IsDisabled,
ReferralsEnabled: false,
}
if !jfUser.LastActivityDate.IsZero() {
user.LastActive = jfUser.LastActivityDate.Unix()
}
if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok {
user.Email = email.Addr
user.NotifyThroughEmail = email.Contact
user.Label = email.Label
user.AccountsAdmin = (app.jellyfinLogin) && (email.Admin || (adminOnly && jfUser.Policy.IsAdministrator) || allowAll)
}
expiry, ok := app.storage.GetUserExpiryKey(jfUser.ID)
if ok {
user.Expiry = expiry.Expiry.Unix()
}
if tgUser, ok := app.storage.GetTelegramKey(jfUser.ID); ok {
user.Telegram = tgUser.Username
user.NotifyThroughTelegram = tgUser.Contact
}
if mxUser, ok := app.storage.GetMatrixKey(jfUser.ID); ok {
user.Matrix = mxUser.UserID
user.NotifyThroughMatrix = mxUser.Contact
}
if dcUser, ok := app.storage.GetDiscordKey(jfUser.ID); ok {
user.Discord = RenderDiscordUsername(dcUser)
// user.Discord = dcUser.Username + "#" + dcUser.Discriminator
user.DiscordID = dcUser.ID
user.NotifyThroughDiscord = dcUser.Contact
}
// FIXME: Send referral data
referrerInv := Invite{}
if referralsEnabled {
// 1. Directly attached invite.
err := app.storage.db.FindOne(&referrerInv, badgerhold.Where("ReferrerJellyfinID").Eq(jfUser.ID))
if err == nil {
user.ReferralsEnabled = true
// 2. Referrals via profile template. Shallow check, doesn't look for the thing in the database.
} else if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok && email.ReferralTemplateKey != "" {
user.ReferralsEnabled = true
}
}
resp.UserList[i] = user
i++
}
gc.JSON(200, resp)
}
// @Summary Set whether or not a user can access jfa-go. Redundant if the user is a Jellyfin admin.
// @Produce json
// @Param setAccountsAdminDTO body setAccountsAdminDTO true "Map of userIDs to whether or not they have access."
// @Success 204 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/accounts-admin [post]
// @Security Bearer
// @tags Users
func (app *appContext) SetAccountsAdmin(gc *gin.Context) {
var req setAccountsAdminDTO
gc.BindJSON(&req)
users, status, err := app.jf.GetUsers(false)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Couldn't get users", gc)
return
}
for _, jfUser := range users {
id := jfUser.ID
if admin, ok := req[id]; ok {
var emailStore = EmailAddress{}
if oldEmail, ok := app.storage.GetEmailsKey(id); ok {
emailStore = oldEmail
}
emailStore.Admin = admin
app.storage.SetEmailsKey(id, emailStore)
app.info.Printf(lm.UserAdminAdjusted, id, admin)
}
}
respondBool(204, true, gc)
}
// @Summary Modify user's labels, which show next to their name in the accounts tab.
// @Produce json
// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to labels"
// @Success 204 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/labels [post]
// @Security Bearer
// @tags Users
func (app *appContext) ModifyLabels(gc *gin.Context) {
var req modifyEmailsDTO
gc.BindJSON(&req)
users, status, err := app.jf.GetUsers(false)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Couldn't get users", gc)
return
}
for _, jfUser := range users {
id := jfUser.ID
if label, ok := req[id]; ok {
var emailStore = EmailAddress{}
if oldEmail, ok := app.storage.GetEmailsKey(id); ok {
emailStore = oldEmail
}
emailStore.Label = label
app.debug.Println(lm.UserLabelAdjusted, id, label)
app.storage.SetEmailsKey(id, emailStore)
}
}
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(lm.FailedSetEmailAddress, lm.Ombi, jfID, 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(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err)
} else if contactPrefChanged {
contactMethods := map[jellyseerr.NotificationsField]any{
jellyseerr.FieldEmailEnabled: true,
}
err := app.js.ModifyNotifications(jfID, contactMethods)
if err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
}
}
}
}
// @Summary Modify user's email addresses.
// @Produce json
// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to email addresses"
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /users/emails [post]
// @Security Bearer
// @tags Users
func (app *appContext) ModifyEmails(gc *gin.Context) {
var req modifyEmailsDTO
gc.BindJSON(&req)
users, status, err := app.jf.GetUsers(false)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
respond(500, "Couldn't get users", gc)
return
}
for _, jfUser := range users {
id := jfUser.ID
if address, ok := req[id]; ok {
app.modifyEmail(id, address)
app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId"))
activityType := ActivityContactLinked
if address == "" {
activityType = ActivityContactUnlinked
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: activityType,
UserID: id,
SourceType: ActivityAdmin,
Source: gc.GetString("jfId"),
Value: "email",
Time: time.Now(),
}, gc, false)
}
}
respondBool(200, true, gc)
}
// @Summary Apply settings to a list of users, either from a profile or from another user.
// @Produce json
// @Param userSettingsDTO body userSettingsDTO true "Parameters for applying settings"
// @Success 200 {object} errorListDTO
// @Failure 500 {object} errorListDTO "Lists of errors that occurred while applying settings"
// @Router /users/settings [post]
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) ApplySettings(gc *gin.Context) {
app.info.Println("User settings change requested")
var req userSettingsDTO
gc.BindJSON(&req)
applyingFromType := lm.Profile
applyingFromSource := "?"
var policy mediabrowser.Policy
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)
if !ok {
app.err.Printf(lm.FailedGetProfile, req.Profile)
respond(500, "Couldn't find profile", gc)
return
}
applyingFromSource = req.Profile
if req.Homescreen {
if profile.Homescreen {
configuration = profile.Configuration
displayprefs = profile.Displayprefs
} else {
req.Homescreen = false
app.err.Printf(lm.ProfileNoHomescreen, req.Profile)
respond(500, "No homescreen template available", gc)
return
}
}
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" {
applyingFromType = lm.User
app.jf.CacheExpiry = time.Now()
user, status, err := app.jf.UserByID(req.ID, false)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedGetUser, req.ID, lm.Jellyfin, err)
respond(500, "Couldn't get user", gc)
return
}
applyingFromSource = user.Name
if req.Policy {
policy = user.Policy
}
if req.Homescreen {
displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err)
respond(500, "Couldn't get displayprefs", gc)
return
}
configuration = user.Configuration
}
}
app.info.Printf(lm.ApplyingTemplatesFrom, applyingFromType, applyingFromSource, len(req.ApplyTo))
errors := errorListDTO{
"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
than 100 users are modified. A delay totalling 500ms between requests is used
if so. */
const requestDelayThreshold = 100
var shouldDelay bool = len(req.ApplyTo) >= requestDelayThreshold
if shouldDelay {
app.debug.Printf(lm.DelayingRequests, requestDelayThreshold)
}
for _, id := range req.ApplyTo {
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)
}
if req.Homescreen {
status, err = app.jf.SetConfiguration(id, configuration)
errorString := ""
if !(status == 200 || status == 204) || err != nil {
errorString += fmt.Sprintf("Configuration %d: %v ", status, err)
} else {
status, err = app.jf.SetDisplayPreferences(id, displayprefs)
if !(status == 200 || status == 204) || err != nil {
errorString += fmt.Sprintf("Displayprefs %d: %v ", status, err)
}
}
if errorString != "" {
errors["homescreen"][id] = errorString
}
}
if ombi != nil {
errorString := ""
user, status, err := app.getOmbiUser(id)
if status != 200 || err != nil {
errorString += fmt.Sprintf("Ombi GetUser %d: %v ", status, err)
} else {
// newUser := ombi
// newUser["id"] = user["id"]
// newUser["userName"] = user["userName"]
// newUser["alias"] = user["alias"]
// newUser["emailAddress"] = user["emailAddress"]
status, err = app.applyOmbiProfile(user, ombi)
if status != 200 || err != nil {
errorString += fmt.Sprintf("Apply %d: %v ", status, err)
}
}
if errorString != "" {
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)
}
}
code := 200
if len(errors["policy"]) == len(req.ApplyTo) || len(errors["homescreen"]) == len(req.ApplyTo) {
code = 500
}
gc.JSON(code, errors)
}