mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 17:10:10 +00:00
Merge Jellyseerr Support
Jellyseerr integration (similar to Ombi, but better)
This commit is contained in:
commit
efa113ab5f
100
api-jellyseerr.go
Normal file
100
api-jellyseerr.go
Normal file
@ -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)
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
@ -95,7 +96,8 @@ func (app *appContext) OmbiUsers(gc *gin.Context) {
|
|||||||
func (app *appContext) SetOmbiProfile(gc *gin.Context) {
|
func (app *appContext) SetOmbiProfile(gc *gin.Context) {
|
||||||
var req ombiUser
|
var req ombiUser
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
profileName := gc.Param("profile")
|
escapedProfileName := gc.Param("profile")
|
||||||
|
profileName, _ := url.QueryUnescape(escapedProfileName)
|
||||||
profile, ok := app.storage.GetProfileKey(profileName)
|
profile, ok := app.storage.GetProfileKey(profileName)
|
||||||
if !ok {
|
if !ok {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
@ -122,7 +124,8 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Ombi
|
// @tags Ombi
|
||||||
func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
|
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)
|
profile, ok := app.storage.GetProfileKey(profileName)
|
||||||
if !ok {
|
if !ok {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
|
@ -27,6 +27,7 @@ func (app *appContext) GetProfiles(gc *gin.Context) {
|
|||||||
LibraryAccess: p.LibraryAccess,
|
LibraryAccess: p.LibraryAccess,
|
||||||
FromUser: p.FromUser,
|
FromUser: p.FromUser,
|
||||||
Ombi: p.Ombi != nil,
|
Ombi: p.Ombi != nil,
|
||||||
|
Jellyseerr: p.Jellyseerr.Enabled,
|
||||||
ReferralsEnabled: false,
|
ReferralsEnabled: false,
|
||||||
}
|
}
|
||||||
if referralsEnabled {
|
if referralsEnabled {
|
||||||
|
@ -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"),
|
||||||
|
176
api-users.go
176
api-users.go
@ -9,6 +9,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/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
@ -94,6 +95,29 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
|||||||
app.info.Println("Created Ombi user")
|
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 != "" {
|
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)
|
app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email)
|
||||||
msg, err := app.email.constructWelcome(req.Username, time.Time{}, app, false)
|
msg, err := app.email.constructWelcome(req.Username, time.Time{}, app, false)
|
||||||
@ -338,6 +362,10 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
|
|||||||
Addr: req.Email,
|
Addr: req.Email,
|
||||||
Contact: (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 != "" {
|
if invite.UserLabel != "" {
|
||||||
emailStore.Label = invite.UserLabel
|
emailStore.Label = invite.UserLabel
|
||||||
@ -468,6 +496,51 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
|
|||||||
app.debug.Printf("Skipping Ombi: Profile \"%s\" was empty", invite.Profile)
|
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.ModifyMainUserSettings(id, jellyseerr.MainUserSettings{Email: 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] = u.ChatID
|
||||||
|
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 {
|
if matrixVerified {
|
||||||
matrixUser.Contact = req.MatrixContact
|
matrixUser.Contact = req.MatrixContact
|
||||||
delete(app.matrix.tokens, req.MatrixPIN)
|
delete(app.matrix.tokens, req.MatrixPIN)
|
||||||
@ -1184,6 +1257,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"
|
||||||
@ -1202,22 +1313,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 == "" {
|
||||||
@ -1231,17 +1330,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")
|
||||||
@ -1265,6 +1353,8 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
var configuration mediabrowser.Configuration
|
var configuration mediabrowser.Configuration
|
||||||
var displayprefs map[string]interface{}
|
var displayprefs map[string]interface{}
|
||||||
var ombi map[string]interface{}
|
var ombi map[string]interface{}
|
||||||
|
var jellyseerr JellyseerrTemplate
|
||||||
|
jellyseerr.Enabled = false
|
||||||
if req.From == "profile" {
|
if req.From == "profile" {
|
||||||
// Check profile exists & isn't empty
|
// Check profile exists & isn't empty
|
||||||
profile, ok := app.storage.GetProfileKey(req.Profile)
|
profile, ok := app.storage.GetProfileKey(req.Profile)
|
||||||
@ -1282,12 +1372,19 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
configuration = profile.Configuration
|
configuration = profile.Configuration
|
||||||
displayprefs = profile.Displayprefs
|
displayprefs = profile.Displayprefs
|
||||||
}
|
}
|
||||||
|
if req.Policy {
|
||||||
policy = profile.Policy
|
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 {
|
if profile.Ombi != nil && len(profile.Ombi) != 0 {
|
||||||
ombi = profile.Ombi
|
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" {
|
} else if req.From == "user" {
|
||||||
applyingFrom = "user"
|
applyingFrom = "user"
|
||||||
@ -1299,7 +1396,9 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
applyingFrom = "\"" + user.Name + "\""
|
applyingFrom = "\"" + user.Name + "\""
|
||||||
|
if req.Policy {
|
||||||
policy = user.Policy
|
policy = user.Policy
|
||||||
|
}
|
||||||
if req.Homescreen {
|
if req.Homescreen {
|
||||||
displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
|
displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
@ -1315,6 +1414,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
"policy": map[string]string{},
|
"policy": map[string]string{},
|
||||||
"homescreen": map[string]string{},
|
"homescreen": map[string]string{},
|
||||||
"ombi": 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
|
/* 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
|
and can crash and mess up its database. Issue #160 says this occurs when more
|
||||||
@ -1325,10 +1425,14 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
app.debug.Println("Adding delay between requests for large batch")
|
app.debug.Println("Adding delay between requests for large batch")
|
||||||
}
|
}
|
||||||
for _, id := range req.ApplyTo {
|
for _, id := range req.ApplyTo {
|
||||||
status, err := app.jf.SetPolicy(id, policy)
|
var status int
|
||||||
|
var err error
|
||||||
|
if req.Policy {
|
||||||
|
status, err = app.jf.SetPolicy(id, policy)
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
|
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if shouldDelay {
|
if shouldDelay {
|
||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(250 * time.Millisecond)
|
||||||
}
|
}
|
||||||
@ -1367,6 +1471,26 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
|||||||
errors["ombi"][id] = 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 {
|
if shouldDelay {
|
||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(250 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
15
backups.go
15
backups.go
@ -161,20 +161,13 @@ func (app *appContext) loadPendingBackup() {
|
|||||||
LOADBAK = ""
|
LOADBAK = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBackupDaemon(app *appContext) *housekeepingDaemon {
|
func newBackupDaemon(app *appContext) *GenericDaemon {
|
||||||
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
|
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
|
||||||
daemon := housekeepingDaemon{
|
d := NewGenericDaemon(interval, app,
|
||||||
Stopped: false,
|
|
||||||
ShutdownChannel: make(chan string),
|
|
||||||
Interval: interval,
|
|
||||||
period: interval,
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
daemon.jobs = []func(app *appContext){
|
|
||||||
func(app *appContext) {
|
func(app *appContext) {
|
||||||
app.debug.Println("Backups: Creating backup")
|
app.debug.Println("Backups: Creating backup")
|
||||||
app.makeBackup()
|
app.makeBackup()
|
||||||
},
|
},
|
||||||
}
|
)
|
||||||
return &daemon
|
return d
|
||||||
}
|
}
|
||||||
|
@ -1580,6 +1580,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"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,
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"import_existing": {
|
||||||
|
"name": "Import existing users to Jellyseerr",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": true,
|
||||||
|
"type": "bool",
|
||||||
|
"value": false,
|
||||||
|
"depends_true": "enabled",
|
||||||
|
"description": "Existing users (and those created outside jfa-go) will have their contact info imported to Jellyseerr."
|
||||||
|
},
|
||||||
|
"constraints_note": {
|
||||||
|
"name": "Unique Emails:",
|
||||||
|
"type": "note",
|
||||||
|
"value": "",
|
||||||
|
"depends_true": "import_existing",
|
||||||
|
"required": "false",
|
||||||
|
"description": "Jellyseerr requires email addresses to be unique. If this is not the case, you may see errors in jfa-go's logs. You can require unique addresses in Settings > Email."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"backups": {
|
"backups": {
|
||||||
"order": [],
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
|
69
daemon.go
69
daemon.go
@ -116,32 +116,16 @@ func (app *appContext) clearActivities() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||||
|
d := NewGenericDaemon(interval, app,
|
||||||
type housekeepingDaemon struct {
|
|
||||||
Stopped bool
|
|
||||||
ShutdownChannel chan string
|
|
||||||
Interval time.Duration
|
|
||||||
period time.Duration
|
|
||||||
jobs []func(app *appContext)
|
|
||||||
app *appContext
|
|
||||||
}
|
|
||||||
|
|
||||||
func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemon {
|
|
||||||
daemon := housekeepingDaemon{
|
|
||||||
Stopped: false,
|
|
||||||
ShutdownChannel: make(chan string),
|
|
||||||
Interval: interval,
|
|
||||||
period: interval,
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
daemon.jobs = []func(app *appContext){
|
|
||||||
func(app *appContext) {
|
func(app *appContext) {
|
||||||
app.debug.Println("Housekeeping: Checking for expired invites")
|
app.debug.Println("Housekeeping: Checking for expired invites")
|
||||||
app.checkInvites()
|
app.checkInvites()
|
||||||
},
|
},
|
||||||
func(app *appContext) { app.clearActivities() },
|
func(app *appContext) { app.clearActivities() },
|
||||||
}
|
)
|
||||||
|
|
||||||
|
d.Name("Housekeeping daemon")
|
||||||
|
|
||||||
clearEmail := app.config.Section("email").Key("require_unique").MustBool(false)
|
clearEmail := app.config.Section("email").Key("require_unique").MustBool(false)
|
||||||
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
|
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
|
||||||
@ -150,53 +134,24 @@ func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemo
|
|||||||
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
||||||
|
|
||||||
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
|
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
d.appendJobs(func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
||||||
}
|
}
|
||||||
|
|
||||||
if clearEmail {
|
if clearEmail {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearEmails() })
|
d.appendJobs(func(app *appContext) { app.clearEmails() })
|
||||||
}
|
}
|
||||||
if clearDiscord {
|
if clearDiscord {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearDiscord() })
|
d.appendJobs(func(app *appContext) { app.clearDiscord() })
|
||||||
}
|
}
|
||||||
if clearTelegram {
|
if clearTelegram {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearTelegram() })
|
d.appendJobs(func(app *appContext) { app.clearTelegram() })
|
||||||
}
|
}
|
||||||
if clearMatrix {
|
if clearMatrix {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() })
|
d.appendJobs(func(app *appContext) { app.clearMatrix() })
|
||||||
}
|
}
|
||||||
if clearPWR {
|
if clearPWR {
|
||||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearPWRCaptchas() })
|
d.appendJobs(func(app *appContext) { app.clearPWRCaptchas() })
|
||||||
}
|
}
|
||||||
|
|
||||||
return &daemon
|
return d
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *housekeepingDaemon) run() {
|
|
||||||
rt.app.info.Println("Invite daemon started")
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-rt.ShutdownChannel:
|
|
||||||
rt.ShutdownChannel <- "Down"
|
|
||||||
return
|
|
||||||
case <-time.After(rt.period):
|
|
||||||
break
|
|
||||||
}
|
|
||||||
started := time.Now()
|
|
||||||
|
|
||||||
for _, job := range rt.jobs {
|
|
||||||
job(rt.app)
|
|
||||||
}
|
|
||||||
|
|
||||||
finished := time.Now()
|
|
||||||
duration := finished.Sub(started)
|
|
||||||
rt.period = rt.Interval - duration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *housekeepingDaemon) Shutdown() {
|
|
||||||
rt.Stopped = true
|
|
||||||
rt.ShutdownChannel <- "Down"
|
|
||||||
<-rt.ShutdownChannel
|
|
||||||
close(rt.ShutdownChannel)
|
|
||||||
}
|
}
|
||||||
|
65
genericdaemon.go
Normal file
65
genericdaemon.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
||||||
|
|
||||||
|
type GenericDaemon struct {
|
||||||
|
Stopped bool
|
||||||
|
ShutdownChannel chan string
|
||||||
|
Interval time.Duration
|
||||||
|
period time.Duration
|
||||||
|
jobs []func(app *appContext)
|
||||||
|
app *appContext
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GenericDaemon) appendJobs(jobs ...func(app *appContext)) {
|
||||||
|
d.jobs = append(d.jobs, jobs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGenericDaemon returns a daemon which can be given jobs that utilize appContext.
|
||||||
|
func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app *appContext)) *GenericDaemon {
|
||||||
|
d := GenericDaemon{
|
||||||
|
Stopped: false,
|
||||||
|
ShutdownChannel: make(chan string),
|
||||||
|
Interval: interval,
|
||||||
|
period: interval,
|
||||||
|
app: app,
|
||||||
|
name: "Generic Daemon",
|
||||||
|
}
|
||||||
|
d.jobs = jobs
|
||||||
|
return &d
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GenericDaemon) Name(name string) { d.name = name }
|
||||||
|
|
||||||
|
func (d *GenericDaemon) run() {
|
||||||
|
d.app.info.Printf("%s started", d.name)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-d.ShutdownChannel:
|
||||||
|
d.ShutdownChannel <- "Down"
|
||||||
|
return
|
||||||
|
case <-time.After(d.period):
|
||||||
|
break
|
||||||
|
}
|
||||||
|
started := time.Now()
|
||||||
|
|
||||||
|
for _, job := range d.jobs {
|
||||||
|
job(d.app)
|
||||||
|
}
|
||||||
|
|
||||||
|
finished := time.Now()
|
||||||
|
duration := finished.Sub(started)
|
||||||
|
d.period = d.Interval - duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *GenericDaemon) Shutdown() {
|
||||||
|
d.Stopped = true
|
||||||
|
d.ShutdownChannel <- "Down"
|
||||||
|
<-d.ShutdownChannel
|
||||||
|
close(d.ShutdownChannel)
|
||||||
|
}
|
5
go.mod
5
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/easyproxy => ./easyproxy
|
||||||
|
|
||||||
|
replace github.com/hrfee/jfa-go/jellyseerr => ./jellyseerr
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bwmarrin/discordgo v0.27.1
|
github.com/bwmarrin/discordgo v0.27.1
|
||||||
github.com/dgraph-io/badger/v3 v3.2103.5
|
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/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
|
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/docs v0.0.0-20230626224816-f72960635dc3
|
||||||
github.com/hrfee/jfa-go/easyproxy v0.0.0-00010101000000-000000000000
|
github.com/hrfee/jfa-go/easyproxy v0.0.0-00010101000000-000000000000
|
||||||
github.com/hrfee/jfa-go/linecache v0.0.0-20230626224816-f72960635dc3
|
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/google/uuid v1.3.0 // indirect
|
||||||
github.com/gorilla/mux v1.8.0 // indirect
|
github.com/gorilla/mux v1.8.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.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/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.16.6 // indirect
|
github.com/klauspost/compress v1.16.6 // indirect
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
window.discordEnabled = {{ .discordEnabled }};
|
window.discordEnabled = {{ .discordEnabled }};
|
||||||
window.matrixEnabled = {{ .matrixEnabled }};
|
window.matrixEnabled = {{ .matrixEnabled }};
|
||||||
window.ombiEnabled = {{ .ombiEnabled }};
|
window.ombiEnabled = {{ .ombiEnabled }};
|
||||||
|
window.jellyseerrEnabled = {{ .jellyseerrEnabled }};
|
||||||
window.usernameEnabled = {{ .username }};
|
window.usernameEnabled = {{ .username }};
|
||||||
window.langFile = JSON.parse({{ .language }});
|
window.langFile = JSON.parse({{ .language }});
|
||||||
window.linkResetEnabled = {{ .linkResetEnabled }};
|
window.linkResetEnabled = {{ .linkResetEnabled }};
|
||||||
@ -83,30 +84,44 @@
|
|||||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-modify-user" href="">
|
<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">×</span></span>
|
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">×</span></span>
|
||||||
<p class="content my-4">{{ .strings.modifySettingsDescription }}</p>
|
<p class="content my-4">{{ .strings.modifySettingsDescription }}</p>
|
||||||
<div class="flex flex-row mb-4">
|
<div class="flex flex-col gap-4 my-2">
|
||||||
<label class="grow mr-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>
|
<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>
|
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="grow ml-2">
|
<label class="grow">
|
||||||
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-user">
|
<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>
|
<span class="button ~neutral @low supra full-width center">{{ .strings.user }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="select ~neutral @low mb-4">
|
<div class="select ~neutral @low">
|
||||||
<select id="modify-user-profiles"></select>
|
<select id="modify-user-profiles"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="select ~neutral @low mb-4 unfocused">
|
<div class="select ~neutral @low unfocused">
|
||||||
<select id="modify-user-users"></select>
|
<select id="modify-user-users"></select>
|
||||||
</div>
|
</div>
|
||||||
<label class="switch mb-4">
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="modify-user-configuration" checked>
|
||||||
|
<span>{{ .strings.applyConfigurationAndPolicy }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="switch">
|
||||||
<input type="checkbox" id="modify-user-homescreen" checked>
|
<input type="checkbox" id="modify-user-homescreen" checked>
|
||||||
<span>{{ .strings.applyHomescreenLayout }}</span>
|
<span>{{ .strings.applyHomescreenLayout }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
<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>
|
<label>
|
||||||
<input type="submit" class="unfocused">
|
<input type="submit" class="unfocused">
|
||||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.apply }}</span>
|
<span class="button ~urge @low full-width center supra submit">{{ .strings.apply }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{{ if .referralsEnabled }}
|
{{ if .referralsEnabled }}
|
||||||
@ -396,6 +411,19 @@
|
|||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="modal-jellyseerr-profile" class="modal">
|
||||||
|
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-jellyseerr-defaults" href="">
|
||||||
|
<span class="heading">{{ .strings.jellyseerrProfile }} <span class="modal-close">×</span></span>
|
||||||
|
<p class="content my-4">{{ .strings.jellyseerrUserDefaultsDescription }}</p>
|
||||||
|
<div class="select ~neutral @low mb-4">
|
||||||
|
<select></select>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
<input type="submit" class="unfocused">
|
||||||
|
<span class="button ~urge @low full-width center supra submit">{{ .strings.submit }}</span>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
<div id="modal-user-profiles" class="modal">
|
<div id="modal-user-profiles" class="modal">
|
||||||
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-2/3 content card">
|
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-2/3 content card">
|
||||||
<span class="heading">{{ .strings.userProfiles }} <span class="modal-close">×</span></span>
|
<span class="heading">{{ .strings.userProfiles }} <span class="modal-close">×</span></span>
|
||||||
@ -409,6 +437,9 @@
|
|||||||
{{ if .ombiEnabled }}
|
{{ if .ombiEnabled }}
|
||||||
<th>Ombi</th>
|
<th>Ombi</th>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .jellyseerrEnabled }}
|
||||||
|
<th>Jellyseerr</th>
|
||||||
|
{{ end }}
|
||||||
{{ if .referralsEnabled }}
|
{{ if .referralsEnabled }}
|
||||||
<th>{{ .strings.referrals }}</th>
|
<th>{{ .strings.referrals }}</th>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
7
jellyseerr/go.mod
Normal file
7
jellyseerr/go.mod
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module github.com/hrfee/jfa-go/jellyseerr
|
||||||
|
|
||||||
|
replace github.com/hrfee/jfa-go/common => ../common
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769 // indirect
|
460
jellyseerr/jellyseerr.go
Normal file
460
jellyseerr/jellyseerr.go
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
package jellyseerr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hrfee/jfa-go/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
API_SUFFIX = "/api/v1"
|
||||||
|
BogusIdentifier = "123412341234123456"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Jellyseerr represents a running Jellyseerr instance.
|
||||||
|
type Jellyseerr struct {
|
||||||
|
server, key string
|
||||||
|
header map[string]string
|
||||||
|
httpClient *http.Client
|
||||||
|
userCache map[string]User // Map of jellyfin IDs to users
|
||||||
|
cacheExpiry time.Time
|
||||||
|
cacheLength time.Duration
|
||||||
|
timeoutHandler common.TimeoutHandler
|
||||||
|
LogRequestBodies bool
|
||||||
|
AutoImportUsers bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJellyseerr returns an Ombi object.
|
||||||
|
func NewJellyseerr(server, key string, timeoutHandler common.TimeoutHandler) *Jellyseerr {
|
||||||
|
if !strings.HasSuffix(server, API_SUFFIX) {
|
||||||
|
server = server + API_SUFFIX
|
||||||
|
}
|
||||||
|
return &Jellyseerr{
|
||||||
|
server: server,
|
||||||
|
key: key,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
header: map[string]string{
|
||||||
|
"X-Api-Key": key,
|
||||||
|
},
|
||||||
|
cacheLength: time.Duration(30) * time.Minute,
|
||||||
|
cacheExpiry: time.Now(),
|
||||||
|
timeoutHandler: timeoutHandler,
|
||||||
|
userCache: map[string]User{},
|
||||||
|
LogRequestBodies: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) req(mode string, uri string, data any, queryParams url.Values, headers map[string]string, response bool) (string, int, error) {
|
||||||
|
var params []byte
|
||||||
|
if data != nil {
|
||||||
|
params, _ = json.Marshal(data)
|
||||||
|
}
|
||||||
|
if js.LogRequestBodies {
|
||||||
|
fmt.Printf("Jellyseerr API Client: Sending Data \"%s\" to \"%s\"\n", string(params), uri)
|
||||||
|
}
|
||||||
|
if qp := queryParams.Encode(); qp != "" {
|
||||||
|
uri += "?" + qp
|
||||||
|
}
|
||||||
|
var req *http.Request
|
||||||
|
if data != nil {
|
||||||
|
req, _ = http.NewRequest(mode, uri, bytes.NewBuffer(params))
|
||||||
|
} else {
|
||||||
|
req, _ = http.NewRequest(mode, uri, nil)
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
for name, value := range js.header {
|
||||||
|
req.Header.Add(name, value)
|
||||||
|
}
|
||||||
|
if headers != nil {
|
||||||
|
for name, value := range headers {
|
||||||
|
req.Header.Add(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp, err := js.httpClient.Do(req)
|
||||||
|
reqFailed := err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201)
|
||||||
|
defer js.timeoutHandler()
|
||||||
|
var responseText string
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if response || reqFailed {
|
||||||
|
responseText, err = js.decodeResp(resp)
|
||||||
|
if err != nil {
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reqFailed {
|
||||||
|
var msg ErrorDTO
|
||||||
|
err = json.Unmarshal([]byte(responseText), &msg)
|
||||||
|
if err != nil {
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
if msg.Message == "" {
|
||||||
|
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("got %d: %s", resp.StatusCode, msg.Message)
|
||||||
|
}
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) decodeResp(resp *http.Response) (string, error) {
|
||||||
|
var out io.Reader
|
||||||
|
switch resp.Header.Get("Content-Encoding") {
|
||||||
|
case "gzip":
|
||||||
|
out, _ = gzip.NewReader(resp.Body)
|
||||||
|
default:
|
||||||
|
out = resp.Body
|
||||||
|
}
|
||||||
|
buf := new(strings.Builder)
|
||||||
|
_, err := io.Copy(buf, out)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) get(uri string, data any, params url.Values) (string, int, error) {
|
||||||
|
return js.req(http.MethodGet, uri, data, params, nil, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) post(uri string, data any, response bool) (string, int, error) {
|
||||||
|
return js.req(http.MethodPost, uri, data, url.Values{}, nil, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) put(uri string, data any, response bool) (string, int, error) {
|
||||||
|
return js.req(http.MethodPut, uri, data, url.Values{}, nil, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) delete(uri string, data any) (int, error) {
|
||||||
|
_, status, err := js.req(http.MethodDelete, uri, data, url.Values{}, nil, false)
|
||||||
|
return status, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) ImportFromJellyfin(jfIDs ...string) ([]User, error) {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"jellyfinUserIds": jfIDs,
|
||||||
|
}
|
||||||
|
resp, status, err := js.post(js.server+"/user/import-from-jellyfin", params, true)
|
||||||
|
var data []User
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
|
for _, u := range data {
|
||||||
|
if u.JellyfinUserID != "" {
|
||||||
|
js.userCache[u.JellyfinUserID] = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) getUsers() error {
|
||||||
|
if js.cacheExpiry.After(time.Now()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
js.cacheExpiry = time.Now().Add(js.cacheLength)
|
||||||
|
pageCount := 1
|
||||||
|
pageIndex := 0
|
||||||
|
for {
|
||||||
|
res, err := js.getUserPage(pageIndex)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, u := range res.Results {
|
||||||
|
if u.JellyfinUserID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
js.userCache[u.JellyfinUserID] = u
|
||||||
|
}
|
||||||
|
pageCount = res.Page.Pages
|
||||||
|
pageIndex++
|
||||||
|
if pageIndex >= pageCount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("take", "30")
|
||||||
|
params.Add("skip", strconv.Itoa(page*30))
|
||||||
|
params.Add("sort", "created")
|
||||||
|
if js.LogRequestBodies {
|
||||||
|
fmt.Printf("Jellyseerr API Client: Sending with URL params \"%+v\"\n", params)
|
||||||
|
}
|
||||||
|
resp, status, err := js.get(js.server+"/user", nil, params)
|
||||||
|
var data GetUsersDTO
|
||||||
|
if status != 200 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) MustGetUser(jfID string) (User, error) {
|
||||||
|
u, _, err := js.GetOrImportUser(jfID)
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImportedUser provides the same function as ImportFromJellyfin, but will always return the user,
|
||||||
|
// even if they already existed. Also returns whether the user was imported or not,
|
||||||
|
func (js *Jellyseerr) GetOrImportUser(jfID string) (u User, imported bool, err error) {
|
||||||
|
imported = false
|
||||||
|
u, err = js.GetExistingUser(jfID)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var users []User
|
||||||
|
users, err = js.ImportFromJellyfin(jfID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(users) != 0 {
|
||||||
|
u = users[0]
|
||||||
|
err = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("user not found or imported")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) GetExistingUser(jfID string) (u User, err error) {
|
||||||
|
js.getUsers()
|
||||||
|
ok := false
|
||||||
|
err = nil
|
||||||
|
if u, ok = js.userCache[jfID]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
js.cacheExpiry = time.Now()
|
||||||
|
js.getUsers()
|
||||||
|
if u, ok = js.userCache[jfID]; ok {
|
||||||
|
err = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) getUser(jfID string) (User, error) {
|
||||||
|
if js.AutoImportUsers {
|
||||||
|
return js.MustGetUser(jfID)
|
||||||
|
}
|
||||||
|
return js.GetExistingUser(jfID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) Me() (User, error) {
|
||||||
|
resp, status, err := js.get(js.server+"/auth/me", nil, url.Values{})
|
||||||
|
var data User
|
||||||
|
data.ID = -1
|
||||||
|
if status != 200 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
|
||||||
|
data := permissionsDTO{Permissions: -1}
|
||||||
|
u, err := js.getUser(jfID)
|
||||||
|
if err != nil {
|
||||||
|
return data.Permissions, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
|
||||||
|
if err != nil {
|
||||||
|
return data.Permissions, err
|
||||||
|
}
|
||||||
|
if status != 200 {
|
||||||
|
return data.Permissions, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
|
return data.Permissions, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
|
||||||
|
u, err := js.getUser(jfID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), permissionsDTO{Permissions: perm}, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
u.Permissions = perm
|
||||||
|
js.userCache[jfID] = u
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error {
|
||||||
|
u, err := js.getUser(jfID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), tmpl, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
u.UserTemplate = tmpl
|
||||||
|
js.userCache[jfID] = u
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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.getUser(jfID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d", 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) DeleteUser(jfID string) error {
|
||||||
|
u, err := js.getUser(jfID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := js.delete(fmt.Sprintf(js.server+"/user/%d", u.ID), nil)
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(js.userCache, jfID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, error) {
|
||||||
|
u, err := js.getUser(jfID)
|
||||||
|
if err != nil {
|
||||||
|
return Notifications{}, err
|
||||||
|
}
|
||||||
|
return js.GetNotificationPreferencesByID(u.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) GetNotificationPreferencesByID(jellyseerrID int64) (Notifications, error) {
|
||||||
|
var data Notifications
|
||||||
|
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
if status != 200 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl NotificationsTemplate) error {
|
||||||
|
// This behaviour is not desired, this being all-zero means no notifications, which is a settings state we'd want to store!
|
||||||
|
/* if tmpl.NotifTypes.Empty() {
|
||||||
|
tmpl.NotifTypes = nil
|
||||||
|
}*/
|
||||||
|
u, err := js.getUser(jfID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), tmpl, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsField]any) error {
|
||||||
|
u, err := js.getUser(jfID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), conf, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
return fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) GetUsers() (map[string]User, error) {
|
||||||
|
err := js.getUsers()
|
||||||
|
return js.userCache, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
|
||||||
|
resp, status, err := js.get(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
|
||||||
|
var data User
|
||||||
|
if status != 200 {
|
||||||
|
return data, fmt.Errorf("failed (error %d)", status)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(resp), &data)
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (js *Jellyseerr) ModifyMainUserSettings(jfID string, conf MainUserSettings) error {
|
||||||
|
u, err := js.getUser(jfID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, status, err := js.post(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
|
||||||
|
}
|
69
jellyseerr/jellyseerr_test.go
Normal file
69
jellyseerr/jellyseerr_test.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package jellyseerr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hrfee/jfa-go/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
API_KEY = "MTcyMjI2MDM2MTYyMzMxNDZkZmYyLTE4MzMtNDUyNy1hODJlLTI0MTZkZGUyMDg2Ng=="
|
||||||
|
URI = "http://localhost:5055"
|
||||||
|
PERM = 2097184
|
||||||
|
)
|
||||||
|
|
||||||
|
func client() *Jellyseerr {
|
||||||
|
return NewJellyseerr(URI, API_KEY, common.NewTimeoutHandler("Jellyseerr", URI, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMe(t *testing.T) {
|
||||||
|
js := client()
|
||||||
|
u, err := js.Me()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("returned error %+v", err)
|
||||||
|
}
|
||||||
|
if u.ID < 0 {
|
||||||
|
t.Fatalf("returned no user %+v\n", u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* func TestImportFromJellyfin(t *testing.T) {
|
||||||
|
js := client()
|
||||||
|
list, err := js.ImportFromJellyfin("6b75e189efb744f583aa2e8e9cee41d3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("returned error %+v", err)
|
||||||
|
}
|
||||||
|
if len(list) == 0 {
|
||||||
|
t.Fatalf("returned no users")
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
func TestMustGetUser(t *testing.T) {
|
||||||
|
js := client()
|
||||||
|
u, err := js.MustGetUser("8c9d25c070d641cd8ad9cf825f622a16")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("returned error %+v", err)
|
||||||
|
}
|
||||||
|
if u.ID < 0 {
|
||||||
|
t.Fatalf("returned no users")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetPermissions(t *testing.T) {
|
||||||
|
js := client()
|
||||||
|
err := js.SetPermissions("6b75e189efb744f583aa2e8e9cee41d3", PERM)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("returned error %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPermissions(t *testing.T) {
|
||||||
|
js := client()
|
||||||
|
perm, err := js.GetPermissions("6b75e189efb744f583aa2e8e9cee41d3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("returned error %+v", err)
|
||||||
|
}
|
||||||
|
if perm != PERM {
|
||||||
|
t.Fatalf("got unexpected perm code %d", perm)
|
||||||
|
}
|
||||||
|
}
|
136
jellyseerr/models.go
Normal file
136
jellyseerr/models.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package jellyseerr
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type UserField string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FieldDisplayName UserField = "displayName"
|
||||||
|
FieldEmail UserField = "email"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
UserTemplate // Note: You can set this with User.UserTemplate = value.
|
||||||
|
UserType int64 `json:"userType,omitempty"`
|
||||||
|
Warnings []any `json:"warnings,omitempty"`
|
||||||
|
ID int64 `json:"id,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
PlexUsername string `json:"plexUsername,omitempty"`
|
||||||
|
JellyfinUsername string `json:"jellyfinUsername,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
RecoveryLinkExpirationDate any `json:"recoveryLinkExpirationDate,omitempty"`
|
||||||
|
PlexID string `json:"plexId,omitempty"`
|
||||||
|
JellyfinUserID string `json:"jellyfinUserId,omitempty"`
|
||||||
|
JellyfinDeviceID string `json:"jellyfinDeviceId,omitempty"`
|
||||||
|
JellyfinAuthToken string `json:"jellyfinAuthToken,omitempty"`
|
||||||
|
PlexToken string `json:"plexToken,omitempty"`
|
||||||
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||||
|
RequestCount int64 `json:"requestCount,omitempty"`
|
||||||
|
DisplayName string `json:"displayName,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u User) Name() string {
|
||||||
|
var n string
|
||||||
|
if u.Username != "" {
|
||||||
|
n = u.Username
|
||||||
|
} else if u.JellyfinUsername != "" {
|
||||||
|
n = u.JellyfinUsername
|
||||||
|
}
|
||||||
|
if u.DisplayName != "" {
|
||||||
|
n += " (" + u.DisplayName + ")"
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserTemplate struct {
|
||||||
|
Permissions Permissions `json:"permissions,omitempty"`
|
||||||
|
MovieQuotaLimit any `json:"movieQuotaLimit,omitempty"`
|
||||||
|
MovieQuotaDays any `json:"movieQuotaDays,omitempty"`
|
||||||
|
TvQuotaLimit any `json:"tvQuotaLimit,omitempty"`
|
||||||
|
TvQuotaDays any `json:"tvQuotaDays,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageInfo struct {
|
||||||
|
Pages int `json:"pages,omitempty"`
|
||||||
|
PageSize int `json:"pageSize,omitempty"`
|
||||||
|
Results int `json:"results,omitempty"`
|
||||||
|
Page int `json:"page,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetUsersDTO struct {
|
||||||
|
Page PageInfo `json:"pageInfo,omitempty"`
|
||||||
|
Results []User `json:"results,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type permissionsDTO struct {
|
||||||
|
Permissions Permissions `json:"permissions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Permissions int
|
||||||
|
|
||||||
|
type NotificationTypes struct {
|
||||||
|
Discord int64 `json:"discord"`
|
||||||
|
Email int64 `json:"email"`
|
||||||
|
Pushbullet int64 `json:"pushbullet"`
|
||||||
|
Pushover int64 `json:"pushover"`
|
||||||
|
Slack int64 `json:"slack"`
|
||||||
|
Telegram int64 `json:"telegram"`
|
||||||
|
Webhook int64 `json:"webhook"`
|
||||||
|
Webpush int64 `json:"webpush"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* func (nt *NotificationTypes) Empty() bool {
|
||||||
|
return nt.Discord == 0 && nt.Email == 0 && nt.Pushbullet == 0 && nt.Pushover == 0 && nt.Slack == 0 && nt.Telegram == 0 && nt.Webhook == 0 && nt.Webpush == 0
|
||||||
|
} */
|
||||||
|
|
||||||
|
type NotificationsField string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FieldDiscord NotificationsField = "discordId"
|
||||||
|
FieldTelegram NotificationsField = "telegramChatId"
|
||||||
|
FieldEmailEnabled NotificationsField = "emailEnabled"
|
||||||
|
FieldDiscordEnabled NotificationsField = "discordEnabled"
|
||||||
|
FieldTelegramEnabled NotificationsField = "telegramEnabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Notifications struct {
|
||||||
|
NotificationsTemplate
|
||||||
|
PgpKey any `json:"pgpKey,omitempty"`
|
||||||
|
DiscordID string `json:"discordId,omitempty"`
|
||||||
|
PushbulletAccessToken any `json:"pushbulletAccessToken,omitempty"`
|
||||||
|
PushoverApplicationToken any `json:"pushoverApplicationToken,omitempty"`
|
||||||
|
PushoverUserKey any `json:"pushoverUserKey,omitempty"`
|
||||||
|
TelegramChatID string `json:"telegramChatId,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationsTemplate struct {
|
||||||
|
EmailEnabled bool `json:"emailEnabled,omitempty"`
|
||||||
|
DiscordEnabled bool `json:"discordEnabled,omitempty"`
|
||||||
|
DiscordEnabledTypes int64 `json:"discordEnabledTypes,omitempty"`
|
||||||
|
PushoverSound any `json:"pushoverSound,omitempty"`
|
||||||
|
TelegramEnabled bool `json:"telegramEnabled,omitempty"`
|
||||||
|
TelegramSendSilently any `json:"telegramSendSilently,omitempty"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorDTO struct {
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
81
jellyseerrdaemon.go
Normal file
81
jellyseerrdaemon.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||||
|
user, imported, err := app.js.GetOrImportUser(jfID)
|
||||||
|
if err != nil {
|
||||||
|
app.debug.Printf("Failed to get or trigger import for Jellyseerr (user \"%s\"): %v", jfID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if imported {
|
||||||
|
app.debug.Printf("Jellyseerr: Triggered import for Jellyfin user \"%s\" (ID %d)", jfID, user.ID)
|
||||||
|
}
|
||||||
|
notif, err := app.js.GetNotificationPreferencesByID(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
app.debug.Printf("Failed to get notification prefs for Jellyseerr (user \"%s\"): %v", jfID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contactMethods := map[jellyseerr.NotificationsField]any{}
|
||||||
|
email, ok := app.storage.GetEmailsKey(jfID)
|
||||||
|
if ok && email.Addr != "" && user.Email != email.Addr {
|
||||||
|
err = app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: email.Addr})
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
|
||||||
|
} else {
|
||||||
|
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if discordEnabled {
|
||||||
|
dcUser, ok := app.storage.GetDiscordKey(jfID)
|
||||||
|
if ok && dcUser.ID != "" && notif.DiscordID != dcUser.ID {
|
||||||
|
contactMethods[jellyseerr.FieldDiscord] = dcUser.ID
|
||||||
|
contactMethods[jellyseerr.FieldDiscordEnabled] = dcUser.Contact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if telegramEnabled {
|
||||||
|
tgUser, ok := app.storage.GetTelegramKey(jfID)
|
||||||
|
chatID, _ := strconv.ParseInt(notif.TelegramChatID, 10, 64)
|
||||||
|
if ok && tgUser.ChatID != 0 && chatID != tgUser.ChatID {
|
||||||
|
u, _ := app.storage.GetTelegramKey(jfID)
|
||||||
|
contactMethods[jellyseerr.FieldTelegram] = u.ChatID
|
||||||
|
contactMethods[jellyseerr.FieldTelegramEnabled] = tgUser.Contact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(contactMethods) != 0 {
|
||||||
|
err := app.js.ModifyNotifications(jfID, contactMethods)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) SynchronizeJellyseerrUsers() {
|
||||||
|
users, status, err := app.jf.GetUsers(false)
|
||||||
|
if err != nil || status != 200 {
|
||||||
|
app.err.Printf("Failed to get users (%d): %s", status, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// I'm sure Jellyseerr can handle it,
|
||||||
|
// but past issues with the Jellyfin db scare me from
|
||||||
|
// running these concurrently. W/e, its a bg task anyway.
|
||||||
|
for _, user := range users {
|
||||||
|
app.SynchronizeJellyseerrUser(user.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newJellyseerrDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||||
|
d := NewGenericDaemon(interval, app,
|
||||||
|
func(app *appContext) {
|
||||||
|
app.SynchronizeJellyseerrUsers()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
d.Name("Jellyseerr import daemon")
|
||||||
|
return d
|
||||||
|
}
|
@ -81,6 +81,9 @@
|
|||||||
"useInviteExpiry": "Set expiry from profile/invite",
|
"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.",
|
"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",
|
"applyHomescreenLayout": "Apply homescreen layout",
|
||||||
|
"applyConfigurationAndPolicy": "Apply Jellyfin configuration/policy",
|
||||||
|
"applyOmbi": "Apply Ombi profile (if available)",
|
||||||
|
"applyJellyseerr": "Apply Jellyseerr profile (if available)",
|
||||||
"sendDeleteNotificationEmail": "Send notification message",
|
"sendDeleteNotificationEmail": "Send notification message",
|
||||||
"sendDeleteNotifiationExample": "Your account has been deleted.",
|
"sendDeleteNotifiationExample": "Your account has been deleted.",
|
||||||
"settingsRestart": "Restart",
|
"settingsRestart": "Restart",
|
||||||
@ -99,6 +102,8 @@
|
|||||||
"settingsMaybeUnderAdvanced": "Tip: You might find what you're looking for by enabling Advanced Settings.",
|
"settingsMaybeUnderAdvanced": "Tip: You might find what you're looking for by enabling Advanced Settings.",
|
||||||
"ombiProfile": "Ombi user profile",
|
"ombiProfile": "Ombi user profile",
|
||||||
"ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go when this profile is selected.",
|
"ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go when this profile is selected.",
|
||||||
|
"jellyseerrProfile": "Jellyseerr user profile",
|
||||||
|
"jellyseerrUserDefaultsDescription": "Create a Jellyseerr user and configure it, then select it below. It's settings/permissions will be stored and applied to new Jellyseerr users created by jfa-go when this profile is selected.",
|
||||||
"userProfiles": "User Profiles",
|
"userProfiles": "User Profiles",
|
||||||
"userProfilesDescription": "Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.",
|
"userProfilesDescription": "Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.",
|
||||||
"userProfilesIsDefault": "Default",
|
"userProfilesIsDefault": "Default",
|
||||||
@ -208,6 +213,7 @@
|
|||||||
"sentAnnouncement": "Announcement sent.",
|
"sentAnnouncement": "Announcement sent.",
|
||||||
"savedAnnouncement": "Announcement saved.",
|
"savedAnnouncement": "Announcement saved.",
|
||||||
"setOmbiProfile": "Stored ombi profile.",
|
"setOmbiProfile": "Stored ombi profile.",
|
||||||
|
"savedProfile": "Stored profile changes.",
|
||||||
"updateApplied": "Update applied, please restart.",
|
"updateApplied": "Update applied, please restart.",
|
||||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||||
"telegramVerified": "Telegram account verified.",
|
"telegramVerified": "Telegram account verified.",
|
||||||
@ -224,6 +230,7 @@
|
|||||||
"errorDeleteProfile": "Failed to delete profile {n}",
|
"errorDeleteProfile": "Failed to delete profile {n}",
|
||||||
"errorLoadProfiles": "Failed to load profiles.",
|
"errorLoadProfiles": "Failed to load profiles.",
|
||||||
"errorCreateProfile": "Failed to create profile {n}",
|
"errorCreateProfile": "Failed to create profile {n}",
|
||||||
|
"errorSavedProfile": "Failed to save profile {n}",
|
||||||
"errorSetDefaultProfile": "Failed to set default profile.",
|
"errorSetDefaultProfile": "Failed to set default profile.",
|
||||||
"errorLoadUsers": "Failed to load users.",
|
"errorLoadUsers": "Failed to load users.",
|
||||||
"errorLoadSettings": "Failed to load settings.",
|
"errorLoadSettings": "Failed to load settings.",
|
||||||
|
29
main.go
29
main.go
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/hrfee/jfa-go/common"
|
"github.com/hrfee/jfa-go/common"
|
||||||
_ "github.com/hrfee/jfa-go/docs"
|
_ "github.com/hrfee/jfa-go/docs"
|
||||||
"github.com/hrfee/jfa-go/easyproxy"
|
"github.com/hrfee/jfa-go/easyproxy"
|
||||||
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
"github.com/hrfee/jfa-go/logger"
|
"github.com/hrfee/jfa-go/logger"
|
||||||
"github.com/hrfee/jfa-go/ombi"
|
"github.com/hrfee/jfa-go/ombi"
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
@ -101,6 +102,7 @@ type appContext struct {
|
|||||||
jf *mediabrowser.MediaBrowser
|
jf *mediabrowser.MediaBrowser
|
||||||
authJf *mediabrowser.MediaBrowser
|
authJf *mediabrowser.MediaBrowser
|
||||||
ombi *ombi.Ombi
|
ombi *ombi.Ombi
|
||||||
|
js *jellyseerr.Jellyseerr
|
||||||
datePattern string
|
datePattern string
|
||||||
timePattern string
|
timePattern string
|
||||||
storage Storage
|
storage Storage
|
||||||
@ -359,6 +361,19 @@ func start(asDaemon, firstCall bool) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
||||||
|
app.debug.Printf("Connecting to Jellyseerr")
|
||||||
|
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
|
||||||
|
app.js = jellyseerr.NewJellyseerr(
|
||||||
|
jellyseerrServer,
|
||||||
|
app.config.Section("jellyseerr").Key("api_key").String(),
|
||||||
|
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
|
||||||
|
)
|
||||||
|
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
|
||||||
|
// app.js.LogRequestBodies = true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
app.storage.db_path = filepath.Join(app.dataPath, "db")
|
app.storage.db_path = filepath.Join(app.dataPath, "db")
|
||||||
app.loadPendingBackup()
|
app.loadPendingBackup()
|
||||||
app.ConnectDB()
|
app.ConnectDB()
|
||||||
@ -466,13 +481,21 @@ func start(asDaemon, firstCall bool) {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
invDaemon := newInviteDaemon(time.Duration(60*time.Second), app)
|
invDaemon := newHousekeepingDaemon(time.Duration(60*time.Second), app)
|
||||||
go invDaemon.run()
|
go invDaemon.run()
|
||||||
defer invDaemon.Shutdown()
|
defer invDaemon.Shutdown()
|
||||||
|
|
||||||
userDaemon := newUserDaemon(time.Duration(60*time.Second), app)
|
userDaemon := newUserDaemon(time.Duration(60*time.Second), app)
|
||||||
go userDaemon.run()
|
go userDaemon.run()
|
||||||
defer userDaemon.shutdown()
|
defer userDaemon.Shutdown()
|
||||||
|
|
||||||
|
var jellyseerrDaemon *GenericDaemon
|
||||||
|
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) && app.config.Section("jellyseerr").Key("import_existing").MustBool(false) {
|
||||||
|
// jellyseerrDaemon = newJellyseerrDaemon(time.Duration(30*time.Second), app)
|
||||||
|
jellyseerrDaemon = newJellyseerrDaemon(time.Duration(10*time.Minute), app)
|
||||||
|
go jellyseerrDaemon.run()
|
||||||
|
defer jellyseerrDaemon.Shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
|
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
|
||||||
go app.StartPWR()
|
go app.StartPWR()
|
||||||
@ -482,7 +505,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
go app.checkForUpdates()
|
go app.checkForUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
var backupDaemon *housekeepingDaemon
|
var backupDaemon *GenericDaemon
|
||||||
if app.config.Section("backups").Key("enabled").MustBool(false) {
|
if app.config.Section("backups").Key("enabled").MustBool(false) {
|
||||||
backupDaemon = newBackupDaemon(app)
|
backupDaemon = newBackupDaemon(app)
|
||||||
go backupDaemon.run()
|
go backupDaemon.run()
|
||||||
|
@ -16,6 +16,7 @@ type newUserDTO struct {
|
|||||||
Username string `json:"username" example:"jeff" binding:"required"` // User's username
|
Username string `json:"username" example:"jeff" binding:"required"` // User's username
|
||||||
Password string `json:"password" example:"guest" binding:"required"` // User's password
|
Password string `json:"password" example:"guest" binding:"required"` // User's password
|
||||||
Email string `json:"email" example:"jeff@jellyf.in"` // User's email address
|
Email string `json:"email" example:"jeff@jellyf.in"` // User's email address
|
||||||
|
EmailContact bool `json:"email_contact"` // Whether or not to use email for notifications/pwrs
|
||||||
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
|
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
|
||||||
TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used)
|
TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used)
|
||||||
TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs
|
TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs
|
||||||
@ -76,6 +77,7 @@ type profileDTO struct {
|
|||||||
LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
|
LibraryAccess string `json:"libraries" example:"all"` // Number of libraries profile has access to
|
||||||
FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
|
FromUser string `json:"fromUser" example:"jeff"` // The user the profile is based on
|
||||||
Ombi bool `json:"ombi"` // Whether or not Ombi settings are stored in this profile.
|
Ombi bool `json:"ombi"` // Whether or not Ombi settings are stored in this profile.
|
||||||
|
Jellyseerr bool `json:"jellyseerr"` // Whether or not Jellyseerr settings are stored in this profile.
|
||||||
ReferralsEnabled bool `json:"referrals_enabled" example:"true"` // Whether or not the profile has referrals enabled, and has a template invite stored.
|
ReferralsEnabled bool `json:"referrals_enabled" example:"true"` // Whether or not the profile has referrals enabled, and has a template invite stored.
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +178,12 @@ type userSettingsDTO struct {
|
|||||||
Profile string `json:"profile"` // Name of profile (if from = "profile")
|
Profile string `json:"profile"` // Name of profile (if from = "profile")
|
||||||
ApplyTo []string `json:"apply_to"` // Users to apply settings to
|
ApplyTo []string `json:"apply_to"` // Users to apply settings to
|
||||||
ID string `json:"id"` // ID of user (if from = "user")
|
ID string `json:"id"` // ID of user (if from = "user")
|
||||||
|
// Note confusing name: "Configuration" on the admin UI just means it in the sense
|
||||||
|
// of the account's settings.
|
||||||
|
Policy bool `json:"configuration"` // Whether to apply jf policy not
|
||||||
Homescreen bool `json:"homescreen"` // Whether to apply homescreen layout or not
|
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 {
|
type announcementDTO struct {
|
||||||
|
@ -238,6 +238,11 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
api.GET(p+"/users/discord/:username", app.DiscordGetUsers)
|
api.GET(p+"/users/discord/:username", app.DiscordGetUsers)
|
||||||
api.POST(p+"/users/discord", app.DiscordConnect)
|
api.POST(p+"/users/discord", app.DiscordConnect)
|
||||||
}
|
}
|
||||||
|
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
||||||
|
api.GET(p+"/jellyseerr/users", app.JellyseerrUsers)
|
||||||
|
api.POST(p+"/profiles/jellyseerr/:profile/:id", app.SetJellyseerrProfile)
|
||||||
|
api.DELETE(p+"/profiles/jellyseerr/:profile", app.DeleteJellyseerrProfile)
|
||||||
|
}
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
api.GET(p+"/ombi/users", app.OmbiUsers)
|
api.GET(p+"/ombi/users", app.OmbiUsers)
|
||||||
api.POST(p+"/profiles/ombi/:profile", app.SetOmbiProfile)
|
api.POST(p+"/profiles/ombi/:profile", app.SetOmbiProfile)
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
"github.com/hrfee/jfa-go/logger"
|
"github.com/hrfee/jfa-go/logger"
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
@ -650,9 +651,16 @@ type Profile struct {
|
|||||||
Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
|
Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
|
||||||
Default bool `json:"default,omitempty"`
|
Default bool `json:"default,omitempty"`
|
||||||
Ombi map[string]interface{} `json:"ombi,omitempty"`
|
Ombi map[string]interface{} `json:"ombi,omitempty"`
|
||||||
|
Jellyseerr JellyseerrTemplate `json:"jellyseerr,omitempty"`
|
||||||
ReferralTemplateKey string
|
ReferralTemplateKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JellyseerrTemplate struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
User jellyseerr.UserTemplate `json:"user,omitempty"`
|
||||||
|
Notifications jellyseerr.NotificationsTemplate `json:"notifications,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type Invite struct {
|
type Invite struct {
|
||||||
Code string `badgerhold:"key"`
|
Code string `badgerhold:"key"`
|
||||||
Created time.Time `json:"created"`
|
Created time.Time `json:"created"`
|
||||||
|
@ -50,6 +50,9 @@ window.availableProfiles = window.availableProfiles || [];
|
|||||||
window.modals.ombiProfile = new Modal(document.getElementById('modal-ombi-profile'));
|
window.modals.ombiProfile = new Modal(document.getElementById('modal-ombi-profile'));
|
||||||
document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiProfile.close);
|
document.getElementById('form-ombi-defaults').addEventListener('submit', window.modals.ombiProfile.close);
|
||||||
|
|
||||||
|
window.modals.jellyseerrProfile = new Modal(document.getElementById('modal-jellyseerr-profile'));
|
||||||
|
document.getElementById('form-jellyseerr-defaults').addEventListener('submit', window.modals.jellyseerrProfile.close);
|
||||||
|
|
||||||
window.modals.profiles = new Modal(document.getElementById("modal-user-profiles"));
|
window.modals.profiles = new Modal(document.getElementById("modal-user-profiles"));
|
||||||
|
|
||||||
window.modals.addProfile = new Modal(document.getElementById("modal-add-profile"));
|
window.modals.addProfile = new Modal(document.getElementById("modal-add-profile"));
|
||||||
@ -184,6 +187,7 @@ login.onLogin = () => {
|
|||||||
console.log("Logged in.");
|
console.log("Logged in.");
|
||||||
window.updater = new Updater();
|
window.updater = new Updater();
|
||||||
// FIXME: Decide whether to autoload activity or not
|
// FIXME: Decide whether to autoload activity or not
|
||||||
|
window.invites.reload()
|
||||||
setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000);
|
setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000);
|
||||||
const currentTab = window.tabs.current;
|
const currentTab = window.tabs.current;
|
||||||
switch (currentTab) {
|
switch (currentTab) {
|
||||||
|
@ -224,6 +224,7 @@ if (window.emailRequired) {
|
|||||||
interface sendDTO {
|
interface sendDTO {
|
||||||
code: string;
|
code: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
email_contact?: boolean;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
telegram_pin?: string;
|
telegram_pin?: string;
|
||||||
@ -252,8 +253,9 @@ const create = (event: SubmitEvent) => {
|
|||||||
code: window.code,
|
code: window.code,
|
||||||
username: usernameField.value,
|
username: usernameField.value,
|
||||||
email: emailField.value,
|
email: emailField.value,
|
||||||
|
email_contact: true,
|
||||||
password: passwordField.value
|
password: passwordField.value
|
||||||
};
|
}
|
||||||
if (telegramVerified) {
|
if (telegramVerified) {
|
||||||
send.telegram_pin = window.telegramPIN;
|
send.telegram_pin = window.telegramPIN;
|
||||||
const checkbox = document.getElementById("contact-via-telegram") as HTMLInputElement;
|
const checkbox = document.getElementById("contact-via-telegram") as HTMLInputElement;
|
||||||
@ -275,6 +277,10 @@ const create = (event: SubmitEvent) => {
|
|||||||
send.matrix_contact = true;
|
send.matrix_contact = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (matrixVerified || discordVerified || telegramVerified) {
|
||||||
|
const checkbox = document.getElementById("contact-via-email") as HTMLInputElement;
|
||||||
|
send.email_contact = checkbox.checked;
|
||||||
|
}
|
||||||
if (window.captcha) {
|
if (window.captcha) {
|
||||||
if (window.reCAPTCHA) {
|
if (window.reCAPTCHA) {
|
||||||
send.captcha_text = grecaptcha.getResponse();
|
send.captcha_text = grecaptcha.getResponse();
|
||||||
|
@ -795,6 +795,11 @@ export class accountsList {
|
|||||||
private _searchBox = document.getElementById("accounts-search") as HTMLInputElement;
|
private _searchBox = document.getElementById("accounts-search") as HTMLInputElement;
|
||||||
private _search: Search;
|
private _search: Search;
|
||||||
|
|
||||||
|
private _applyHomescreen = document.getElementById("modify-user-homescreen") as HTMLInputElement;
|
||||||
|
private _applyConfiguration = document.getElementById("modify-user-configuration") 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 _selectAll = document.getElementById("accounts-select-all") as HTMLInputElement;
|
||||||
private _users: { [id: string]: user };
|
private _users: { [id: string]: user };
|
||||||
private _ordering: string[] = [];
|
private _ordering: string[] = [];
|
||||||
@ -1459,6 +1464,7 @@ export class accountsList {
|
|||||||
const modalHeader = document.getElementById("header-modify-user");
|
const modalHeader = document.getElementById("header-modify-user");
|
||||||
modalHeader.textContent = window.lang.quantity("modifySettingsFor", this._collectUsers().length)
|
modalHeader.textContent = window.lang.quantity("modifySettingsFor", this._collectUsers().length)
|
||||||
let list = this._collectUsers();
|
let list = this._collectUsers();
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
let innerHTML = "";
|
let innerHTML = "";
|
||||||
for (const profile of window.availableProfiles) {
|
for (const profile of window.availableProfiles) {
|
||||||
@ -1477,6 +1483,7 @@ export class accountsList {
|
|||||||
|
|
||||||
const form = document.getElementById("form-modify-user") as HTMLFormElement;
|
const form = document.getElementById("form-modify-user") as HTMLFormElement;
|
||||||
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
||||||
|
|
||||||
this._modifySettingsProfile.checked = true;
|
this._modifySettingsProfile.checked = true;
|
||||||
this._modifySettingsUser.checked = false;
|
this._modifySettingsUser.checked = false;
|
||||||
form.onsubmit = (event: Event) => {
|
form.onsubmit = (event: Event) => {
|
||||||
@ -1484,7 +1491,10 @@ export class accountsList {
|
|||||||
toggleLoader(button);
|
toggleLoader(button);
|
||||||
let send = {
|
let send = {
|
||||||
"apply_to": list,
|
"apply_to": list,
|
||||||
"homescreen": (document.getElementById("modify-user-homescreen") as HTMLInputElement).checked
|
"homescreen": this._applyHomescreen.checked,
|
||||||
|
"configuration": this._applyConfiguration.checked,
|
||||||
|
"ombi": this._applyOmbi.checked,
|
||||||
|
"jellyseerr": this._applyJellyseerr.checked
|
||||||
};
|
};
|
||||||
if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) {
|
if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) {
|
||||||
send["from"] = "profile";
|
send["from"] = "profile";
|
||||||
@ -1821,6 +1831,16 @@ export class accountsList {
|
|||||||
};
|
};
|
||||||
this._modifySettings.onclick = this.modifyUsers;
|
this._modifySettings.onclick = this.modifyUsers;
|
||||||
this._modifySettings.classList.add("unfocused");
|
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 checkSource = () => {
|
||||||
const profileSpan = this._modifySettingsProfile.nextElementSibling as HTMLSpanElement;
|
const profileSpan = this._modifySettingsProfile.nextElementSibling as HTMLSpanElement;
|
||||||
const userSpan = this._modifySettingsUser.nextElementSibling as HTMLSpanElement;
|
const userSpan = this._modifySettingsUser.nextElementSibling as HTMLSpanElement;
|
||||||
@ -1831,6 +1851,8 @@ export class accountsList {
|
|||||||
profileSpan.classList.remove("@low");
|
profileSpan.classList.remove("@low");
|
||||||
userSpan.classList.remove("@high");
|
userSpan.classList.remove("@high");
|
||||||
userSpan.classList.add("@low");
|
userSpan.classList.add("@low");
|
||||||
|
this._applyOmbi.parentElement.classList.remove("unfocused");
|
||||||
|
this._applyJellyseerr.parentElement.classList.remove("unfocused");
|
||||||
} else {
|
} else {
|
||||||
this._userSelect.parentElement.classList.remove("unfocused");
|
this._userSelect.parentElement.classList.remove("unfocused");
|
||||||
this._profileSelect.parentElement.classList.add("unfocused");
|
this._profileSelect.parentElement.classList.add("unfocused");
|
||||||
@ -1838,6 +1860,8 @@ export class accountsList {
|
|||||||
userSpan.classList.remove("@low");
|
userSpan.classList.remove("@low");
|
||||||
profileSpan.classList.remove("@high");
|
profileSpan.classList.remove("@high");
|
||||||
profileSpan.classList.add("@low");
|
profileSpan.classList.add("@low");
|
||||||
|
this._applyOmbi.parentElement.classList.add("unfocused");
|
||||||
|
this._applyJellyseerr.parentElement.classList.add("unfocused");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this._modifySettingsProfile.onchange = checkSource;
|
this._modifySettingsProfile.onchange = checkSource;
|
||||||
|
@ -5,6 +5,7 @@ interface Profile {
|
|||||||
libraries: string;
|
libraries: string;
|
||||||
fromUser: string;
|
fromUser: string;
|
||||||
ombi: boolean;
|
ombi: boolean;
|
||||||
|
jellyseerr: boolean;
|
||||||
referrals_enabled: boolean;
|
referrals_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,9 +15,11 @@ class profile implements Profile {
|
|||||||
private _adminChip: HTMLSpanElement;
|
private _adminChip: HTMLSpanElement;
|
||||||
private _libraries: HTMLTableDataCellElement;
|
private _libraries: HTMLTableDataCellElement;
|
||||||
private _ombiButton: HTMLSpanElement;
|
private _ombiButton: HTMLSpanElement;
|
||||||
|
private _ombi: boolean;
|
||||||
|
private _jellyseerrButton: HTMLSpanElement;
|
||||||
|
private _jellyseerr: boolean;
|
||||||
private _fromUser: HTMLTableDataCellElement;
|
private _fromUser: HTMLTableDataCellElement;
|
||||||
private _defaultRadio: HTMLInputElement;
|
private _defaultRadio: HTMLInputElement;
|
||||||
private _ombi: boolean;
|
|
||||||
private _referralsButton: HTMLSpanElement;
|
private _referralsButton: HTMLSpanElement;
|
||||||
private _referralsEnabled: boolean;
|
private _referralsEnabled: boolean;
|
||||||
|
|
||||||
@ -52,6 +55,21 @@ class profile implements Profile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get jellyseerr(): boolean { return this._jellyseerr; }
|
||||||
|
set jellyseerr(v: boolean) {
|
||||||
|
if (!window.jellyseerrEnabled) return;
|
||||||
|
this._jellyseerr = v;
|
||||||
|
if (v) {
|
||||||
|
this._jellyseerrButton.textContent = window.lang.strings("delete");
|
||||||
|
this._jellyseerrButton.classList.add("~critical");
|
||||||
|
this._jellyseerrButton.classList.remove("~neutral");
|
||||||
|
} else {
|
||||||
|
this._jellyseerrButton.textContent = window.lang.strings("add");
|
||||||
|
this._jellyseerrButton.classList.add("~neutral");
|
||||||
|
this._jellyseerrButton.classList.remove("~critical");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get fromUser(): string { return this._fromUser.textContent; }
|
get fromUser(): string { return this._fromUser.textContent; }
|
||||||
set fromUser(v: string) { this._fromUser.textContent = v; }
|
set fromUser(v: string) { this._fromUser.textContent = v; }
|
||||||
|
|
||||||
@ -82,6 +100,9 @@ class profile implements Profile {
|
|||||||
if (window.ombiEnabled) innerHTML += `
|
if (window.ombiEnabled) innerHTML += `
|
||||||
<td><span class="button @low profile-ombi"></span></td>
|
<td><span class="button @low profile-ombi"></span></td>
|
||||||
`;
|
`;
|
||||||
|
if (window.jellyseerrEnabled) innerHTML += `
|
||||||
|
<td><span class="button @low profile-jellyseerr"></span></td>
|
||||||
|
`;
|
||||||
if (window.referralsEnabled) innerHTML += `
|
if (window.referralsEnabled) innerHTML += `
|
||||||
<td><span class="button @low profile-referrals"></span></td>
|
<td><span class="button @low profile-referrals"></span></td>
|
||||||
`;
|
`;
|
||||||
@ -96,6 +117,8 @@ class profile implements Profile {
|
|||||||
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
|
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
|
||||||
if (window.ombiEnabled)
|
if (window.ombiEnabled)
|
||||||
this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement;
|
this._ombiButton = this._row.querySelector("span.profile-ombi") as HTMLSpanElement;
|
||||||
|
if (window.jellyseerrEnabled)
|
||||||
|
this._jellyseerrButton = this._row.querySelector("span.profile-jellyseerr") as HTMLSpanElement;
|
||||||
if (window.referralsEnabled)
|
if (window.referralsEnabled)
|
||||||
this._referralsButton = this._row.querySelector("span.profile-referrals") as HTMLSpanElement;
|
this._referralsButton = this._row.querySelector("span.profile-referrals") as HTMLSpanElement;
|
||||||
this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement;
|
this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement;
|
||||||
@ -112,10 +135,12 @@ class profile implements Profile {
|
|||||||
this.fromUser = p.fromUser;
|
this.fromUser = p.fromUser;
|
||||||
this.libraries = p.libraries;
|
this.libraries = p.libraries;
|
||||||
this.ombi = p.ombi;
|
this.ombi = p.ombi;
|
||||||
|
this.jellyseerr = p.jellyseerr;
|
||||||
this.referrals_enabled = p.referrals_enabled;
|
this.referrals_enabled = p.referrals_enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => { this._ombiButton.onclick = () => ombiFunc(this._ombi); }
|
setOmbiFunc = (ombiFunc: (ombi: boolean) => void) => { this._ombiButton.onclick = () => ombiFunc(this._ombi); }
|
||||||
|
setJellyseerrFunc = (jellyseerrFunc: (jellyseerr: boolean) => void) => { this._jellyseerrButton.onclick = () => jellyseerrFunc(this._jellyseerr); }
|
||||||
setReferralFunc = (referralFunc: (enabled: boolean) => void) => { this._referralsButton.onclick = () => referralFunc(this._referralsEnabled); }
|
setReferralFunc = (referralFunc: (enabled: boolean) => void) => { this._referralsButton.onclick = () => referralFunc(this._referralsEnabled); }
|
||||||
|
|
||||||
remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); }
|
remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); }
|
||||||
@ -144,6 +169,7 @@ export class ProfileEditor {
|
|||||||
private _profiles: { [name: string]: profile } = {};
|
private _profiles: { [name: string]: profile } = {};
|
||||||
private _default: string;
|
private _default: string;
|
||||||
private _ombiProfiles: ombiProfiles;
|
private _ombiProfiles: ombiProfiles;
|
||||||
|
private _jellyseerrProfiles: jellyseerrProfiles;
|
||||||
|
|
||||||
private _createForm = document.getElementById("form-add-profile") as HTMLFormElement;
|
private _createForm = document.getElementById("form-add-profile") as HTMLFormElement;
|
||||||
private _profileName = document.getElementById("add-profile-name") as HTMLInputElement;
|
private _profileName = document.getElementById("add-profile-name") as HTMLInputElement;
|
||||||
@ -181,7 +207,7 @@ export class ProfileEditor {
|
|||||||
this._profiles[name].update(name, resp.profiles[name]);
|
this._profiles[name].update(name, resp.profiles[name]);
|
||||||
} else {
|
} else {
|
||||||
this._profiles[name] = new profile(name, resp.profiles[name]);
|
this._profiles[name] = new profile(name, resp.profiles[name]);
|
||||||
if (window.ombiEnabled)
|
if (window.ombiEnabled) {
|
||||||
this._profiles[name].setOmbiFunc((ombi: boolean) => {
|
this._profiles[name].setOmbiFunc((ombi: boolean) => {
|
||||||
if (ombi) {
|
if (ombi) {
|
||||||
this._ombiProfiles.delete(name, (req: XMLHttpRequest) => {
|
this._ombiProfiles.delete(name, (req: XMLHttpRequest) => {
|
||||||
@ -198,7 +224,26 @@ export class ProfileEditor {
|
|||||||
this._ombiProfiles.load(name);
|
this._ombiProfiles.load(name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (window.referralsEnabled)
|
}
|
||||||
|
if (window.jellyseerrEnabled) {
|
||||||
|
this._profiles[name].setJellyseerrFunc((jellyseerr: boolean) => {
|
||||||
|
if (jellyseerr) {
|
||||||
|
this._jellyseerrProfiles.delete(name, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status != 204) {
|
||||||
|
window.notifications.customError("errorDeleteJellyseerr", window.lang.notif("errorUnknown"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._profiles[name].jellyseerr = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.modals.profiles.close();
|
||||||
|
this._jellyseerrProfiles.load(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (window.referralsEnabled) {
|
||||||
this._profiles[name].setReferralFunc((enabled: boolean) => {
|
this._profiles[name].setReferralFunc((enabled: boolean) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
this.disableReferrals(name);
|
this.disableReferrals(name);
|
||||||
@ -206,6 +251,7 @@ export class ProfileEditor {
|
|||||||
this.enableReferrals(name);
|
this.enableReferrals(name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
this._table.appendChild(this._profiles[name].asElement());
|
this._table.appendChild(this._profiles[name].asElement());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -299,6 +345,8 @@ export class ProfileEditor {
|
|||||||
|
|
||||||
if (window.ombiEnabled)
|
if (window.ombiEnabled)
|
||||||
this._ombiProfiles = new ombiProfiles();
|
this._ombiProfiles = new ombiProfiles();
|
||||||
|
if (window.jellyseerrEnabled)
|
||||||
|
this._jellyseerrProfiles = new jellyseerrProfiles();
|
||||||
|
|
||||||
this._createButton.onclick = () => _get("/users", null, (req: XMLHttpRequest) => {
|
this._createButton.onclick = () => _get("/users", null, (req: XMLHttpRequest) => {
|
||||||
if (req.readyState == 4) {
|
if (req.readyState == 4) {
|
||||||
@ -366,7 +414,7 @@ export class ombiProfiles {
|
|||||||
let resp = {} as ombiUser;
|
let resp = {} as ombiUser;
|
||||||
resp.id = this._select.value;
|
resp.id = this._select.value;
|
||||||
resp.name = this._users[resp.id];
|
resp.name = this._users[resp.id];
|
||||||
_post("/profiles/ombi/" + this._currentProfile, resp, (req: XMLHttpRequest) => {
|
_post("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(this._currentProfile)), resp, (req: XMLHttpRequest) => {
|
||||||
if (req.readyState == 4) {
|
if (req.readyState == 4) {
|
||||||
toggleLoader(button);
|
toggleLoader(button);
|
||||||
if (req.status == 200 || req.status == 204) {
|
if (req.status == 200 || req.status == 204) {
|
||||||
@ -379,7 +427,7 @@ export class ombiProfiles {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/ombi/" + profile, null, post);
|
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(profile)), null, post);
|
||||||
|
|
||||||
load = (profile: string) => {
|
load = (profile: string) => {
|
||||||
this._currentProfile = profile;
|
this._currentProfile = profile;
|
||||||
@ -401,3 +449,54 @@ export class ombiProfiles {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class jellyseerrProfiles {
|
||||||
|
private _form: HTMLFormElement;
|
||||||
|
private _select: HTMLSelectElement;
|
||||||
|
private _users: { [id: string]: string } = {};
|
||||||
|
private _currentProfile: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._form = document.getElementById("form-jellyseerr-defaults") as HTMLFormElement;
|
||||||
|
this._form.onsubmit = this.send;
|
||||||
|
this._select = this._form.querySelector("select") as HTMLSelectElement;
|
||||||
|
}
|
||||||
|
send = () => {
|
||||||
|
const button = this._form.querySelector("span.submit") as HTMLSpanElement;
|
||||||
|
toggleLoader(button);
|
||||||
|
let encodedProfile = encodeURIComponent(encodeURIComponent(this._currentProfile));
|
||||||
|
_post("/profiles/jellyseerr/" + encodedProfile + "/" + this._select.value, null, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
toggleLoader(button);
|
||||||
|
if (req.status == 200 || req.status == 204) {
|
||||||
|
window.notifications.customSuccess("jellyseerrDefaults", window.lang.notif("savedProfile"));
|
||||||
|
} else {
|
||||||
|
window.notifications.customError("jellyseerrDefaults", window.lang.notif("errorSavedProfile"));
|
||||||
|
}
|
||||||
|
window.modals.jellyseerrProfile.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/jellyseerr/" + encodeURIComponent(encodeURIComponent(profile)), null, post);
|
||||||
|
|
||||||
|
load = (profile: string) => {
|
||||||
|
this._currentProfile = profile;
|
||||||
|
_get("/jellyseerr/users", null, (req: XMLHttpRequest) => {
|
||||||
|
if (req.readyState == 4) {
|
||||||
|
if (req.status == 200 && "users" in req.response) {
|
||||||
|
const users = req.response["users"] as ombiUser[];
|
||||||
|
let innerHTML = "";
|
||||||
|
for (let user of users) {
|
||||||
|
this._users[user.id] = user.name;
|
||||||
|
innerHTML += `<option value="${user.id}">${user.name}</option>`;
|
||||||
|
}
|
||||||
|
this._select.innerHTML = innerHTML;
|
||||||
|
window.modals.jellyseerrProfile.show();
|
||||||
|
} else {
|
||||||
|
window.notifications.customError("jellyseerrLoadError", window.lang.notif("errorLoadUsers"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -24,6 +24,7 @@ declare interface Window {
|
|||||||
discordEnabled: boolean;
|
discordEnabled: boolean;
|
||||||
matrixEnabled: boolean;
|
matrixEnabled: boolean;
|
||||||
ombiEnabled: boolean;
|
ombiEnabled: boolean;
|
||||||
|
jellyseerrEnabled: boolean;
|
||||||
usernameEnabled: boolean;
|
usernameEnabled: boolean;
|
||||||
linkResetEnabled: boolean;
|
linkResetEnabled: boolean;
|
||||||
token: string;
|
token: string;
|
||||||
@ -101,6 +102,7 @@ declare interface Modals {
|
|||||||
settingsRestart: Modal;
|
settingsRestart: Modal;
|
||||||
settingsRefresh: Modal;
|
settingsRefresh: Modal;
|
||||||
ombiProfile?: Modal;
|
ombiProfile?: Modal;
|
||||||
|
jellyseerrProfile?: Modal;
|
||||||
profiles: Modal;
|
profiles: Modal;
|
||||||
addProfile: Modal;
|
addProfile: Modal;
|
||||||
announce: Modal;
|
announce: Modal;
|
||||||
|
@ -7,47 +7,14 @@ import (
|
|||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type userDaemon struct {
|
func newUserDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||||
Stopped bool
|
d := NewGenericDaemon(interval, app,
|
||||||
ShutdownChannel chan string
|
func(app *appContext) {
|
||||||
Interval time.Duration
|
app.checkUsers()
|
||||||
period time.Duration
|
},
|
||||||
app *appContext
|
)
|
||||||
}
|
d.Name("User daemon")
|
||||||
|
return d
|
||||||
func newUserDaemon(interval time.Duration, app *appContext) *userDaemon {
|
|
||||||
return &userDaemon{
|
|
||||||
Stopped: false,
|
|
||||||
ShutdownChannel: make(chan string),
|
|
||||||
Interval: interval,
|
|
||||||
period: interval,
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *userDaemon) run() {
|
|
||||||
rt.app.info.Println("User daemon started")
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-rt.ShutdownChannel:
|
|
||||||
rt.ShutdownChannel <- "Down"
|
|
||||||
return
|
|
||||||
case <-time.After(rt.period):
|
|
||||||
break
|
|
||||||
}
|
|
||||||
started := time.Now()
|
|
||||||
rt.app.checkUsers()
|
|
||||||
finished := time.Now()
|
|
||||||
duration := finished.Sub(started)
|
|
||||||
rt.period = rt.Interval - duration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *userDaemon) shutdown() {
|
|
||||||
rt.Stopped = true
|
|
||||||
rt.ShutdownChannel <- "Down"
|
|
||||||
<-rt.ShutdownChannel
|
|
||||||
close(rt.ShutdownChannel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) checkUsers() {
|
func (app *appContext) checkUsers() {
|
||||||
|
5
views.go
5
views.go
@ -133,6 +133,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
|||||||
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
||||||
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
||||||
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
||||||
|
jellyseerrEnabled := app.config.Section("jellyseerr").Key("enabled").MustBool(false)
|
||||||
jfAdminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
jfAdminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
||||||
jfAllowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
|
jfAllowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
|
||||||
var license string
|
var license string
|
||||||
@ -164,6 +165,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
|||||||
"discordEnabled": discordEnabled,
|
"discordEnabled": discordEnabled,
|
||||||
"matrixEnabled": matrixEnabled,
|
"matrixEnabled": matrixEnabled,
|
||||||
"ombiEnabled": ombiEnabled,
|
"ombiEnabled": ombiEnabled,
|
||||||
|
"jellyseerrEnabled": jellyseerrEnabled,
|
||||||
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
|
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
|
||||||
"notifications": notificationsEnabled,
|
"notifications": notificationsEnabled,
|
||||||
"version": version,
|
"version": version,
|
||||||
@ -192,6 +194,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
|
|||||||
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
||||||
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
||||||
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
||||||
|
jellyseerrEnabled := app.config.Section("jellyseerr").Key("enabled").MustBool(false)
|
||||||
data := gin.H{
|
data := gin.H{
|
||||||
"urlBase": app.getURLBase(gc),
|
"urlBase": app.getURLBase(gc),
|
||||||
"cssClass": app.cssClass,
|
"cssClass": app.cssClass,
|
||||||
@ -203,6 +206,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
|
|||||||
"discordEnabled": discordEnabled,
|
"discordEnabled": discordEnabled,
|
||||||
"matrixEnabled": matrixEnabled,
|
"matrixEnabled": matrixEnabled,
|
||||||
"ombiEnabled": ombiEnabled,
|
"ombiEnabled": ombiEnabled,
|
||||||
|
"jellyseerrEnabled": jellyseerrEnabled,
|
||||||
"pwrEnabled": app.config.Section("password_resets").Key("enabled").MustBool(false),
|
"pwrEnabled": app.config.Section("password_resets").Key("enabled").MustBool(false),
|
||||||
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
|
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
|
||||||
"notifications": notificationsEnabled,
|
"notifications": notificationsEnabled,
|
||||||
@ -278,6 +282,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
"strings": app.storage.lang.PasswordReset[lang].Strings,
|
"strings": app.storage.lang.PasswordReset[lang].Strings,
|
||||||
"success": false,
|
"success": false,
|
||||||
"ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false),
|
"ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false),
|
||||||
|
"jellyseerrEnabled": app.config.Section("jellyseerr").Key("enabled").MustBool(false),
|
||||||
"customSuccessCard": false,
|
"customSuccessCard": false,
|
||||||
}
|
}
|
||||||
pwr, isInternal := app.internalPWRs[pin]
|
pwr, isInternal := app.internalPWRs[pin]
|
||||||
|
Loading…
Reference in New Issue
Block a user