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

Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
610ca635f3
Merge 97506c0bcd into 6b576f2ffe 2024-07-24 22:11:51 +02:00
48 changed files with 363 additions and 2137 deletions

View File

@ -80,9 +80,6 @@ builds:
dir: ./
env:
- CGO_ENABLED=1
- CC=x86_64-linux-gnu-gcc
- CXX=x86_64-linux-gnu-gcc
- PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH
flags:
- -tags=tray
ldflags:

View File

@ -1,51 +0,0 @@
when:
- event: push
branch: main
# - evaluate: 'CI_PIPELINE_EVENT != "PULL_REQUEST" && CI_COMMIT_BRANCH == CI_REPO_DEFAULT_BRANCH'
clone:
git:
image: woodpeckerci/plugin-git
settings:
tags: true
partial: false
depth: 0
steps:
- name: redoc
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
REDOC_SSH_ID:
from_secret: REDOC_SSH_ID
commands:
- sh -c "echo \"$REDOC_SSH_ID\" > /tmp/id_redoc && chmod 600 /tmp/id_redoc"
- bash -c 'sftp -P 3625 -i /tmp/id_redoc -o StrictHostKeyChecking=no redoc@api.jfa-go.com:/home/redoc <<< $"put docs/swagger.json jfa-go.json"'
- name: build
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
JFA_GO_SNAPSHOT: y
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
commands:
- curl -sfL https://goreleaser.com/static/run > goreleaser
- chmod +x goreleaser
- ./scripts/version.sh ./goreleaser --snapshot --skip=publish --clean
- name: deb-repo
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
REPO_SSH_ID:
from_secret: REPO_SSH_ID
commands:
- sh -c "echo \"$REPO_SSH_ID\" > /tmp/id_repo && chmod 600 /tmp/id_repo"
- bash -c 'sftp -P 2022 -i /tmp/id_repo -o StrictHostKeyChecking=no root@apt.hrfee.dev:/repo/incoming <<< $"put dist/*.deb"'
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "repo-process-deb trusty-unstable"'
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "repo-process-deb trusty"'
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "rm -f /repo/incoming/*.deb"'
- name: buildrone
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
commands:
- wget https://builds.hrfee.pw/upload.py
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.zip ./dist/*.rpm ./dist/*.apk --tag internal-git=true'

View File

@ -1,29 +0,0 @@
when:
- event: push
branch: main
steps:
- name: build
image: docker.io/woodpeckerci/plugin-docker-buildx
secrets: [ BUILT_BY ]
settings:
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_TOKEN
repo: docker.io/hrfee/jfa-go
tags: unstable
registry: docker.io
platforms: linux/amd64,linux/arm64,linux/arm/v7
build_args:
- BUILT_BY: $BUILT_BY
- name: buildrone
image: docker.io/python
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
commands:
- wget https://builds.hrfee.pw/upload.py
- pip install requests
- python upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-unstable=true

View File

@ -1,41 +0,0 @@
when:
- event: tag
branch: main
clone:
git:
image: woodpeckerci/plugin-git
settings:
tags: true
partial: false
depth: 0
steps:
- name: build
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
commands:
- curl -sfL https://goreleaser.com/static/run > ../goreleaser
- chmod +x ../goreleaser
- ./scripts/version.sh ../goreleaser
- name: deb-repo
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
REPO_SSH_ID:
from_secret: REPO_SSH_ID
commands:
- sh -c "echo \"$REPO_SSH_ID\" > /tmp/id_repo && chmod 600 /tmp/id_repo"
- bash -c 'sftp -P 2022 -i /tmp/id_repo -o StrictHostKeyChecking=no root@apt.hrfee.dev:/repo/incoming <<< $"put dist/*.deb"'
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "repo-process-deb trusty"'
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "repo-process-deb trusty-unstable"'
- bash -c 'ssh -i /tmp/id_repo root@apt.hrfee.dev -p 2022 "rm -f /repo/incoming/*.deb"'
- name: buildrone
image: docker.io/hrfee/jfa-go-build-docker:latest
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
commands:
- wget https://builds.hrfee.pw/upload.py
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'

View File

@ -1,29 +0,0 @@
when:
- event: tag
branch: main
steps:
- name: build
image: docker.io/woodpeckerci/plugin-docker-buildx
secrets: [ BUILT_BY ]
settings:
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_TOKEN
repo: docker.io/hrfee/jfa-go
tags: latest
registry: docker.io
platforms: linux/amd64,linux/arm64,linux/arm/v7
build_args:
- BUILT_BY: $BUILT_BY
- name: buildrone
image: docker.io/python
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
commands:
- wget https://builds.hrfee.pw/upload.py
- pip install requests
- python upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-stable=true

View File

@ -1,22 +1,18 @@
# Use this instead if hrfee/jfa-go-build-docker doesn't support your architecture
# FROM --platform=$BUILDPLATFORM golang:latest AS support
FROM --platform=$BUILDPLATFORM hrfee/jfa-go-build-docker AS support
FROM --platform=$BUILDPLATFORM golang:latest AS support
COPY . /opt/build
# Uncomment this if hrfee/jfa-go-build-docker doesn't support your architecture
# RUN apt-get update -y \
# && apt-get install build-essential python3-pip -y \
# && (curl -sL https://deb.nodesource.com/setup_current.x | bash -) \
# && apt-get install nodejs
RUN (cd /opt/build; make configuration npm email typescript variants-html bundle-css inline-css swagger copy INTERNAL=off GOESBUILD=on) \
RUN apt-get update -y \
&& apt-get install build-essential python3-pip curl software-properties-common sed -y \
&& (curl -sL https://deb.nodesource.com/setup_current.x | bash -) \
&& apt-get install nodejs \
&& (cd /opt/build; make configuration npm email typescript variants-html bundle-css inline-css swagger copy INTERNAL=off GOESBUILD=on) \
&& sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' /opt/build/build/data/html/setup.html
FROM --platform=$BUILDPLATFORM golang:latest AS build
ARG TARGETARCH
ENV GOARCH=$TARGETARCH
ARG BUILT_BY
ENV BUILTBY=$BUILT_BY
COPY --from=support /opt/build /opt/build

View File

@ -1,100 +0,0 @@
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)
}

View File

@ -5,7 +5,6 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/jellyseerr"
"github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1"
)
@ -33,14 +32,12 @@ func (app *appContext) GetCustomContent(gc *gin.Context) {
"UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.MustGetCustomContentKey("UserDeleted").Enabled},
"UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserDisabled").Enabled},
"UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserEnabled").Enabled},
"UserExpiryAdjusted": {Name: app.storage.lang.Email[lang].UserExpiryAdjusted["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpiryAdjusted").Enabled},
"InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.MustGetCustomContentKey("InviteEmail").Enabled},
"WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.MustGetCustomContentKey("WelcomeEmail").Enabled},
"EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.MustGetCustomContentKey("EmailConfirmation").Enabled},
"UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpired").Enabled},
"UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.MustGetCustomContentKey("UserLogin").Enabled},
"UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.MustGetCustomContentKey("UserPage").Enabled},
"PostSignupCard": {Name: app.storage.lang.Admin[adminLang].Strings["postSignupCard"], Enabled: app.storage.MustGetCustomContentKey("PostSignupCard").Enabled, Description: app.storage.lang.Admin[adminLang].Strings["postSignupCardDescription"]},
}
filter := gc.Query("filter")
@ -54,6 +51,39 @@ func (app *appContext) GetCustomContent(gc *gin.Context) {
gc.JSON(200, list)
}
// No longer needed, these are stored by string keys in the database now.
/* func (app *appContext) getCustomMessage(id string) *CustomContent {
switch id {
case "Announcement":
return &CustomContent{}
case "UserCreated":
return &app.storage.customEmails.UserCreated
case "InviteExpiry":
return &app.storage.customEmails.InviteExpiry
case "PasswordReset":
return &app.storage.customEmails.PasswordReset
case "UserDeleted":
return &app.storage.customEmails.UserDeleted
case "UserDisabled":
return &app.storage.customEmails.UserDisabled
case "UserEnabled":
return &app.storage.customEmails.UserEnabled
case "InviteEmail":
return &app.storage.customEmails.InviteEmail
case "WelcomeEmail":
return &app.storage.customEmails.WelcomeEmail
case "EmailConfirmation":
return &app.storage.customEmails.EmailConfirmation
case "UserExpired":
return &app.storage.customEmails.UserExpired
case "UserLogin":
return &app.storage.userPage.Login
case "UserPage":
return &app.storage.userPage.Page
}
return nil
} */
// @Summary Sets the corresponding custom content.
// @Produce json
// @Param CustomContent body CustomContent true "Content = email (in markdown)."
@ -147,11 +177,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
} else if id == "UserLogin" {
variables = []string{}
customMessage.Variables = variables
} else if id == "PostSignupCard" {
variables = []string{"{username}", "{myAccountURL}"}
customMessage.Variables = variables
}
content = customMessage.Content
noContent := content == ""
if !noContent {
@ -191,11 +217,6 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
msg, err = app.email.constructEnabled("", app, true)
}
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
case "UserExpiryAdjusted":
if noContent {
msg, err = app.email.constructExpiryAdjusted("", time.Time{}, "", app, true)
}
values = app.email.expiryAdjustedValues(username, time.Now(), app.storage.lang.Email[lang].Strings.get("reason"), app, false, true)
case "InviteEmail":
if noContent {
msg, err = app.email.constructInvite("", Invite{}, app, true)
@ -216,14 +237,14 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
msg, err = app.email.constructUserExpired(app, true)
}
values = app.email.userExpiredValues(app, false)
case "UserLogin", "UserPage", "PostSignupCard":
case "UserLogin", "UserPage":
values = map[string]interface{}{}
}
if err != nil {
respondBool(500, false, gc)
return
}
if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" && id != "PostSignupCard" {
if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" {
content = msg.Text
variables = make([]string, strings.Count(content, "{"))
i := 0
@ -249,32 +270,17 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
}
app.storage.SetCustomContentKey(id, customMessage)
var mail *Message
if id != "UserLogin" && id != "UserPage" && id != "PostSignupCard" {
if id != "UserLogin" && id != "UserPage" {
mail, err = app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
if err != nil {
respondBool(500, false, gc)
return
}
} else if id == "PostSignupCard" {
// Jankiness follows.
// Source content from "Success Message" setting.
if noContent {
content = "# " + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("successHeader") + "\n" + app.config.Section("ui").Key("success_message").String()
if app.config.Section("user_page").Key("enabled").MustBool(false) {
content += "\n\n<br>\n" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.template("userPageSuccessMessage", tmpl{
"myAccount": "[" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("myAccount") + "]({myAccountURL})",
})
}
}
mail = &Message{
HTML: "<div class=\"card ~neutral dark:~d_neutral @low\"><div class=\"preview-content\"></div><br><button class=\"button ~urge dark:~d_urge @low full-width center supra submit\">" + app.storage.lang.User[app.storage.lang.chosenUserLang].Strings.get("continue") + "</a></div>",
}
mail.Markdown = mail.HTML
} else {
mail = &Message{
HTML: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
Markdown: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
}
mail.Markdown = mail.HTML
}
gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Conditionals: conditionals, Values: values, HTML: mail.HTML, Plaintext: mail.Text})
}
@ -323,14 +329,6 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
tgUser.Lang = lang
}
app.storage.SetTelegramKey(req.ID, tgUser)
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
jellyseerr.FieldTelegram: tgUser.ChatID,
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
}); err != nil {
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
linkExistingOmbiDiscordTelegram(app)
respondBool(200, true, gc)
}
@ -355,7 +353,6 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
}
func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Context) {
jsPrefs := map[jellyseerr.NotificationsField]any{}
if tgUser, ok := app.storage.GetTelegramKey(req.ID); ok {
change := tgUser.Contact != req.Telegram
tgUser.Contact = req.Telegram
@ -366,7 +363,6 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
msg = " not"
}
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
jsPrefs[jellyseerr.FieldTelegramEnabled] = req.Telegram
}
}
if dcUser, ok := app.storage.GetDiscordKey(req.ID); ok {
@ -379,7 +375,6 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
msg = " not"
}
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
jsPrefs[jellyseerr.FieldDiscordEnabled] = req.Discord
}
}
if mxUser, ok := app.storage.GetMatrixKey(req.ID); ok {
@ -404,13 +399,6 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
msg = " not"
}
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
jsPrefs[jellyseerr.FieldEmailEnabled] = req.Email
}
}
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
err := app.js.ModifyNotifications(req.ID, jsPrefs)
if err != nil {
app.err.Printf("Failed to sync contact prefs with Jellyseerr: %v", err)
}
}
respondBool(200, true, gc)
@ -697,13 +685,6 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
app.storage.SetDiscordKey(req.JellyfinID, user)
if err := app.js.ModifyNotifications(req.JellyfinID, map[jellyseerr.NotificationsField]any{
jellyseerr.FieldDiscord: req.DiscordID,
jellyseerr.FieldDiscordEnabled: true,
}); err != nil {
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked,
UserID: req.JellyfinID,
@ -734,14 +715,6 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
} */
app.storage.DeleteDiscordKey(req.ID)
// May not actually remove Discord ID, but should disable interaction.
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
jellyseerr.FieldDiscordEnabled: false,
}); err != nil {
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactUnlinked,
UserID: req.ID,
@ -771,13 +744,6 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
} */
app.storage.DeleteTelegramKey(req.ID)
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
jellyseerr.FieldTelegramEnabled: false,
}); err != nil {
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactUnlinked,
UserID: req.ID,

View File

@ -2,7 +2,6 @@ package main
import (
"fmt"
"net/url"
"github.com/gin-gonic/gin"
"github.com/hrfee/mediabrowser"
@ -96,8 +95,7 @@ func (app *appContext) OmbiUsers(gc *gin.Context) {
func (app *appContext) SetOmbiProfile(gc *gin.Context) {
var req ombiUser
gc.BindJSON(&req)
escapedProfileName := gc.Param("profile")
profileName, _ := url.QueryUnescape(escapedProfileName)
profileName := gc.Param("profile")
profile, ok := app.storage.GetProfileKey(profileName)
if !ok {
respondBool(400, false, gc)
@ -124,8 +122,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
// @Security Bearer
// @tags Ombi
func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
escapedProfileName := gc.Param("profile")
profileName, _ := url.QueryUnescape(escapedProfileName)
profileName := gc.Param("profile")
profile, ok := app.storage.GetProfileKey(profileName)
if !ok {
respondBool(400, false, gc)

View File

@ -27,7 +27,6 @@ func (app *appContext) GetProfiles(gc *gin.Context) {
LibraryAccess: p.LibraryAccess,
FromUser: p.FromUser,
Ombi: p.Ombi != nil,
Jellyseerr: p.Jellyseerr.Enabled,
ReferralsEnabled: false,
}
if referralsEnabled {

View File

@ -8,7 +8,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/hrfee/jfa-go/jellyseerr"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4"
)
@ -201,7 +200,14 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
gc.Redirect(http.StatusSeeOther, "/my/account")
return
} else if target == UserEmailChange {
app.modifyEmail(id, claims["email"].(string))
emailStore, ok := app.storage.GetEmailsKey(id)
if !ok {
emailStore = EmailAddress{
Contact: true,
}
}
emailStore.Addr = claims["email"].(string)
app.storage.SetEmailsKey(id, emailStore)
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked,
@ -212,6 +218,17 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
Time: time.Now(),
}, gc, true)
if app.config.Section("ombi").Key("enabled").MustBool(false) {
ombiUser, code, err := app.getOmbiUser(id)
if code == 200 && err == nil {
ombiUser["emailAddress"] = claims["email"].(string)
code, err = app.ombi.ModifyUser(ombiUser)
if code != 200 || err != nil {
app.err.Printf("%s: Failed to change ombi email address (%d): %v", ombiUser["userName"].(string), code, err)
}
}
}
app.info.Println("Email list modified")
gc.Redirect(http.StatusSeeOther, "/my/account")
return
@ -354,13 +371,6 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
}
app.storage.SetDiscordKey(gc.GetString("jfId"), dcUser)
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
jellyseerr.FieldDiscord: dcUser.ID,
jellyseerr.FieldDiscordEnabled: dcUser.Contact,
}); err != nil {
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked,
UserID: gc.GetString("jfId"),
@ -409,13 +419,6 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
}
app.storage.SetTelegramKey(gc.GetString("jfId"), tgUser)
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
jellyseerr.FieldTelegram: tgUser.ChatID,
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
}); err != nil {
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactLinked,
UserID: gc.GetString("jfId"),
@ -519,13 +522,6 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
app.storage.DeleteDiscordKey(gc.GetString("jfId"))
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
jellyseerr.FieldDiscordEnabled: false,
}); err != nil {
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactUnlinked,
UserID: gc.GetString("jfId"),
@ -547,13 +543,6 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
app.storage.DeleteTelegramKey(gc.GetString("jfId"))
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
jellyseerr.FieldTelegramEnabled: false,
}); err != nil {
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityContactUnlinked,
UserID: gc.GetString("jfId"),

View File

@ -9,7 +9,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt"
"github.com/hrfee/jfa-go/jellyseerr"
"github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4"
@ -95,29 +94,6 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
app.info.Println("Created Ombi user")
}
}
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
// Gets existing user (not possible) or imports the given user.
_, err := app.js.MustGetUser(id)
if err != nil {
app.err.Printf("Failed to create Jellyseerr user: %v", err)
} else {
app.info.Println("Created Jellyseerr user")
}
err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User)
if err != nil {
app.err.Printf("Failed to apply Jellyseerr user template: %v\n", err)
}
err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications)
if err != nil {
app.err.Printf("Failed to apply Jellyseerr notifications template: %v\n", err)
}
if emailEnabled {
err = app.js.ModifyUser(id, map[jellyseerr.UserField]any{jellyseerr.FieldEmail: req.Email})
if err != nil {
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
}
}
}
if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" {
app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email)
msg, err := app.email.constructWelcome(req.Username, time.Time{}, app, false)
@ -317,24 +293,24 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) {
for address, settings := range invite.Notify {
if settings["notify-creation"] {
go func(addr string) {
go func() {
msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app, false)
if err != nil {
app.err.Printf("%s: Failed to construct user creation notification: %v", req.Code, err)
} else {
// Check whether notify "addr" is an email address of Jellyfin ID
if strings.Contains(addr, "@") {
err = app.email.send(msg, addr)
// Check whether notify "address" is an email address of Jellyfin ID
if strings.Contains(address, "@") {
err = app.email.send(msg, address)
} else {
err = app.sendByID(msg, addr)
err = app.sendByID(msg, address)
}
if err != nil {
app.err.Printf("%s: Failed to send user creation notification: %v", req.Code, err)
} else {
app.info.Printf("Sent user creation notification to %s", addr)
app.info.Printf("Sent user creation notification to %s", address)
}
}
}(address)
}()
}
}
}
@ -362,10 +338,6 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
Addr: req.Email,
Contact: (req.Email != ""),
}
// Only allow disabling of email contact if some other method is available.
if req.DiscordContact || req.TelegramContact || req.MatrixContact {
emailStore.Contact = req.EmailContact
}
if invite.UserLabel != "" {
emailStore.Label = invite.UserLabel
@ -496,51 +468,6 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
app.debug.Printf("Skipping Ombi: Profile \"%s\" was empty", invite.Profile)
}
}
if invite.Profile != "" && app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
if profile.Jellyseerr.Enabled {
// Gets existing user (not possible) or imports the given user.
_, err := app.js.MustGetUser(id)
if err != nil {
app.err.Printf("Failed to create Jellyseerr user: %v", err)
} else {
app.info.Println("Created Jellyseerr user")
}
err = app.js.ApplyTemplateToUser(id, profile.Jellyseerr.User)
if err != nil {
app.err.Printf("Failed to apply Jellyseerr user template: %v\n", err)
}
err = app.js.ApplyNotificationsTemplateToUser(id, profile.Jellyseerr.Notifications)
if err != nil {
app.err.Printf("Failed to apply Jellyseerr notifications template: %v\n", err)
}
contactMethods := map[jellyseerr.NotificationsField]any{}
if emailEnabled {
err = app.js.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 {
matrixUser.Contact = req.MatrixContact
delete(app.matrix.tokens, req.MatrixPIN)
@ -812,22 +739,6 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
expiry.Expiry = base.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute)
}
app.storage.SetUserExpiryKey(id, expiry)
if messagesEnabled && req.Notify {
go func(uid string, exp time.Time) {
user, status, err := app.jf.UserByID(uid, false)
if status != 200 || err != nil {
return
}
msg, err := app.email.constructExpiryAdjusted(user.Name, exp, req.Reason, app, false)
if err != nil {
app.err.Printf("%s: Failed to construct expiry adjustment notification: %v", uid, err)
return
}
if err := app.sendByID(msg, uid); err != nil {
app.err.Printf("%s: Failed to send expiry adjustment notification: %v", uid, err)
}
}(id, expiry.Expiry)
}
}
respondBool(204, true, gc)
}
@ -1257,44 +1168,6 @@ func (app *appContext) ModifyLabels(gc *gin.Context) {
respondBool(204, true, gc)
}
func (app *appContext) modifyEmail(jfID string, addr string) {
contactPrefChanged := false
emailStore, ok := app.storage.GetEmailsKey(jfID)
// Auto enable contact by email for newly added addresses
if !ok || emailStore.Addr == "" {
emailStore = EmailAddress{
Contact: true,
}
contactPrefChanged = true
}
emailStore.Addr = addr
app.storage.SetEmailsKey(jfID, emailStore)
if app.config.Section("ombi").Key("enabled").MustBool(false) {
ombiUser, code, err := app.getOmbiUser(jfID)
if code == 200 && err == nil {
ombiUser["emailAddress"] = addr
code, err = app.ombi.ModifyUser(ombiUser)
if code != 200 || err != nil {
app.err.Printf("%s: Failed to change ombi email address (%d): %v", ombiUser["userName"].(string), code, err)
}
}
}
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
err := app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: addr})
if err != nil {
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
} else if contactPrefChanged {
contactMethods := map[jellyseerr.NotificationsField]any{
jellyseerr.FieldEmailEnabled: true,
}
err := app.js.ModifyNotifications(jfID, contactMethods)
if err != nil {
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
}
}
}
// @Summary Modify user's email addresses.
// @Produce json
// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to email addresses"
@ -1313,10 +1186,22 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
respond(500, "Couldn't get users", gc)
return
}
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
for _, jfUser := range users {
id := jfUser.ID
if address, ok := req[id]; ok {
app.modifyEmail(id, address)
var emailStore = EmailAddress{}
oldEmail, ok := app.storage.GetEmailsKey(id)
if ok {
emailStore = oldEmail
}
// Auto enable contact by email for newly added addresses
if !ok || oldEmail.Addr == "" {
emailStore.Contact = true
}
emailStore.Addr = address
app.storage.SetEmailsKey(id, emailStore)
activityType := ActivityContactLinked
if address == "" {
@ -1330,6 +1215,17 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
Value: "email",
Time: time.Now(),
}, gc, false)
if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(id)
if code == 200 && err == nil {
ombiUser["emailAddress"] = address
code, err = app.ombi.ModifyUser(ombiUser)
if code != 200 || err != nil {
app.err.Printf("%s: Failed to change ombi email address (%d): %v", ombiUser["userName"].(string), code, err)
}
}
}
}
}
app.info.Println("Email list modified")
@ -1353,8 +1249,6 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
var configuration mediabrowser.Configuration
var displayprefs map[string]interface{}
var ombi map[string]interface{}
var jellyseerr JellyseerrTemplate
jellyseerr.Enabled = false
if req.From == "profile" {
// Check profile exists & isn't empty
profile, ok := app.storage.GetProfileKey(req.Profile)
@ -1372,19 +1266,12 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
configuration = profile.Configuration
displayprefs = profile.Displayprefs
}
if req.Policy {
policy = profile.Policy
}
if req.Ombi && app.config.Section("ombi").Key("enabled").MustBool(false) {
if app.config.Section("ombi").Key("enabled").MustBool(false) {
if profile.Ombi != nil && len(profile.Ombi) != 0 {
ombi = profile.Ombi
}
}
if req.Jellyseerr && app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
if profile.Jellyseerr.Enabled {
jellyseerr = profile.Jellyseerr
}
}
} else if req.From == "user" {
applyingFrom = "user"
@ -1396,9 +1283,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
return
}
applyingFrom = "\"" + user.Name + "\""
if req.Policy {
policy = user.Policy
}
if req.Homescreen {
displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
if !(status == 200 || status == 204) || err != nil {
@ -1414,7 +1299,6 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
"policy": map[string]string{},
"homescreen": map[string]string{},
"ombi": map[string]string{},
"jellyseerr": map[string]string{},
}
/* Jellyfin doesn't seem to like too many of these requests sent in succession
and can crash and mess up its database. Issue #160 says this occurs when more
@ -1425,14 +1309,10 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
app.debug.Println("Adding delay between requests for large batch")
}
for _, id := range req.ApplyTo {
var status int
var err error
if req.Policy {
status, err = app.jf.SetPolicy(id, policy)
status, err := app.jf.SetPolicy(id, policy)
if !(status == 200 || status == 204) || err != nil {
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
}
}
if shouldDelay {
time.Sleep(250 * time.Millisecond)
}
@ -1471,26 +1351,6 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
errors["ombi"][id] = errorString
}
}
if jellyseerr.Enabled {
errorString := ""
// newUser := ombi
// newUser["id"] = user["id"]
// newUser["userName"] = user["userName"]
// newUser["alias"] = user["alias"]
// newUser["emailAddress"] = user["emailAddress"]
err := app.js.ApplyTemplateToUser(id, jellyseerr.User)
if err != nil {
errorString += fmt.Sprintf("ApplyUser: %v ", err)
}
err = app.js.ApplyNotificationsTemplateToUser(id, jellyseerr.Notifications)
if err != nil {
errorString += fmt.Sprintf("ApplyNotifications: %v ", err)
}
if errorString != "" {
errors["jellyseerr"][id] = errorString
}
}
if shouldDelay {
time.Sleep(250 * time.Millisecond)
}

View File

@ -161,13 +161,20 @@ func (app *appContext) loadPendingBackup() {
LOADBAK = ""
}
func newBackupDaemon(app *appContext) *GenericDaemon {
func newBackupDaemon(app *appContext) *housekeepingDaemon {
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
d := NewGenericDaemon(interval, app,
daemon := housekeepingDaemon{
Stopped: false,
ShutdownChannel: make(chan string),
Interval: interval,
period: interval,
app: app,
}
daemon.jobs = []func(app *appContext){
func(app *appContext) {
app.debug.Println("Backups: Creating backup")
app.makeBackup()
},
)
return d
}
return &daemon
}

View File

@ -105,9 +105,6 @@ func (app *appContext) loadConfig() error {
app.MustSetValue("user_expiry", "email_html", "jfa-go:"+"user-expired.html")
app.MustSetValue("user_expiry", "email_text", "jfa-go:"+"user-expired.txt")
app.MustSetValue("user_expiry", "adjustment_email_html", "jfa-go:"+"expiry-adjusted.html")
app.MustSetValue("user_expiry", "adjustment_email_text", "jfa-go:"+"expiry-adjusted.txt")
app.MustSetValue("matrix", "topic", "Jellyfin notifications")
app.MustSetValue("matrix", "show_on_reg", "true")

View File

@ -247,7 +247,7 @@
"requires_restart": false,
"type": "text",
"value": "Your account has been created. Click below to continue to Jellyfin.",
"description": "Displayed when a user creates an account. Use the \"post-signup card\" in the Message editor for more control."
"description": "Displayed when a user creates an account"
},
"url_base": {
"name": "Reverse Proxy subfolder",
@ -273,7 +273,7 @@
"type": "bool",
"value": false,
"advanced": true,
"description": "Navigate directly to the above URL instead of needing the user to click \"Continue\". Overrides the post-signup card."
"description": "Navigate directly to the above URL instead of needing the user to click \"Continue\"."
},
"login_appearance": {
"name": "Login screen appearance",
@ -702,7 +702,7 @@
"description": "Allow users to start a Password Reset by inputting their Discord/Telegram/Matrix username/id."
},
"pwr_note": {
"name": "PWR Methods:",
"name": "PWR Methods",
"type": "note",
"depends_true": "enabled",
"value": "",
@ -1580,66 +1580,6 @@
}
}
},
"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": {
"order": [],
"meta": {
@ -1773,7 +1713,7 @@
"order": [],
"meta": {
"name": "User Expiry",
"description": "When set on an invite, users will be deleted or disabled a specified amount of time after they create their account. Expiries can also be set and extended for invididual users, optionally with a message why."
"description": "When set on an invite, users will be deleted or disabled a specified amount of time after they create their account."
},
"settings": {
"behaviour": {
@ -1825,35 +1765,6 @@
"type": "text",
"value": "",
"description": "Path to custom email in plain text"
},
"adjustment_subject": {
"name": "Adjustment: email subject",
"required": false,
"requires_restart": false,
"depends_true": "messages|enabled",
"type": "text",
"value": "",
"description": "Subject of adjustment emails, sent optionally when setting/extending an expiry."
},
"adjustment_email_html": {
"name": "Adjustment: Custom email (HTML)",
"required": false,
"requires_restart": false,
"advanced": true,
"depends_true": "messages|enabled",
"type": "text",
"value": "",
"description": "Path to custom email html"
},
"adjustment_email_text": {
"name": "Adjustment: Custom email (plaintext)",
"required": false,
"requires_restart": false,
"advanced": true,
"depends_true": "messages|enabled",
"type": "text",
"value": "",
"description": "Path to custom email in plain text"
}
}
},

View File

@ -116,16 +116,32 @@ func (app *appContext) clearActivities() {
}
}
func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon {
d := NewGenericDaemon(interval, app,
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
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) {
app.debug.Println("Housekeeping: Checking for expired invites")
app.checkInvites()
},
func(app *appContext) { app.clearActivities() },
)
d.Name("Housekeeping daemon")
}
clearEmail := app.config.Section("email").Key("require_unique").MustBool(false)
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
@ -134,24 +150,53 @@ func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaem
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
d.appendJobs(func(app *appContext) { app.jf.CacheExpiry = time.Now() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() })
}
if clearEmail {
d.appendJobs(func(app *appContext) { app.clearEmails() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearEmails() })
}
if clearDiscord {
d.appendJobs(func(app *appContext) { app.clearDiscord() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearDiscord() })
}
if clearTelegram {
d.appendJobs(func(app *appContext) { app.clearTelegram() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearTelegram() })
}
if clearMatrix {
d.appendJobs(func(app *appContext) { app.clearMatrix() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() })
}
if clearPWR {
d.appendJobs(func(app *appContext) { app.clearPWRCaptchas() })
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearPWRCaptchas() })
}
return d
return &daemon
}
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)
}

View File

@ -741,72 +741,6 @@ func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub b
return email, nil
}
func (emailer *Emailer) expiryAdjustedValues(username string, expiry time.Time, reason string, app *appContext, noSub bool, custom bool) map[string]interface{} {
template := map[string]interface{}{
"yourExpiryWasAdjusted": emailer.lang.UserExpiryAdjusted.get("yourExpiryWasAdjusted"),
"ifPreviouslyDisabled": emailer.lang.UserExpiryAdjusted.get("ifPreviouslyDisabled"),
"reasonString": emailer.lang.Strings.get("reason"),
"newExpiry": "",
"message": "",
}
if noSub {
template["helloUser"] = emailer.lang.Strings.get("helloUser")
empty := []string{"reason", "newExpiry"}
for _, v := range empty {
template[v] = "{" + v + "}"
}
} else {
template["reason"] = reason
template["message"] = app.config.Section("messages").Key("message").String()
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username})
exp := app.formatDatetime(expiry)
if !expiry.IsZero() {
if custom {
template["newExpiry"] = exp
} else if !expiry.IsZero() {
template["newExpiry"] = emailer.lang.UserExpiryAdjusted.template("newExpiry", tmpl{
"date": exp,
})
}
}
}
return template
}
func (emailer *Emailer) constructExpiryAdjusted(username string, expiry time.Time, reason string, app *appContext, noSub bool) (*Message, error) {
email := &Message{
Subject: app.config.Section("user_expiry").Key("adjustment_subject").MustString(emailer.lang.UserExpiryAdjusted.get("title")),
}
var err error
var template map[string]interface{}
message := app.storage.MustGetCustomContentKey("UserExpiryAdjusted")
if message.Enabled {
template = emailer.expiryAdjustedValues(username, expiry, reason, app, noSub, true)
} else {
template = emailer.expiryAdjustedValues(username, expiry, reason, app, noSub, false)
}
if noSub {
template["newExpiry"] = emailer.lang.UserExpiryAdjusted.template("newExpiry", tmpl{
"date": "{newExpiry}",
})
}
if message.Enabled {
content := templateEmail(
message.Content,
message.Variables,
nil,
template,
)
email, err = emailer.constructTemplate(email.Subject, content, app)
} else {
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "user_expiry", "adjustment_email_", template)
}
if err != nil {
return nil, err
}
return email, nil
}
func (emailer *Emailer) welcomeValues(username string, expiry time.Time, app *appContext, noSub bool, custom bool) map[string]interface{} {
template := map[string]interface{}{
"welcome": emailer.lang.WelcomeEmail.get("welcome"),

View File

@ -1,65 +0,0 @@
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)
}

11
go.mod
View File

@ -16,11 +16,8 @@ replace github.com/hrfee/jfa-go/api => ./api
replace github.com/hrfee/jfa-go/easyproxy => ./easyproxy
replace github.com/hrfee/jfa-go/jellyseerr => ./jellyseerr
require (
github.com/bwmarrin/discordgo v0.27.1
github.com/dgraph-io/badger/v3 v3.2103.5
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/fatih/color v1.15.0
github.com/fsnotify/fsnotify v1.6.0
@ -31,13 +28,12 @@ require (
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769
github.com/hrfee/jfa-go/common 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/linecache v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/jfa-go/logger v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/jfa-go/ombi v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/mediabrowser v0.3.13
github.com/hrfee/mediabrowser v0.3.12
github.com/itchyny/timefmt-go v0.1.5
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mailgun/mailgun-go/v4 v4.9.1
@ -58,6 +54,7 @@ require (
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgraph-io/badger/v3 v3.2103.5 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
@ -90,7 +87,7 @@ require (
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hrfee/jfa-go/jellyseerr v0.0.0-00010101000000-000000000000 // indirect
github.com/hrfee/jfa-go/easyproxy v0.0.0-00010101000000-000000000000 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.6 // indirect

13
go.sum
View File

@ -224,8 +224,8 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hrfee/mediabrowser v0.3.13 h1:NgQNbq+JWwsP68BdWXL/rwbpfE/oO5LJ5KVkE+aNbX8=
github.com/hrfee/mediabrowser v0.3.13/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/hrfee/mediabrowser v0.3.12 h1:fqDxt1be3e+ZNjAtlKc8MTqg7peo6fuGCrk2wOXo20k=
github.com/hrfee/mediabrowser v0.3.12/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
@ -394,6 +394,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
github.com/xhit/go-simple-mail/v2 v2.13.0 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI=
github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
@ -435,6 +437,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -474,6 +478,8 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -517,6 +523,8 @@ golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -528,6 +536,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=

View File

@ -10,7 +10,6 @@
window.discordEnabled = {{ .discordEnabled }};
window.matrixEnabled = {{ .matrixEnabled }};
window.ombiEnabled = {{ .ombiEnabled }};
window.jellyseerrEnabled = {{ .jellyseerrEnabled }};
window.usernameEnabled = {{ .username }};
window.langFile = JSON.parse({{ .language }});
window.linkResetEnabled = {{ .linkResetEnabled }};
@ -84,44 +83,30 @@
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-modify-user" href="">
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">&times;</span></span>
<p class="content my-4">{{ .strings.modifySettingsDescription }}</p>
<div class="flex flex-col gap-4 my-2">
<div class="flex flex-row gap-2">
<label class="grow">
<div class="flex flex-row mb-4">
<label class="grow mr-2">
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-profile" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
</label>
<label class="grow">
<label class="grow ml-2">
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-user">
<span class="button ~neutral @low supra full-width center">{{ .strings.user }}</span>
</label>
</div>
<div class="select ~neutral @low">
<div class="select ~neutral @low mb-4">
<select id="modify-user-profiles"></select>
</div>
<div class="select ~neutral @low unfocused">
<div class="select ~neutral @low mb-4 unfocused">
<select id="modify-user-users"></select>
</div>
<label class="switch">
<input type="checkbox" id="modify-user-configuration" checked>
<span>{{ .strings.applyConfigurationAndPolicy }}</span>
</label>
<label class="switch">
<label class="switch mb-4">
<input type="checkbox" id="modify-user-homescreen" checked>
<span>{{ .strings.applyHomescreenLayout }}</span>
</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>
<input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.apply }}</span>
</label>
</div>
</form>
</div>
{{ if .referralsEnabled }}
@ -312,7 +297,6 @@
<span class="heading"><span id="header-editor"></span> <span class="modal-close">&times;</span></span>
<div class="row">
<div class="col card ~neutral @low">
<aside class="aside sm ~urge dark:~d_info mb-2 @low" id="aside-editor"></aside>
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span>
<div id="editor-variables" class="mt-4"></div>
<span class="label supra" for="editor-conditionals" id="label-editor-conditionals">{{ .strings.conditionals }}</span>
@ -411,19 +395,6 @@
</label>
</form>
</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">&times;</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 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">&times;</span></span>
@ -437,9 +408,6 @@
{{ if .ombiEnabled }}
<th>Ombi</th>
{{ end }}
{{ if .jellyseerrEnabled }}
<th>Jellyseerr</th>
{{ end }}
{{ if .referralsEnabled }}
<th>{{ .strings.referrals }}</th>
{{ end }}
@ -586,7 +554,7 @@
<div class="card @low dark:~d_neutral">
<span class="heading">{{ .strings.create }}</span>
<div class="flex flex-col md:flex-row gap-3 mt-2" id="create-inv">
<div class="card ~neutral @low flex flex-col gap-2 grow">
<div class="card ~neutral @low flex flex-col gap-2 w-1/2">
<div class="flex flex-row gap-2">
<label class="w-1/2">
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
@ -694,7 +662,7 @@
<input type="text" id="create-user-label" class="input ~neutral @low">
</div>
</div>
<div class="card ~neutral @low flex flex-col justify-between gap-2 grow">
<div class="card ~neutral @low flex flex-col justify-between gap-2 w-1/2">
<div class="flex flex-col gap-2">
<div class="flex flex-col gap-4">
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>

View File

@ -31,11 +31,6 @@
window.reCAPTCHASiteKey = "{{ .reCAPTCHASiteKey }}";
window.userPageEnabled = {{ .userPageEnabled }};
window.userPageAddress = "{{ .userPageAddress }}";
{{ if index . "customSuccessCard" }}
window.customSuccessCard = {{ .customSuccessCard }};
{{ else }}
window.customSuccessCard = false;
{{ end }}
</script>
{{ if .passwordReset }}
<script src="js/pwr.js" type="module"></script>

View File

@ -14,19 +14,12 @@
</head>
<body class="max-w-full overflow-x-hidden section">
<div id="modal-success" class="modal">
{{ if .customSuccessCard }}
<div class="card @low dark:~d_neutral content break-words relative mx-auto my-[10%] w-4/5 lg:w-1/3">
{{ .customSuccessCardContent }}
<a class="button ~urge @low full-width center supra submit my-2" href="{{ .jfLink }}" id="create-success-button">{{ .strings.continue }}</a>
</div>
{{ else }}
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
<span class="heading mb-4">{{ if .passwordReset }}{{ .strings.passwordReset }}{{ else }}{{ .strings.successHeader }}{{ end }}</span>
<p class="content mb-4">{{ if .passwordReset }}{{ .strings.youCanLoginPassword }}{{ else }}{{ .successMessage }}{{ end }}</p>
{{ if .userPageEnabled }}<p class="content mb-4" id="modal-success-user-page-area" my-account-term="{{ .strings.myAccount }}">{{ .strings.userPageSuccessMessage }}</p>{{ end }}
<a class="button ~urge @low full-width center supra submit" href="{{ .jfLink }}" id="create-success-button">{{ .strings.continue }}</a>
</div>
{{ end }}
</div>
<div id="modal-confirmation" class="modal">
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">

View File

@ -2,7 +2,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="Description" content="jfa-go, a better way to manage Jellyfin users.">
<meta name="color-scheme" content="dark light">
<meta name="robots" content="noindex">
<link rel="apple-touch-icon" sizes="180x180" href="{{ .urlBase }}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ .urlBase }}/favicon-32x32.png">

View File

@ -1,7 +0,0 @@
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

View File

@ -1,460 +0,0 @@
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
}

View File

@ -1,69 +0,0 @@
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)
}
}

View File

@ -1,136 +0,0 @@
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"`
}

View File

@ -1,81 +0,0 @@
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
}

View File

@ -101,7 +101,6 @@ type emailLang struct {
UserDeleted langSection `json:"userDeleted"`
UserDisabled langSection `json:"userDisabled"`
UserEnabled langSection `json:"userEnabled"`
UserExpiryAdjusted langSection `json:"userExpiryAdjusted"`
InviteEmail langSection `json:"inviteEmail"`
WelcomeEmail langSection `json:"welcomeEmail"`
EmailConfirmation langSection `json:"emailConfirmation"`

View File

@ -81,9 +81,6 @@
"useInviteExpiry": "Set expiry from profile/invite",
"useInviteExpiryNote": "By default, invites expire after 90 days but can be renewed by the user. Enable for the referral to be disabled after the time set.",
"applyHomescreenLayout": "Apply homescreen layout",
"applyConfigurationAndPolicy": "Apply Jellyfin configuration/policy",
"applyOmbi": "Apply Ombi profile (if available)",
"applyJellyseerr": "Apply Jellyseerr profile (if available)",
"sendDeleteNotificationEmail": "Send notification message",
"sendDeleteNotifiationExample": "Your account has been deleted.",
"settingsRestart": "Restart",
@ -102,8 +99,6 @@
"settingsMaybeUnderAdvanced": "Tip: You might find what you're looking for by enabling Advanced Settings.",
"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.",
"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",
"userProfilesDescription": "Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.",
"userProfilesIsDefault": "Default",
@ -142,8 +137,6 @@
"jellyfinID": "Jellyfin ID",
"userPageLogin": "User Page: Login",
"userPagePage": "User Page: Page",
"postSignupCard": "Post-signup help card",
"postSignupCardDescription": "Card shown to user after signing up. Overrides \"Success Message\". Overriden by \"Auto redirect on success\" setting.",
"buildTime": "Build Time",
"builtBy": "Built By",
"loginNotAdmin": "Not an Admin?",
@ -213,7 +206,6 @@
"sentAnnouncement": "Announcement sent.",
"savedAnnouncement": "Announcement saved.",
"setOmbiProfile": "Stored ombi profile.",
"savedProfile": "Stored profile changes.",
"updateApplied": "Update applied, please restart.",
"updateAppliedRefresh": "Update applied, please refresh.",
"telegramVerified": "Telegram account verified.",
@ -230,7 +222,6 @@
"errorDeleteProfile": "Failed to delete profile {n}",
"errorLoadProfiles": "Failed to load profiles.",
"errorCreateProfile": "Failed to create profile {n}",
"errorSavedProfile": "Failed to save profile {n}",
"errorSetDefaultProfile": "Failed to set default profile.",
"errorLoadUsers": "Failed to load users.",
"errorLoadSettings": "Failed to load settings.",

View File

@ -45,13 +45,6 @@
"title": "Your account has been re-enabled - Jellyfin",
"yourAccountWasEnabled": "Your account was re-enabled."
},
"userExpiryAdjusted": {
"name": "Expiry adjusted",
"title": "Account expiry adjusted - Jellyfin",
"yourExpiryWasAdjusted": "Your account's expiry date has been adjusted.",
"ifPreviouslyDisabled": "If your account was previously disabled, it may have been re-enabled.",
"newExpiry": "Your account will now expire on {date}."
},
"inviteEmail": {
"name": "Invite email",
"title": "Invite - Jellyfin",

View File

@ -1,83 +0,0 @@
<mjml>
<mj-head>
<mj-raw>
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
</mj-raw>
<mj-style>
:root {
Color-scheme: light dark;
supported-color-schemes: light dark;
}
@media (prefers-color-scheme: light) {
Color-scheme: dark;
.body {
background: #242424 !important;
background-color: #242424 !important;
}
[data-ogsc] .body {
background: #242424 !important;
background-color: #242424 !important;
}
[data-ogsb] .body {
background: #242424 !important;
background-color: #242424 !important;
}
}
@media (prefers-color-scheme: dark) {
Color-scheme: dark;
.body {
background: #242424 !important;
background-color: #242424 !important;
}
[data-ogsc] .body {
background: #242424 !important;
background-color: #242424 !important;
}
[data-ogsb] .body {
background: #242424 !important;
background-color: #242424 !important;
}
}
</mj-style>
<mj-attributes>
<mj-class name="bg" background-color="#101010" />
<mj-class name="bg2" background-color="#242424" />
<mj-class name="text" color="#cacaca" />
<mj-class name="bold" color="rgba(255,255,255,0.87)" />
<mj-class name="secondary" color="rgb(153,153,153)" />
<mj-class name="blue" background-color="rgb(0,164,220)" />
</mj-attributes>
<mj-font name="Quicksand" href="https://fonts.googleapis.com/css2?family=Quicksand" />
<mj-font name="Noto Sans" href="https://fonts.googleapis.com/css2?family=Noto+Sans" />
</mj-head>
<mj-body>
<mj-section mj-class="bg2">
<mj-column>
<mj-text mj-class="bold" font-size="25px" font-family="Quicksand, Noto Sans, Helvetica, Arial, sans-serif"> {{ .jellyfin }} </mj-text>
</mj-column>
</mj-section>
<mj-section mj-class="bg">
<mj-column>
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
<p>{{ .helloUser }}</p>
<h3>{{ .yourExpiryWasAdjusted }}</h3>
<p>{{ .ifPreviouslyDisabled }}</p>
<h4>{{ .newExpiry }}</h4>
<p>{{ .reasonString }}: <i>{{ .reason }}</i></p>
</mj-text>
</mj-column>
</mj-section>
<mj-section mj-class="bg2">
<mj-column>
<mj-text mj-class="secondary" font-style="italic" font-size="14px">
{{ .message }}
</mj-text>
</mj-column>
</mj-section>
</body>
</mjml>

View File

@ -1,11 +0,0 @@
{{ .helloUser }}
{{ .yourExpiryWasAdjusted }}
{{ .ifPreviouslyDisabled }}
{{ .newExpiry }}
{{ .reasonString }}: {{ .reason }}
{{ .message }}

29
main.go
View File

@ -25,7 +25,6 @@ import (
"github.com/hrfee/jfa-go/common"
_ "github.com/hrfee/jfa-go/docs"
"github.com/hrfee/jfa-go/easyproxy"
"github.com/hrfee/jfa-go/jellyseerr"
"github.com/hrfee/jfa-go/logger"
"github.com/hrfee/jfa-go/ombi"
"github.com/hrfee/mediabrowser"
@ -102,7 +101,6 @@ type appContext struct {
jf *mediabrowser.MediaBrowser
authJf *mediabrowser.MediaBrowser
ombi *ombi.Ombi
js *jellyseerr.Jellyseerr
datePattern string
timePattern string
storage Storage
@ -361,19 +359,6 @@ 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.loadPendingBackup()
app.ConnectDB()
@ -481,21 +466,13 @@ func start(asDaemon, firstCall bool) {
os.Exit(0)
}
invDaemon := newHousekeepingDaemon(time.Duration(60*time.Second), app)
invDaemon := newInviteDaemon(time.Duration(60*time.Second), app)
go invDaemon.run()
defer invDaemon.Shutdown()
userDaemon := newUserDaemon(time.Duration(60*time.Second), app)
go userDaemon.run()
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()
}
defer userDaemon.shutdown()
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
go app.StartPWR()
@ -505,7 +482,7 @@ func start(asDaemon, firstCall bool) {
go app.checkForUpdates()
}
var backupDaemon *GenericDaemon
var backupDaemon *housekeepingDaemon
if app.config.Section("backups").Key("enabled").MustBool(false) {
backupDaemon = newBackupDaemon(app)
go backupDaemon.run()

View File

@ -17,7 +17,6 @@ func runMigrations(app *appContext) {
linkExistingOmbiDiscordTelegram(app)
// migrateHyphens(app)
migrateToBadger(app)
intialiseCustomContent(app)
}
// Migrate pre-0.2.0 user templates to profiles
@ -330,8 +329,6 @@ func migrateToBadger(app *appContext) {
app.storage.SetCustomContentKey("UserPage", app.storage.deprecatedUserPageContent.Page)
}
// Custom content not present here was added post-badger.
err := app.storage.db.Upsert("migrated_to_db", MigrationStatus{true})
if err != nil {
app.err.Fatalf("Failed to migrate to DB: %v\n", err)
@ -339,57 +336,6 @@ func migrateToBadger(app *appContext) {
app.info.Println("All data migrated to database. JSON files in the config folder can be deleted if you are sure all data is correct in the app. Create an issue if you have problems.")
}
// Simply creates an emply CC template if not in the DB already.
// Add new CC types here!
func intialiseCustomContent(app *appContext) {
emptyCC := CustomContent{
Enabled: false,
}
if _, ok := app.storage.GetCustomContentKey("UserCreated"); !ok {
app.storage.SetCustomContentKey("UserCreated", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("InviteExpiry"); !ok {
app.storage.SetCustomContentKey("InviteExpiry", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("PasswordReset"); !ok {
app.storage.SetCustomContentKey("PasswordReset", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("UserDeleted"); !ok {
app.storage.SetCustomContentKey("UserDeleted", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("UserDisabled"); !ok {
app.storage.SetCustomContentKey("UserDisabled", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("UserEnabled"); !ok {
app.storage.SetCustomContentKey("UserEnabled", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("InviteEmail"); !ok {
app.storage.SetCustomContentKey("InviteEmail", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("WelcomeEmail"); !ok {
app.storage.SetCustomContentKey("WelcomeEmail", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("EmailConfirmation"); !ok {
app.storage.SetCustomContentKey("EmailConfirmation", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("UserExpired"); !ok {
app.storage.SetCustomContentKey("UserExpired", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("UserLogin"); !ok {
app.storage.SetCustomContentKey("UserLogin", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("UserPage"); !ok {
app.storage.SetCustomContentKey("UserPage", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("UserExpiryAdjusted"); !ok {
app.storage.SetCustomContentKey("UserExpiryAdjusted", emptyCC)
}
if _, ok := app.storage.GetCustomContentKey("PostSignupCard"); !ok {
app.storage.SetCustomContentKey("PostSignupCard", emptyCC)
}
}
// Migrate between hyphenated & non-hyphenated user IDs. Doesn't seem to happen anymore, so disabled.
// func migrateHyphens(app *appContext) {
// checkVersion := func(version string) int {

View File

@ -16,7 +16,6 @@ type newUserDTO struct {
Username string `json:"username" example:"jeff" binding:"required"` // User's username
Password string `json:"password" example:"guest" binding:"required"` // User's password
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)
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
@ -77,7 +76,6 @@ type profileDTO struct {
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
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.
}
@ -178,12 +176,7 @@ type userSettingsDTO struct {
Profile string `json:"profile"` // Name of profile (if from = "profile")
ApplyTo []string `json:"apply_to"` // Users to apply settings to
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
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 {
@ -248,7 +241,6 @@ type emailListDTO map[string]emailListEl
type emailListEl struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Description string `json:"description"`
}
type emailSetDTO struct {
@ -275,8 +267,6 @@ type extendExpiryDTO struct {
Hours int `json:"hours" example:"2"` // Number of hours to add.
Minutes int `json:"minutes" example:"3"` // Number of minutes to add.
Timestamp int64 `json:"timestamp"` // Optional, exact time to expire at. Overrides other fields.
Notify bool `json:"notify"` // Whether to message the user(s) about the change.
Reason string `json:"reason" example:"i felt like it"` // Reason for adjustment.
}
type checkUpdateDTO struct {

View File

@ -238,11 +238,6 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/users/discord/:username", app.DiscordGetUsers)
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) {
api.GET(p+"/ombi/users", app.OmbiUsers)
api.POST(p+"/profiles/ombi/:profile", app.SetOmbiProfile)

View File

@ -11,7 +11,6 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/jellyseerr"
"github.com/hrfee/jfa-go/logger"
"github.com/hrfee/mediabrowser"
"github.com/timshannon/badgerhold/v4"
@ -617,7 +616,6 @@ type customEmails struct {
UserDeleted CustomContent `json:"userDeleted"`
UserDisabled CustomContent `json:"userDisabled"`
UserEnabled CustomContent `json:"userEnabled"`
UserExpiryAdjusted CustomContent `json:"userExpiryAdjusted"`
InviteEmail CustomContent `json:"inviteEmail"`
WelcomeEmail CustomContent `json:"welcomeEmail"`
EmailConfirmation CustomContent `json:"emailConfirmation"`
@ -651,16 +649,9 @@ type Profile struct {
Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
Default bool `json:"default,omitempty"`
Ombi map[string]interface{} `json:"ombi,omitempty"`
Jellyseerr JellyseerrTemplate `json:"jellyseerr,omitempty"`
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 {
Code string `badgerhold:"key"`
Created time.Time `json:"created"`
@ -1234,7 +1225,6 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
patchLang(&lang.UserDeleted, &fallback.UserDeleted, &english.UserDeleted)
patchLang(&lang.UserDisabled, &fallback.UserDisabled, &english.UserDisabled)
patchLang(&lang.UserEnabled, &fallback.UserEnabled, &english.UserEnabled)
patchLang(&lang.UserExpiryAdjusted, &fallback.UserExpiryAdjusted, &english.UserExpiryAdjusted)
patchLang(&lang.InviteEmail, &fallback.InviteEmail, &english.InviteEmail)
patchLang(&lang.WelcomeEmail, &fallback.WelcomeEmail, &english.WelcomeEmail)
patchLang(&lang.EmailConfirmation, &fallback.EmailConfirmation, &english.EmailConfirmation)
@ -1249,7 +1239,6 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
patchLang(&lang.UserDeleted, &english.UserDeleted)
patchLang(&lang.UserDisabled, &english.UserDisabled)
patchLang(&lang.UserEnabled, &english.UserEnabled)
patchLang(&lang.UserExpiryAdjusted, &english.UserExpiryAdjusted)
patchLang(&lang.InviteEmail, &english.InviteEmail)
patchLang(&lang.WelcomeEmail, &english.WelcomeEmail)
patchLang(&lang.EmailConfirmation, &english.EmailConfirmation)

View File

@ -50,9 +50,6 @@ window.availableProfiles = window.availableProfiles || [];
window.modals.ombiProfile = new Modal(document.getElementById('modal-ombi-profile'));
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.addProfile = new Modal(document.getElementById("modal-add-profile"));
@ -187,7 +184,6 @@ login.onLogin = () => {
console.log("Logged in.");
window.updater = new Updater();
// FIXME: Decide whether to autoload activity or not
window.invites.reload()
setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000);
const currentTab = window.tabs.current;
switch (currentTab) {

View File

@ -38,7 +38,6 @@ interface formWindow extends Window {
reCAPTCHASiteKey: string;
userPageEnabled: boolean;
userPageAddress: string;
customSuccessCard: boolean;
}
loadLangSelector("form");
@ -224,7 +223,6 @@ if (window.emailRequired) {
interface sendDTO {
code: string;
email: string;
email_contact?: boolean;
username: string;
password: string;
telegram_pin?: string;
@ -253,9 +251,8 @@ const create = (event: SubmitEvent) => {
code: window.code,
username: usernameField.value,
email: emailField.value,
email_contact: true,
password: passwordField.value
}
};
if (telegramVerified) {
send.telegram_pin = window.telegramPIN;
const checkbox = document.getElementById("contact-via-telegram") as HTMLInputElement;
@ -277,10 +274,6 @@ const create = (event: SubmitEvent) => {
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.reCAPTCHA) {
send.captcha_text = grecaptcha.getResponse();
@ -303,10 +296,7 @@ const create = (event: SubmitEvent) => {
const url = ((document.getElementById("modal-success") as HTMLDivElement).querySelector("a.submit") as HTMLAnchorElement).href;
window.location.href = url;
} else {
if (window.customSuccessCard) {
const content = window.successModal.asElement().querySelector(".card");
content.innerHTML = content.innerHTML.replace(new RegExp("{username}", "g"), send.username)
} else if (window.userPageEnabled) {
if (window.userPageEnabled) {
const userPageNoticeArea = document.getElementById("modal-success-user-page-area");
const link = `<a href="${window.userPageAddress}" target="_blank">${userPageNoticeArea.getAttribute("my-account-term")}</a>`;
userPageNoticeArea.innerHTML = userPageNoticeArea.textContent.replace("{myAccount}", link);

View File

@ -795,11 +795,6 @@ export class accountsList {
private _searchBox = document.getElementById("accounts-search") as HTMLInputElement;
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 _users: { [id: string]: user };
private _ordering: string[] = [];
@ -1464,7 +1459,6 @@ export class accountsList {
const modalHeader = document.getElementById("header-modify-user");
modalHeader.textContent = window.lang.quantity("modifySettingsFor", this._collectUsers().length)
let list = this._collectUsers();
(() => {
let innerHTML = "";
for (const profile of window.availableProfiles) {
@ -1483,7 +1477,6 @@ export class accountsList {
const form = document.getElementById("form-modify-user") as HTMLFormElement;
const button = form.querySelector("span.submit") as HTMLSpanElement;
this._modifySettingsProfile.checked = true;
this._modifySettingsUser.checked = false;
form.onsubmit = (event: Event) => {
@ -1491,10 +1484,7 @@ export class accountsList {
toggleLoader(button);
let send = {
"apply_to": list,
"homescreen": this._applyHomescreen.checked,
"configuration": this._applyConfiguration.checked,
"ombi": this._applyOmbi.checked,
"jellyseerr": this._applyJellyseerr.checked
"homescreen": (document.getElementById("modify-user-homescreen") as HTMLInputElement).checked
};
if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) {
send["from"] = "profile";
@ -1694,25 +1684,22 @@ export class accountsList {
applyList.push(id);
}
this._enableExpiryReason.classList.add("unfocused");
this._enableExpiryNotify.parentElement.classList.remove("unfocused");
this._enableExpiryNotify.checked = false;
this._enableExpiryReason.value = "";
let header: string;
if (enableUser) {
header = window.lang.quantity("reEnableUsers", list.length);
this._enableExpiryNotify.parentElement.classList.remove("unfocused");
this._enableExpiryNotify.checked = false;
this._enableExpiryReason.value = "";
} else if (this._settingExpiry) {
header = window.lang.quantity("setExpiry", list.length);
// this._enableExpiryNotify.parentElement.classList.add("unfocused");
this._enableExpiryNotify.parentElement.classList.add("unfocused");
} else {
header = window.lang.quantity("extendExpiry", applyList.length);
// this._enableExpiryNotify.parentElement.classList.add("unfocused");
this._enableExpiryNotify.parentElement.classList.add("unfocused");
}
document.getElementById("header-extend-expiry").textContent = header;
const extend = () => {
let send = { "users": applyList, "timestamp": 0, "notify": this._enableExpiryNotify.checked }
if (this._enableExpiryNotify.checked) {
send["reason"] = this._enableExpiryReason.value;
}
let send = { "users": applyList, "timestamp": 0 }
if (this._usingExtendExpiryTextInput) {
let date = (Date as any).fromString(this._extendExpiryTextInput.value) as Date;
send["timestamp"] = Math.floor(date.getTime() / 1000);
@ -1741,7 +1728,7 @@ export class accountsList {
this._extendExpiryForm.onsubmit = (event: Event) => {
event.preventDefault();
if (enableUser) {
this._enableDisableUsers(applyList, true, this._enableExpiryNotify.checked, this._enableExpiryNotify.checked ? this._enableExpiryReason.value : null, (req: XMLHttpRequest) => {
this._enableDisableUsers(applyList, true, this._enableExpiryNotify.checked, this._enableExpiryNotify ? this._enableExpiryReason.value : null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200 && req.status != 204) {
window.modals.extendExpiry.close();
@ -1831,16 +1818,6 @@ export class accountsList {
};
this._modifySettings.onclick = this.modifyUsers;
this._modifySettings.classList.add("unfocused");
if (window.ombiEnabled)
this._applyOmbi.parentElement.classList.remove("unfocused");
else
this._applyOmbi.parentElement.classList.add("unfocused");
if (window.jellyseerrEnabled)
this._applyJellyseerr.parentElement.classList.remove("unfocused");
else
this._applyJellyseerr.parentElement.classList.add("unfocused");
const checkSource = () => {
const profileSpan = this._modifySettingsProfile.nextElementSibling as HTMLSpanElement;
const userSpan = this._modifySettingsUser.nextElementSibling as HTMLSpanElement;
@ -1851,8 +1828,6 @@ export class accountsList {
profileSpan.classList.remove("@low");
userSpan.classList.remove("@high");
userSpan.classList.add("@low");
this._applyOmbi.parentElement.classList.remove("unfocused");
this._applyJellyseerr.parentElement.classList.remove("unfocused");
} else {
this._userSelect.parentElement.classList.remove("unfocused");
this._profileSelect.parentElement.classList.add("unfocused");
@ -1860,8 +1835,6 @@ export class accountsList {
userSpan.classList.remove("@low");
profileSpan.classList.remove("@high");
profileSpan.classList.add("@low");
this._applyOmbi.parentElement.classList.add("unfocused");
this._applyJellyseerr.parentElement.classList.add("unfocused");
}
};
this._modifySettingsProfile.onchange = checkSource;

View File

@ -5,7 +5,6 @@ interface Profile {
libraries: string;
fromUser: string;
ombi: boolean;
jellyseerr: boolean;
referrals_enabled: boolean;
}
@ -15,11 +14,9 @@ class profile implements Profile {
private _adminChip: HTMLSpanElement;
private _libraries: HTMLTableDataCellElement;
private _ombiButton: HTMLSpanElement;
private _ombi: boolean;
private _jellyseerrButton: HTMLSpanElement;
private _jellyseerr: boolean;
private _fromUser: HTMLTableDataCellElement;
private _defaultRadio: HTMLInputElement;
private _ombi: boolean;
private _referralsButton: HTMLSpanElement;
private _referralsEnabled: boolean;
@ -55,21 +52,6 @@ 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; }
set fromUser(v: string) { this._fromUser.textContent = v; }
@ -100,9 +82,6 @@ class profile implements Profile {
if (window.ombiEnabled) innerHTML += `
<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 += `
<td><span class="button @low profile-referrals"></span></td>
`;
@ -117,8 +96,6 @@ class profile implements Profile {
this._libraries = this._row.querySelector("td.profile-libraries") as HTMLTableDataCellElement;
if (window.ombiEnabled)
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)
this._referralsButton = this._row.querySelector("span.profile-referrals") as HTMLSpanElement;
this._fromUser = this._row.querySelector("td.profile-from") as HTMLTableDataCellElement;
@ -135,12 +112,10 @@ class profile implements Profile {
this.fromUser = p.fromUser;
this.libraries = p.libraries;
this.ombi = p.ombi;
this.jellyseerr = p.jellyseerr;
this.referrals_enabled = p.referrals_enabled;
}
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); }
remove = () => { document.dispatchEvent(new CustomEvent("profiles-delete", { detail: this._name })); this._row.remove(); }
@ -169,7 +144,6 @@ export class ProfileEditor {
private _profiles: { [name: string]: profile } = {};
private _default: string;
private _ombiProfiles: ombiProfiles;
private _jellyseerrProfiles: jellyseerrProfiles;
private _createForm = document.getElementById("form-add-profile") as HTMLFormElement;
private _profileName = document.getElementById("add-profile-name") as HTMLInputElement;
@ -207,7 +181,7 @@ export class ProfileEditor {
this._profiles[name].update(name, resp.profiles[name]);
} else {
this._profiles[name] = new profile(name, resp.profiles[name]);
if (window.ombiEnabled) {
if (window.ombiEnabled)
this._profiles[name].setOmbiFunc((ombi: boolean) => {
if (ombi) {
this._ombiProfiles.delete(name, (req: XMLHttpRequest) => {
@ -224,26 +198,7 @@ export class ProfileEditor {
this._ombiProfiles.load(name);
}
});
}
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) {
if (window.referralsEnabled)
this._profiles[name].setReferralFunc((enabled: boolean) => {
if (enabled) {
this.disableReferrals(name);
@ -251,7 +206,6 @@ export class ProfileEditor {
this.enableReferrals(name);
}
});
}
this._table.appendChild(this._profiles[name].asElement());
}
}
@ -345,8 +299,6 @@ export class ProfileEditor {
if (window.ombiEnabled)
this._ombiProfiles = new ombiProfiles();
if (window.jellyseerrEnabled)
this._jellyseerrProfiles = new jellyseerrProfiles();
this._createButton.onclick = () => _get("/users", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
@ -414,7 +366,7 @@ export class ombiProfiles {
let resp = {} as ombiUser;
resp.id = this._select.value;
resp.name = this._users[resp.id];
_post("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(this._currentProfile)), resp, (req: XMLHttpRequest) => {
_post("/profiles/ombi/" + this._currentProfile, resp, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(button);
if (req.status == 200 || req.status == 204) {
@ -427,7 +379,7 @@ export class ombiProfiles {
});
}
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(profile)), null, post);
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/ombi/" + profile, null, post);
load = (profile: string) => {
this._currentProfile = profile;
@ -449,54 +401,3 @@ 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"))
}
}
});
}
}

View File

@ -1083,7 +1083,6 @@ export interface templateEmail {
interface emailListEl {
name: string;
enabled: boolean;
description: string;
}
class MessageEditor {
@ -1093,7 +1092,6 @@ class MessageEditor {
private _templ: templateEmail;
private _form = document.getElementById("form-editor") as HTMLFormElement;
private _header = document.getElementById("header-editor") as HTMLSpanElement;
private _aside = document.getElementById("aside-editor") as HTMLElement;
private _variables = document.getElementById("editor-variables") as HTMLDivElement;
private _variablesLabel = document.getElementById("label-editor-variables") as HTMLElement;
private _conditionals = document.getElementById("editor-conditionals") as HTMLDivElement;
@ -1115,12 +1113,6 @@ class MessageEditor {
if (this._names[id] !== undefined) {
this._header.textContent = this._names[id].name;
}
this._aside.classList.add("unfocused");
if (this._names[id].description != "") {
this._aside.textContent = this._names[id].description;
this._aside.classList.remove("unfocused");
}
this._templ = req.response as templateEmail;
this._textArea.value = this._templ.content;
if (this._templ.html == "") {
@ -1220,22 +1212,11 @@ class MessageEditor {
if (this._names[id].enabled) {
resetButton = `<i class="icon ri-restart-line" title="${window.lang.get("strings", "reset")}"></i>`;
}
let innerHTML = `
<td>
${this._names[id].name}
`;
if (this._names[id].description != "") innerHTML += `
<div class="tooltip right">
<i class="icon ri-information-line"></i>
<span class="content sm">${this._names[id].description}</span>
</div>
`;
innerHTML += `
</td>
tr.innerHTML = `
<td>${this._names[id].name}</td>
<td class="table-inline justify-center"><span class="customize-reset">${resetButton}</span></td>
<td><span class="button ~info @low" title="${window.lang.get("strings", "edit")}"><i class="icon ri-edit-line"></i></span></td>
`;
tr.innerHTML = innerHTML;
(tr.querySelector("span.button") as HTMLSpanElement).onclick = () => {
window.modals.customizeEmails.close()
this.loadEditor(id);

View File

@ -24,7 +24,6 @@ declare interface Window {
discordEnabled: boolean;
matrixEnabled: boolean;
ombiEnabled: boolean;
jellyseerrEnabled: boolean;
usernameEnabled: boolean;
linkResetEnabled: boolean;
token: string;
@ -102,7 +101,6 @@ declare interface Modals {
settingsRestart: Modal;
settingsRefresh: Modal;
ombiProfile?: Modal;
jellyseerrProfile?: Modal;
profiles: Modal;
addProfile: Modal;
announce: Modal;

View File

@ -7,14 +7,47 @@ import (
"github.com/lithammer/shortuuid/v3"
)
func newUserDaemon(interval time.Duration, app *appContext) *GenericDaemon {
d := NewGenericDaemon(interval, app,
func(app *appContext) {
app.checkUsers()
},
)
d.Name("User daemon")
return d
type userDaemon struct {
Stopped bool
ShutdownChannel chan string
Interval time.Duration
period time.Duration
app *appContext
}
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() {

View File

@ -133,7 +133,6 @@ func (app *appContext) AdminPage(gc *gin.Context) {
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
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)
jfAllowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
var license string
@ -165,7 +164,6 @@ func (app *appContext) AdminPage(gc *gin.Context) {
"discordEnabled": discordEnabled,
"matrixEnabled": matrixEnabled,
"ombiEnabled": ombiEnabled,
"jellyseerrEnabled": jellyseerrEnabled,
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
"notifications": notificationsEnabled,
"version": version,
@ -194,7 +192,6 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
jellyseerrEnabled := app.config.Section("jellyseerr").Key("enabled").MustBool(false)
data := gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
@ -206,7 +203,6 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
"discordEnabled": discordEnabled,
"matrixEnabled": matrixEnabled,
"ombiEnabled": ombiEnabled,
"jellyseerrEnabled": jellyseerrEnabled,
"pwrEnabled": app.config.Section("password_resets").Key("enabled").MustBool(false),
"linkResetEnabled": app.config.Section("password_resets").Key("link_reset").MustBool(false),
"notifications": notificationsEnabled,
@ -282,8 +278,6 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
"strings": app.storage.lang.PasswordReset[lang].Strings,
"success": false,
"ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false),
"jellyseerrEnabled": app.config.Section("jellyseerr").Key("enabled").MustBool(false),
"customSuccessCard": false,
}
pwr, isInternal := app.internalPWRs[pin]
// if isInternal && setPassword {
@ -740,7 +734,6 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang,
"passwordReset": false,
"customSuccessCard": false,
"telegramEnabled": telegram,
"discordEnabled": discord,
"matrixEnabled": matrix,
@ -773,22 +766,6 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.inviteChannelName != ""
}
if msg, ok := app.storage.GetCustomContentKey("PostSignupCard"); ok && msg.Enabled {
data["customSuccessCard"] = true
// We don't template here, since the username is only known after login.
data["customSuccessCardContent"] = template.HTML(markdown.ToHTML(
[]byte(templateEmail(
msg.Content,
msg.Variables,
msg.Conditionals,
map[string]interface{}{
"username": "{username}",
"myAccountURL": userPageAddress,
},
),
), nil, markdownRenderer,
))
}
// if discordEnabled {
// pin := ""