mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-26 19:10:10 +00:00
Compare commits
32 Commits
610ca635f3
...
6465a175bb
Author | SHA1 | Date | |
---|---|---|---|
|
6465a175bb | ||
efa113ab5f | |||
d60dea61db | |||
a136800ff2 | |||
db1c62cc46 | |||
1fa340f096 | |||
2a6937228c | |||
785395dd20 | |||
385953b0cb | |||
35f8337a36 | |||
769a7c45da | |||
a97bccc88f | |||
7b9cdf385a | |||
73e985c45c | |||
9c34192b4f | |||
dabef831d7 | |||
e44d11c58c | |||
48a2058e81 | |||
cd98e51ea9 | |||
fbbb03a47d | |||
6f5fc0948a | |||
05d473dc97 | |||
e9e361ae60 | |||
98d9bc62ff | |||
6f0f6e6901 | |||
c5d45355a8 | |||
1a85feb344 | |||
c75418db67 | |||
7c8e463929 | |||
8f04f49086 | |||
ec2f826dec | |||
|
97506c0bcd |
@ -80,6 +80,9 @@ 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:
|
||||
|
51
.woodpecker/git-binary.yaml
Normal file
51
.woodpecker/git-binary.yaml
Normal file
@ -0,0 +1,51 @@
|
||||
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'
|
29
.woodpecker/git-docker.yaml
Normal file
29
.woodpecker/git-docker.yaml
Normal file
@ -0,0 +1,29 @@
|
||||
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
|
||||
|
41
.woodpecker/stable-binary.yaml
Normal file
41
.woodpecker/stable-binary.yaml
Normal file
@ -0,0 +1,41 @@
|
||||
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'
|
29
.woodpecker/stable-docker.yaml
Normal file
29
.woodpecker/stable-docker.yaml
Normal file
@ -0,0 +1,29 @@
|
||||
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
|
||||
|
18
Dockerfile
18
Dockerfile
@ -1,18 +1,22 @@
|
||||
FROM --platform=$BUILDPLATFORM golang:latest AS support
|
||||
# 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
|
||||
|
||||
COPY . /opt/build
|
||||
|
||||
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) \
|
||||
# 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) \
|
||||
&& 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
|
||||
|
||||
|
100
api-jellyseerr.go
Normal file
100
api-jellyseerr.go
Normal file
@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary Get a list of Jellyseerr users.
|
||||
// @Produce json
|
||||
// @Success 200 {object} ombiUsersDTO
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /jellyseerr/users [get]
|
||||
// @Security Bearer
|
||||
// @tags Jellyseerr
|
||||
func (app *appContext) JellyseerrUsers(gc *gin.Context) {
|
||||
app.debug.Println("Jellyseerr users requested")
|
||||
users, err := app.js.GetUsers()
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to get users from Jellyseerr: %v", err)
|
||||
respond(500, "Couldn't get users", gc)
|
||||
return
|
||||
}
|
||||
app.debug.Printf("Jellyseerr users retrieved: %d", len(users))
|
||||
userlist := make([]ombiUser, len(users))
|
||||
i := 0
|
||||
for _, u := range users {
|
||||
userlist[i] = ombiUser{
|
||||
Name: u.Name(),
|
||||
ID: strconv.FormatInt(u.ID, 10),
|
||||
}
|
||||
i++
|
||||
}
|
||||
gc.JSON(200, ombiUsersDTO{Users: userlist})
|
||||
}
|
||||
|
||||
// @Summary Store Jellyseerr user template in an existing profile.
|
||||
// @Produce json
|
||||
// @Param id path string true "Jellyseerr ID of user to source from"
|
||||
// @Param profile path string true "Name of profile to store in"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /profiles/jellyseerr/{profile}/{id} [post]
|
||||
// @Security Bearer
|
||||
// @tags Jellyseerr
|
||||
func (app *appContext) SetJellyseerrProfile(gc *gin.Context) {
|
||||
jellyseerrID, err := strconv.ParseInt(gc.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
escapedProfileName := gc.Param("profile")
|
||||
profileName, _ := url.QueryUnescape(escapedProfileName)
|
||||
profile, ok := app.storage.GetProfileKey(profileName)
|
||||
if !ok {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
u, err := app.js.UserByID(jellyseerrID)
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't get user from Jellyseerr: %v", err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
profile.Jellyseerr.User = u.UserTemplate
|
||||
n, err := app.js.GetNotificationPreferencesByID(jellyseerrID)
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't get user's notification prefs from Jellyseerr: %v", err)
|
||||
respond(500, "Couldn't get user notification prefs", gc)
|
||||
return
|
||||
}
|
||||
profile.Jellyseerr.Notifications = n.NotificationsTemplate
|
||||
profile.Jellyseerr.Enabled = true
|
||||
app.storage.SetProfileKey(profileName, profile)
|
||||
respondBool(204, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Remove jellyseerr user template from a profile.
|
||||
// @Produce json
|
||||
// @Param profile path string true "Name of profile to store in"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /profiles/jellyseerr/{profile} [delete]
|
||||
// @Security Bearer
|
||||
// @tags Jellyseerr
|
||||
func (app *appContext) DeleteJellyseerrProfile(gc *gin.Context) {
|
||||
escapedProfileName := gc.Param("profile")
|
||||
profileName, _ := url.QueryUnescape(escapedProfileName)
|
||||
profile, ok := app.storage.GetProfileKey(profileName)
|
||||
if !ok {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
profile.Jellyseerr.Enabled = false
|
||||
app.storage.SetProfileKey(profileName, profile)
|
||||
respondBool(204, true, gc)
|
||||
}
|
108
api-messages.go
108
api-messages.go
@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/jellyseerr"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
@ -32,12 +33,14 @@ 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")
|
||||
@ -51,39 +54,6 @@ 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)."
|
||||
@ -177,7 +147,11 @@ 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 {
|
||||
@ -217,6 +191,11 @@ 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)
|
||||
@ -237,14 +216,14 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
||||
msg, err = app.email.constructUserExpired(app, true)
|
||||
}
|
||||
values = app.email.userExpiredValues(app, false)
|
||||
case "UserLogin", "UserPage":
|
||||
case "UserLogin", "UserPage", "PostSignupCard":
|
||||
values = map[string]interface{}{}
|
||||
}
|
||||
if err != nil {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" {
|
||||
if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" && id != "PostSignupCard" {
|
||||
content = msg.Text
|
||||
variables = make([]string, strings.Count(content, "{"))
|
||||
i := 0
|
||||
@ -270,17 +249,32 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
||||
}
|
||||
app.storage.SetCustomContentKey(id, customMessage)
|
||||
var mail *Message
|
||||
if id != "UserLogin" && id != "UserPage" {
|
||||
if id != "UserLogin" && id != "UserPage" && id != "PostSignupCard" {
|
||||
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})
|
||||
}
|
||||
@ -329,6 +323,14 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
||||
tgUser.Lang = lang
|
||||
}
|
||||
app.storage.SetTelegramKey(req.ID, tgUser)
|
||||
|
||||
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
|
||||
jellyseerr.FieldTelegram: tgUser.ChatID,
|
||||
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
}
|
||||
|
||||
linkExistingOmbiDiscordTelegram(app)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@ -353,6 +355,7 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
}
|
||||
|
||||
func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Context) {
|
||||
jsPrefs := map[jellyseerr.NotificationsField]any{}
|
||||
if tgUser, ok := app.storage.GetTelegramKey(req.ID); ok {
|
||||
change := tgUser.Contact != req.Telegram
|
||||
tgUser.Contact = req.Telegram
|
||||
@ -363,6 +366,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
|
||||
jsPrefs[jellyseerr.FieldTelegramEnabled] = req.Telegram
|
||||
}
|
||||
}
|
||||
if dcUser, ok := app.storage.GetDiscordKey(req.ID); ok {
|
||||
@ -375,6 +379,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||
jsPrefs[jellyseerr.FieldDiscordEnabled] = req.Discord
|
||||
}
|
||||
}
|
||||
if mxUser, ok := app.storage.GetMatrixKey(req.ID); ok {
|
||||
@ -399,6 +404,13 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
|
||||
jsPrefs[jellyseerr.FieldEmailEnabled] = req.Email
|
||||
}
|
||||
}
|
||||
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
||||
err := app.js.ModifyNotifications(req.ID, jsPrefs)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to sync contact prefs with Jellyseerr: %v", err)
|
||||
}
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
@ -685,6 +697,13 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
|
||||
|
||||
app.storage.SetDiscordKey(req.JellyfinID, user)
|
||||
|
||||
if err := app.js.ModifyNotifications(req.JellyfinID, map[jellyseerr.NotificationsField]any{
|
||||
jellyseerr.FieldDiscord: req.DiscordID,
|
||||
jellyseerr.FieldDiscordEnabled: true,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityContactLinked,
|
||||
UserID: req.JellyfinID,
|
||||
@ -715,6 +734,14 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
|
||||
} */
|
||||
app.storage.DeleteDiscordKey(req.ID)
|
||||
|
||||
// May not actually remove Discord ID, but should disable interaction.
|
||||
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
|
||||
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldDiscordEnabled: false,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityContactUnlinked,
|
||||
UserID: req.ID,
|
||||
@ -744,6 +771,13 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
|
||||
} */
|
||||
app.storage.DeleteTelegramKey(req.ID)
|
||||
|
||||
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
|
||||
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldTelegramEnabled: false,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityContactUnlinked,
|
||||
UserID: req.ID,
|
||||
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
@ -95,7 +96,8 @@ func (app *appContext) OmbiUsers(gc *gin.Context) {
|
||||
func (app *appContext) SetOmbiProfile(gc *gin.Context) {
|
||||
var req ombiUser
|
||||
gc.BindJSON(&req)
|
||||
profileName := gc.Param("profile")
|
||||
escapedProfileName := gc.Param("profile")
|
||||
profileName, _ := url.QueryUnescape(escapedProfileName)
|
||||
profile, ok := app.storage.GetProfileKey(profileName)
|
||||
if !ok {
|
||||
respondBool(400, false, gc)
|
||||
@ -122,7 +124,8 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @tags Ombi
|
||||
func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
|
||||
profileName := gc.Param("profile")
|
||||
escapedProfileName := gc.Param("profile")
|
||||
profileName, _ := url.QueryUnescape(escapedProfileName)
|
||||
profile, ok := app.storage.GetProfileKey(profileName)
|
||||
if !ok {
|
||||
respondBool(400, false, gc)
|
||||
|
@ -27,6 +27,7 @@ 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 {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/hrfee/jfa-go/jellyseerr"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
@ -200,14 +201,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
||||
gc.Redirect(http.StatusSeeOther, "/my/account")
|
||||
return
|
||||
} else if target == UserEmailChange {
|
||||
emailStore, ok := app.storage.GetEmailsKey(id)
|
||||
if !ok {
|
||||
emailStore = EmailAddress{
|
||||
Contact: true,
|
||||
}
|
||||
}
|
||||
emailStore.Addr = claims["email"].(string)
|
||||
app.storage.SetEmailsKey(id, emailStore)
|
||||
app.modifyEmail(id, claims["email"].(string))
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityContactLinked,
|
||||
@ -218,17 +212,6 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
||||
Time: time.Now(),
|
||||
}, gc, true)
|
||||
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
ombiUser, code, err := app.getOmbiUser(id)
|
||||
if code == 200 && err == nil {
|
||||
ombiUser["emailAddress"] = claims["email"].(string)
|
||||
code, err = app.ombi.ModifyUser(ombiUser)
|
||||
if code != 200 || err != nil {
|
||||
app.err.Printf("%s: Failed to change ombi email address (%d): %v", ombiUser["userName"].(string), code, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.info.Println("Email list modified")
|
||||
gc.Redirect(http.StatusSeeOther, "/my/account")
|
||||
return
|
||||
@ -371,6 +354,13 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
||||
}
|
||||
app.storage.SetDiscordKey(gc.GetString("jfId"), dcUser)
|
||||
|
||||
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
|
||||
jellyseerr.FieldDiscord: dcUser.ID,
|
||||
jellyseerr.FieldDiscordEnabled: dcUser.Contact,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityContactLinked,
|
||||
UserID: gc.GetString("jfId"),
|
||||
@ -419,6 +409,13 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
|
||||
}
|
||||
app.storage.SetTelegramKey(gc.GetString("jfId"), tgUser)
|
||||
|
||||
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
|
||||
jellyseerr.FieldTelegram: tgUser.ChatID,
|
||||
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityContactLinked,
|
||||
UserID: gc.GetString("jfId"),
|
||||
@ -522,6 +519,13 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
|
||||
func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
|
||||
app.storage.DeleteDiscordKey(gc.GetString("jfId"))
|
||||
|
||||
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
|
||||
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldDiscordEnabled: false,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityContactUnlinked,
|
||||
UserID: gc.GetString("jfId"),
|
||||
@ -543,6 +547,13 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
|
||||
func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
|
||||
app.storage.DeleteTelegramKey(gc.GetString("jfId"))
|
||||
|
||||
if err := app.js.ModifyNotifications(gc.GetString("jfId"), map[jellyseerr.NotificationsField]any{
|
||||
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldTelegramEnabled: false,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityContactUnlinked,
|
||||
UserID: gc.GetString("jfId"),
|
||||
|
206
api-users.go
206
api-users.go
@ -9,6 +9,7 @@ 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"
|
||||
@ -94,6 +95,29 @@ 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)
|
||||
@ -293,24 +317,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() {
|
||||
go func(addr string) {
|
||||
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 "address" is an email address of Jellyfin ID
|
||||
if strings.Contains(address, "@") {
|
||||
err = app.email.send(msg, address)
|
||||
// Check whether notify "addr" is an email address of Jellyfin ID
|
||||
if strings.Contains(addr, "@") {
|
||||
err = app.email.send(msg, addr)
|
||||
} else {
|
||||
err = app.sendByID(msg, address)
|
||||
err = app.sendByID(msg, addr)
|
||||
}
|
||||
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", address)
|
||||
app.info.Printf("Sent user creation notification to %s", addr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -338,6 +362,10 @@ 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
|
||||
@ -468,6 +496,51 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool, gc *gin.Context)
|
||||
app.debug.Printf("Skipping Ombi: Profile \"%s\" was empty", invite.Profile)
|
||||
}
|
||||
}
|
||||
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)
|
||||
@ -739,6 +812,22 @@ 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)
|
||||
}
|
||||
@ -1168,6 +1257,44 @@ func (app *appContext) ModifyLabels(gc *gin.Context) {
|
||||
respondBool(204, true, gc)
|
||||
}
|
||||
|
||||
func (app *appContext) modifyEmail(jfID string, addr string) {
|
||||
contactPrefChanged := false
|
||||
emailStore, ok := app.storage.GetEmailsKey(jfID)
|
||||
// Auto enable contact by email for newly added addresses
|
||||
if !ok || emailStore.Addr == "" {
|
||||
emailStore = EmailAddress{
|
||||
Contact: true,
|
||||
}
|
||||
contactPrefChanged = true
|
||||
}
|
||||
emailStore.Addr = addr
|
||||
app.storage.SetEmailsKey(jfID, emailStore)
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
ombiUser, code, err := app.getOmbiUser(jfID)
|
||||
if code == 200 && err == nil {
|
||||
ombiUser["emailAddress"] = addr
|
||||
code, err = app.ombi.ModifyUser(ombiUser)
|
||||
if code != 200 || err != nil {
|
||||
app.err.Printf("%s: Failed to change ombi email address (%d): %v", ombiUser["userName"].(string), code, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
||||
err := app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: addr})
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
|
||||
} else if contactPrefChanged {
|
||||
contactMethods := map[jellyseerr.NotificationsField]any{
|
||||
jellyseerr.FieldEmailEnabled: true,
|
||||
}
|
||||
err := app.js.ModifyNotifications(jfID, contactMethods)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Modify user's email addresses.
|
||||
// @Produce json
|
||||
// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to email addresses"
|
||||
@ -1186,22 +1313,10 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
respond(500, "Couldn't get users", gc)
|
||||
return
|
||||
}
|
||||
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
||||
for _, jfUser := range users {
|
||||
id := jfUser.ID
|
||||
if address, ok := req[id]; ok {
|
||||
var emailStore = EmailAddress{}
|
||||
oldEmail, ok := app.storage.GetEmailsKey(id)
|
||||
if ok {
|
||||
emailStore = oldEmail
|
||||
}
|
||||
// Auto enable contact by email for newly added addresses
|
||||
if !ok || oldEmail.Addr == "" {
|
||||
emailStore.Contact = true
|
||||
}
|
||||
|
||||
emailStore.Addr = address
|
||||
app.storage.SetEmailsKey(id, emailStore)
|
||||
app.modifyEmail(id, address)
|
||||
|
||||
activityType := ActivityContactLinked
|
||||
if address == "" {
|
||||
@ -1215,17 +1330,6 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
Value: "email",
|
||||
Time: time.Now(),
|
||||
}, gc, false)
|
||||
|
||||
if ombiEnabled {
|
||||
ombiUser, code, err := app.getOmbiUser(id)
|
||||
if code == 200 && err == nil {
|
||||
ombiUser["emailAddress"] = address
|
||||
code, err = app.ombi.ModifyUser(ombiUser)
|
||||
if code != 200 || err != nil {
|
||||
app.err.Printf("%s: Failed to change ombi email address (%d): %v", ombiUser["userName"].(string), code, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
app.info.Println("Email list modified")
|
||||
@ -1249,6 +1353,8 @@ 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)
|
||||
@ -1266,12 +1372,19 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
||||
configuration = profile.Configuration
|
||||
displayprefs = profile.Displayprefs
|
||||
}
|
||||
if req.Policy {
|
||||
policy = profile.Policy
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
}
|
||||
if req.Ombi && app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
if profile.Ombi != nil && len(profile.Ombi) != 0 {
|
||||
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"
|
||||
@ -1283,7 +1396,9 @@ 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 {
|
||||
@ -1299,6 +1414,7 @@ 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
|
||||
@ -1309,10 +1425,14 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
|
||||
app.debug.Println("Adding delay between requests for large batch")
|
||||
}
|
||||
for _, id := range req.ApplyTo {
|
||||
status, err := app.jf.SetPolicy(id, policy)
|
||||
var status int
|
||||
var err error
|
||||
if req.Policy {
|
||||
status, err = app.jf.SetPolicy(id, policy)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
errors["policy"][id] = fmt.Sprintf("%d: %s", status, err)
|
||||
}
|
||||
}
|
||||
if shouldDelay {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
@ -1351,6 +1471,26 @@ 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)
|
||||
}
|
||||
|
15
backups.go
15
backups.go
@ -161,20 +161,13 @@ func (app *appContext) loadPendingBackup() {
|
||||
LOADBAK = ""
|
||||
}
|
||||
|
||||
func newBackupDaemon(app *appContext) *housekeepingDaemon {
|
||||
func newBackupDaemon(app *appContext) *GenericDaemon {
|
||||
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
|
||||
daemon := housekeepingDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
Interval: interval,
|
||||
period: interval,
|
||||
app: app,
|
||||
}
|
||||
daemon.jobs = []func(app *appContext){
|
||||
d := NewGenericDaemon(interval, app,
|
||||
func(app *appContext) {
|
||||
app.debug.Println("Backups: Creating backup")
|
||||
app.makeBackup()
|
||||
},
|
||||
}
|
||||
return &daemon
|
||||
)
|
||||
return d
|
||||
}
|
||||
|
@ -105,6 +105,9 @@ 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")
|
||||
|
||||
|
@ -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"
|
||||
"description": "Displayed when a user creates an account. Use the \"post-signup card\" in the Message editor for more control."
|
||||
},
|
||||
"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\"."
|
||||
"description": "Navigate directly to the above URL instead of needing the user to click \"Continue\". Overrides the post-signup card."
|
||||
},
|
||||
"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,6 +1580,66 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"jellyseerr": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Jellyseerr Integration",
|
||||
"description": "Connect to Jellyseerr to automatically trigger the import of users on account creation, and to automatically link contact methods (email, discord and telegram). A template must be added to a User Profile for accounts to be created."
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enable the Jellyseerr integration."
|
||||
},
|
||||
"usertype_note": {
|
||||
"name": "Password Changes:",
|
||||
"type": "note",
|
||||
"value": "",
|
||||
"depends_true": "enabled",
|
||||
"required": "false",
|
||||
"description": "Ensure existing users on Jellyseerr are \"Jellyfin User\"s not \"Local User\"s, as password changes are not synced with Jellyseerr."
|
||||
},
|
||||
"server": {
|
||||
"name": "URL",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "localhost:5000",
|
||||
"depends_true": "enabled",
|
||||
"description": "Jellyseerr server URL."
|
||||
},
|
||||
"api_key": {
|
||||
"name": "API Key",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"depends_true": "enabled",
|
||||
"description": "API Key. Get this from the first tab in Jellyseerr's settings."
|
||||
},
|
||||
"import_existing": {
|
||||
"name": "Import existing users to Jellyseerr",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"depends_true": "enabled",
|
||||
"description": "Existing users (and those created outside jfa-go) will have their contact info imported to Jellyseerr."
|
||||
},
|
||||
"constraints_note": {
|
||||
"name": "Unique Emails:",
|
||||
"type": "note",
|
||||
"value": "",
|
||||
"depends_true": "import_existing",
|
||||
"required": "false",
|
||||
"description": "Jellyseerr requires email addresses to be unique. If this is not the case, you may see errors in jfa-go's logs. You can require unique addresses in Settings > Email."
|
||||
}
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
@ -1713,7 +1773,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."
|
||||
"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."
|
||||
},
|
||||
"settings": {
|
||||
"behaviour": {
|
||||
@ -1765,6 +1825,35 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
69
daemon.go
69
daemon.go
@ -116,32 +116,16 @@ func (app *appContext) clearActivities() {
|
||||
}
|
||||
}
|
||||
|
||||
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
||||
|
||||
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 newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||
d := NewGenericDaemon(interval, app,
|
||||
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)
|
||||
@ -150,53 +134,24 @@ func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemo
|
||||
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
||||
|
||||
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
||||
d.appendJobs(func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
||||
}
|
||||
|
||||
if clearEmail {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearEmails() })
|
||||
d.appendJobs(func(app *appContext) { app.clearEmails() })
|
||||
}
|
||||
if clearDiscord {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearDiscord() })
|
||||
d.appendJobs(func(app *appContext) { app.clearDiscord() })
|
||||
}
|
||||
if clearTelegram {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearTelegram() })
|
||||
d.appendJobs(func(app *appContext) { app.clearTelegram() })
|
||||
}
|
||||
if clearMatrix {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() })
|
||||
d.appendJobs(func(app *appContext) { app.clearMatrix() })
|
||||
}
|
||||
if clearPWR {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearPWRCaptchas() })
|
||||
d.appendJobs(func(app *appContext) { app.clearPWRCaptchas() })
|
||||
}
|
||||
|
||||
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)
|
||||
return d
|
||||
}
|
||||
|
66
email.go
66
email.go
@ -741,6 +741,72 @@ 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"),
|
||||
|
65
genericdaemon.go
Normal file
65
genericdaemon.go
Normal file
@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import "time"
|
||||
|
||||
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
||||
|
||||
type GenericDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
Interval time.Duration
|
||||
period time.Duration
|
||||
jobs []func(app *appContext)
|
||||
app *appContext
|
||||
name string
|
||||
}
|
||||
|
||||
func (d *GenericDaemon) appendJobs(jobs ...func(app *appContext)) {
|
||||
d.jobs = append(d.jobs, jobs...)
|
||||
}
|
||||
|
||||
// NewGenericDaemon returns a daemon which can be given jobs that utilize appContext.
|
||||
func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app *appContext)) *GenericDaemon {
|
||||
d := GenericDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
Interval: interval,
|
||||
period: interval,
|
||||
app: app,
|
||||
name: "Generic Daemon",
|
||||
}
|
||||
d.jobs = jobs
|
||||
return &d
|
||||
|
||||
}
|
||||
|
||||
func (d *GenericDaemon) Name(name string) { d.name = name }
|
||||
|
||||
func (d *GenericDaemon) run() {
|
||||
d.app.info.Printf("%s started", d.name)
|
||||
for {
|
||||
select {
|
||||
case <-d.ShutdownChannel:
|
||||
d.ShutdownChannel <- "Down"
|
||||
return
|
||||
case <-time.After(d.period):
|
||||
break
|
||||
}
|
||||
started := time.Now()
|
||||
|
||||
for _, job := range d.jobs {
|
||||
job(d.app)
|
||||
}
|
||||
|
||||
finished := time.Now()
|
||||
duration := finished.Sub(started)
|
||||
d.period = d.Interval - duration
|
||||
}
|
||||
}
|
||||
|
||||
func (d *GenericDaemon) Shutdown() {
|
||||
d.Stopped = true
|
||||
d.ShutdownChannel <- "Down"
|
||||
<-d.ShutdownChannel
|
||||
close(d.ShutdownChannel)
|
||||
}
|
11
go.mod
11
go.mod
@ -16,8 +16,11 @@ 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
|
||||
@ -28,12 +31,13 @@ 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-20230626224816-f72960635dc3
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769
|
||||
github.com/hrfee/jfa-go/docs v0.0.0-20230626224816-f72960635dc3
|
||||
github.com/hrfee/jfa-go/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.12
|
||||
github.com/hrfee/mediabrowser v0.3.13
|
||||
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
|
||||
@ -54,7 +58,6 @@ 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
|
||||
@ -87,7 +90,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/easyproxy v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/hrfee/jfa-go/jellyseerr 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
13
go.sum
@ -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.12 h1:fqDxt1be3e+ZNjAtlKc8MTqg7peo6fuGCrk2wOXo20k=
|
||||
github.com/hrfee/mediabrowser v0.3.12/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||
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/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,8 +394,6 @@ 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=
|
||||
@ -437,8 +435,6 @@ 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=
|
||||
@ -478,8 +474,6 @@ 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=
|
||||
@ -523,8 +517,6 @@ 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=
|
||||
@ -536,7 +528,6 @@ 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=
|
||||
|
@ -10,6 +10,7 @@
|
||||
window.discordEnabled = {{ .discordEnabled }};
|
||||
window.matrixEnabled = {{ .matrixEnabled }};
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.jellyseerrEnabled = {{ .jellyseerrEnabled }};
|
||||
window.usernameEnabled = {{ .username }};
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
window.linkResetEnabled = {{ .linkResetEnabled }};
|
||||
@ -83,30 +84,44 @@
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-modify-user" href="">
|
||||
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.modifySettingsDescription }}</p>
|
||||
<div class="flex flex-row mb-4">
|
||||
<label class="grow mr-2">
|
||||
<div class="flex flex-col gap-4 my-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<label class="grow">
|
||||
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-profile" checked>
|
||||
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
|
||||
</label>
|
||||
<label class="grow ml-2">
|
||||
<label class="grow">
|
||||
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-user">
|
||||
<span class="button ~neutral @low supra full-width center">{{ .strings.user }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="select ~neutral @low mb-4">
|
||||
<div class="select ~neutral @low">
|
||||
<select id="modify-user-profiles"></select>
|
||||
</div>
|
||||
<div class="select ~neutral @low mb-4 unfocused">
|
||||
<div class="select ~neutral @low unfocused">
|
||||
<select id="modify-user-users"></select>
|
||||
</div>
|
||||
<label class="switch mb-4">
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="modify-user-configuration" checked>
|
||||
<span>{{ .strings.applyConfigurationAndPolicy }}</span>
|
||||
</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="modify-user-homescreen" checked>
|
||||
<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 }}
|
||||
@ -297,6 +312,7 @@
|
||||
<span class="heading"><span id="header-editor"></span> <span class="modal-close">×</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>
|
||||
@ -395,6 +411,19 @@
|
||||
</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">×</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">×</span></span>
|
||||
@ -408,6 +437,9 @@
|
||||
{{ if .ombiEnabled }}
|
||||
<th>Ombi</th>
|
||||
{{ end }}
|
||||
{{ if .jellyseerrEnabled }}
|
||||
<th>Jellyseerr</th>
|
||||
{{ end }}
|
||||
{{ if .referralsEnabled }}
|
||||
<th>{{ .strings.referrals }}</th>
|
||||
{{ end }}
|
||||
@ -554,7 +586,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 w-1/2">
|
||||
<div class="card ~neutral @low flex flex-col gap-2 grow">
|
||||
<div class="flex flex-row gap-2">
|
||||
<label class="w-1/2">
|
||||
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
|
||||
@ -662,7 +694,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 w-1/2">
|
||||
<div class="card ~neutral @low flex flex-col justify-between gap-2 grow">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-4">
|
||||
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
|
||||
|
@ -31,6 +31,11 @@
|
||||
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>
|
||||
|
@ -14,12 +14,19 @@
|
||||
</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">
|
||||
|
@ -2,6 +2,7 @@
|
||||
<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">
|
||||
|
7
jellyseerr/go.mod
Normal file
7
jellyseerr/go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module github.com/hrfee/jfa-go/jellyseerr
|
||||
|
||||
replace github.com/hrfee/jfa-go/common => ../common
|
||||
|
||||
go 1.18
|
||||
|
||||
require github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769 // indirect
|
460
jellyseerr/jellyseerr.go
Normal file
460
jellyseerr/jellyseerr.go
Normal file
@ -0,0 +1,460 @@
|
||||
package jellyseerr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
)
|
||||
|
||||
const (
|
||||
API_SUFFIX = "/api/v1"
|
||||
BogusIdentifier = "123412341234123456"
|
||||
)
|
||||
|
||||
// Jellyseerr represents a running Jellyseerr instance.
|
||||
type Jellyseerr struct {
|
||||
server, key string
|
||||
header map[string]string
|
||||
httpClient *http.Client
|
||||
userCache map[string]User // Map of jellyfin IDs to users
|
||||
cacheExpiry time.Time
|
||||
cacheLength time.Duration
|
||||
timeoutHandler common.TimeoutHandler
|
||||
LogRequestBodies bool
|
||||
AutoImportUsers bool
|
||||
}
|
||||
|
||||
// NewJellyseerr returns an Ombi object.
|
||||
func NewJellyseerr(server, key string, timeoutHandler common.TimeoutHandler) *Jellyseerr {
|
||||
if !strings.HasSuffix(server, API_SUFFIX) {
|
||||
server = server + API_SUFFIX
|
||||
}
|
||||
return &Jellyseerr{
|
||||
server: server,
|
||||
key: key,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
header: map[string]string{
|
||||
"X-Api-Key": key,
|
||||
},
|
||||
cacheLength: time.Duration(30) * time.Minute,
|
||||
cacheExpiry: time.Now(),
|
||||
timeoutHandler: timeoutHandler,
|
||||
userCache: map[string]User{},
|
||||
LogRequestBodies: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) req(mode string, uri string, data any, queryParams url.Values, headers map[string]string, response bool) (string, int, error) {
|
||||
var params []byte
|
||||
if data != nil {
|
||||
params, _ = json.Marshal(data)
|
||||
}
|
||||
if js.LogRequestBodies {
|
||||
fmt.Printf("Jellyseerr API Client: Sending Data \"%s\" to \"%s\"\n", string(params), uri)
|
||||
}
|
||||
if qp := queryParams.Encode(); qp != "" {
|
||||
uri += "?" + qp
|
||||
}
|
||||
var req *http.Request
|
||||
if data != nil {
|
||||
req, _ = http.NewRequest(mode, uri, bytes.NewBuffer(params))
|
||||
} else {
|
||||
req, _ = http.NewRequest(mode, uri, nil)
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
for name, value := range js.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
if headers != nil {
|
||||
for name, value := range headers {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
}
|
||||
resp, err := js.httpClient.Do(req)
|
||||
reqFailed := err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201)
|
||||
defer js.timeoutHandler()
|
||||
var responseText string
|
||||
defer resp.Body.Close()
|
||||
if response || reqFailed {
|
||||
responseText, err = js.decodeResp(resp)
|
||||
if err != nil {
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
}
|
||||
if reqFailed {
|
||||
var msg ErrorDTO
|
||||
err = json.Unmarshal([]byte(responseText), &msg)
|
||||
if err != nil {
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
if msg.Message == "" {
|
||||
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
|
||||
} else {
|
||||
err = fmt.Errorf("got %d: %s", resp.StatusCode, msg.Message)
|
||||
}
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) decodeResp(resp *http.Response) (string, error) {
|
||||
var out io.Reader
|
||||
switch resp.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
out, _ = gzip.NewReader(resp.Body)
|
||||
default:
|
||||
out = resp.Body
|
||||
}
|
||||
buf := new(strings.Builder)
|
||||
_, err := io.Copy(buf, out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) get(uri string, data any, params url.Values) (string, int, error) {
|
||||
return js.req(http.MethodGet, uri, data, params, nil, true)
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) post(uri string, data any, response bool) (string, int, error) {
|
||||
return js.req(http.MethodPost, uri, data, url.Values{}, nil, response)
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) put(uri string, data any, response bool) (string, int, error) {
|
||||
return js.req(http.MethodPut, uri, data, url.Values{}, nil, response)
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) delete(uri string, data any) (int, error) {
|
||||
_, status, err := js.req(http.MethodDelete, uri, data, url.Values{}, nil, false)
|
||||
return status, err
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) ImportFromJellyfin(jfIDs ...string) ([]User, error) {
|
||||
params := map[string]interface{}{
|
||||
"jellyfinUserIds": jfIDs,
|
||||
}
|
||||
resp, status, err := js.post(js.server+"/user/import-from-jellyfin", params, true)
|
||||
var data []User
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
err = json.Unmarshal([]byte(resp), &data)
|
||||
for _, u := range data {
|
||||
if u.JellyfinUserID != "" {
|
||||
js.userCache[u.JellyfinUserID] = u
|
||||
}
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) getUsers() error {
|
||||
if js.cacheExpiry.After(time.Now()) {
|
||||
return nil
|
||||
}
|
||||
js.cacheExpiry = time.Now().Add(js.cacheLength)
|
||||
pageCount := 1
|
||||
pageIndex := 0
|
||||
for {
|
||||
res, err := js.getUserPage(pageIndex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, u := range res.Results {
|
||||
if u.JellyfinUserID == "" {
|
||||
continue
|
||||
}
|
||||
js.userCache[u.JellyfinUserID] = u
|
||||
}
|
||||
pageCount = res.Page.Pages
|
||||
pageIndex++
|
||||
if pageIndex >= pageCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
|
||||
params := url.Values{}
|
||||
params.Add("take", "30")
|
||||
params.Add("skip", strconv.Itoa(page*30))
|
||||
params.Add("sort", "created")
|
||||
if js.LogRequestBodies {
|
||||
fmt.Printf("Jellyseerr API Client: Sending with URL params \"%+v\"\n", params)
|
||||
}
|
||||
resp, status, err := js.get(js.server+"/user", nil, params)
|
||||
var data GetUsersDTO
|
||||
if status != 200 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
err = json.Unmarshal([]byte(resp), &data)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) MustGetUser(jfID string) (User, error) {
|
||||
u, _, err := js.GetOrImportUser(jfID)
|
||||
return u, err
|
||||
}
|
||||
|
||||
// GetImportedUser provides the same function as ImportFromJellyfin, but will always return the user,
|
||||
// even if they already existed. Also returns whether the user was imported or not,
|
||||
func (js *Jellyseerr) GetOrImportUser(jfID string) (u User, imported bool, err error) {
|
||||
imported = false
|
||||
u, err = js.GetExistingUser(jfID)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
var users []User
|
||||
users, err = js.ImportFromJellyfin(jfID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(users) != 0 {
|
||||
u = users[0]
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("user not found or imported")
|
||||
return
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) GetExistingUser(jfID string) (u User, err error) {
|
||||
js.getUsers()
|
||||
ok := false
|
||||
err = nil
|
||||
if u, ok = js.userCache[jfID]; ok {
|
||||
return
|
||||
}
|
||||
js.cacheExpiry = time.Now()
|
||||
js.getUsers()
|
||||
if u, ok = js.userCache[jfID]; ok {
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
err = fmt.Errorf("user not found")
|
||||
return
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) getUser(jfID string) (User, error) {
|
||||
if js.AutoImportUsers {
|
||||
return js.MustGetUser(jfID)
|
||||
}
|
||||
return js.GetExistingUser(jfID)
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) Me() (User, error) {
|
||||
resp, status, err := js.get(js.server+"/auth/me", nil, url.Values{})
|
||||
var data User
|
||||
data.ID = -1
|
||||
if status != 200 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
err = json.Unmarshal([]byte(resp), &data)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
|
||||
data := permissionsDTO{Permissions: -1}
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return data.Permissions, err
|
||||
}
|
||||
|
||||
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
|
||||
if err != nil {
|
||||
return data.Permissions, err
|
||||
}
|
||||
if status != 200 {
|
||||
return data.Permissions, fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
err = json.Unmarshal([]byte(resp), &data)
|
||||
return data.Permissions, err
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), permissionsDTO{Permissions: perm}, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
u.Permissions = perm
|
||||
js.userCache[jfID] = u
|
||||
return nil
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error {
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), tmpl, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
u.UserTemplate = tmpl
|
||||
js.userCache[jfID] = u
|
||||
return nil
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error {
|
||||
if _, ok := conf[FieldEmail]; ok {
|
||||
return fmt.Errorf("email is read only, set with ModifyMainUserSettings instead")
|
||||
}
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), conf, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
// Lazily just invalidate the cache.
|
||||
js.cacheExpiry = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) DeleteUser(jfID string) error {
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := js.delete(fmt.Sprintf(js.server+"/user/%d", u.ID), nil)
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delete(js.userCache, jfID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, error) {
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return Notifications{}, err
|
||||
}
|
||||
return js.GetNotificationPreferencesByID(u.ID)
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) GetNotificationPreferencesByID(jellyseerrID int64) (Notifications, error) {
|
||||
var data Notifications
|
||||
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
if status != 200 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
err = json.Unmarshal([]byte(resp), &data)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl NotificationsTemplate) error {
|
||||
// This behaviour is not desired, this being all-zero means no notifications, which is a settings state we'd want to store!
|
||||
/* if tmpl.NotifTypes.Empty() {
|
||||
tmpl.NotifTypes = nil
|
||||
}*/
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), tmpl, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsField]any) error {
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), conf, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) GetUsers() (map[string]User, error) {
|
||||
err := js.getUsers()
|
||||
return js.userCache, err
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
|
||||
resp, status, err := js.get(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
|
||||
var data User
|
||||
if status != 200 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
err = json.Unmarshal([]byte(resp), &data)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) ModifyMainUserSettings(jfID string, conf MainUserSettings) error {
|
||||
u, err := js.getUser(jfID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
// Lazily just invalidate the cache.
|
||||
js.cacheExpiry = time.Now()
|
||||
return nil
|
||||
}
|
69
jellyseerr/jellyseerr_test.go
Normal file
69
jellyseerr/jellyseerr_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
package jellyseerr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
)
|
||||
|
||||
const (
|
||||
API_KEY = "MTcyMjI2MDM2MTYyMzMxNDZkZmYyLTE4MzMtNDUyNy1hODJlLTI0MTZkZGUyMDg2Ng=="
|
||||
URI = "http://localhost:5055"
|
||||
PERM = 2097184
|
||||
)
|
||||
|
||||
func client() *Jellyseerr {
|
||||
return NewJellyseerr(URI, API_KEY, common.NewTimeoutHandler("Jellyseerr", URI, false))
|
||||
}
|
||||
|
||||
func TestMe(t *testing.T) {
|
||||
js := client()
|
||||
u, err := js.Me()
|
||||
if err != nil {
|
||||
t.Fatalf("returned error %+v", err)
|
||||
}
|
||||
if u.ID < 0 {
|
||||
t.Fatalf("returned no user %+v\n", u)
|
||||
}
|
||||
}
|
||||
|
||||
/* func TestImportFromJellyfin(t *testing.T) {
|
||||
js := client()
|
||||
list, err := js.ImportFromJellyfin("6b75e189efb744f583aa2e8e9cee41d3")
|
||||
if err != nil {
|
||||
t.Fatalf("returned error %+v", err)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
t.Fatalf("returned no users")
|
||||
}
|
||||
} */
|
||||
|
||||
func TestMustGetUser(t *testing.T) {
|
||||
js := client()
|
||||
u, err := js.MustGetUser("8c9d25c070d641cd8ad9cf825f622a16")
|
||||
if err != nil {
|
||||
t.Fatalf("returned error %+v", err)
|
||||
}
|
||||
if u.ID < 0 {
|
||||
t.Fatalf("returned no users")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPermissions(t *testing.T) {
|
||||
js := client()
|
||||
err := js.SetPermissions("6b75e189efb744f583aa2e8e9cee41d3", PERM)
|
||||
if err != nil {
|
||||
t.Fatalf("returned error %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPermissions(t *testing.T) {
|
||||
js := client()
|
||||
perm, err := js.GetPermissions("6b75e189efb744f583aa2e8e9cee41d3")
|
||||
if err != nil {
|
||||
t.Fatalf("returned error %+v", err)
|
||||
}
|
||||
if perm != PERM {
|
||||
t.Fatalf("got unexpected perm code %d", perm)
|
||||
}
|
||||
}
|
136
jellyseerr/models.go
Normal file
136
jellyseerr/models.go
Normal file
@ -0,0 +1,136 @@
|
||||
package jellyseerr
|
||||
|
||||
import "time"
|
||||
|
||||
type UserField string
|
||||
|
||||
const (
|
||||
FieldDisplayName UserField = "displayName"
|
||||
FieldEmail UserField = "email"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
UserTemplate // Note: You can set this with User.UserTemplate = value.
|
||||
UserType int64 `json:"userType,omitempty"`
|
||||
Warnings []any `json:"warnings,omitempty"`
|
||||
ID int64 `json:"id,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
PlexUsername string `json:"plexUsername,omitempty"`
|
||||
JellyfinUsername string `json:"jellyfinUsername,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
RecoveryLinkExpirationDate any `json:"recoveryLinkExpirationDate,omitempty"`
|
||||
PlexID string `json:"plexId,omitempty"`
|
||||
JellyfinUserID string `json:"jellyfinUserId,omitempty"`
|
||||
JellyfinDeviceID string `json:"jellyfinDeviceId,omitempty"`
|
||||
JellyfinAuthToken string `json:"jellyfinAuthToken,omitempty"`
|
||||
PlexToken string `json:"plexToken,omitempty"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||
RequestCount int64 `json:"requestCount,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
}
|
||||
|
||||
func (u User) Name() string {
|
||||
var n string
|
||||
if u.Username != "" {
|
||||
n = u.Username
|
||||
} else if u.JellyfinUsername != "" {
|
||||
n = u.JellyfinUsername
|
||||
}
|
||||
if u.DisplayName != "" {
|
||||
n += " (" + u.DisplayName + ")"
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
type UserTemplate struct {
|
||||
Permissions Permissions `json:"permissions,omitempty"`
|
||||
MovieQuotaLimit any `json:"movieQuotaLimit,omitempty"`
|
||||
MovieQuotaDays any `json:"movieQuotaDays,omitempty"`
|
||||
TvQuotaLimit any `json:"tvQuotaLimit,omitempty"`
|
||||
TvQuotaDays any `json:"tvQuotaDays,omitempty"`
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
Pages int `json:"pages,omitempty"`
|
||||
PageSize int `json:"pageSize,omitempty"`
|
||||
Results int `json:"results,omitempty"`
|
||||
Page int `json:"page,omitempty"`
|
||||
}
|
||||
|
||||
type GetUsersDTO struct {
|
||||
Page PageInfo `json:"pageInfo,omitempty"`
|
||||
Results []User `json:"results,omitempty"`
|
||||
}
|
||||
|
||||
type permissionsDTO struct {
|
||||
Permissions Permissions `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
type Permissions int
|
||||
|
||||
type NotificationTypes struct {
|
||||
Discord int64 `json:"discord"`
|
||||
Email int64 `json:"email"`
|
||||
Pushbullet int64 `json:"pushbullet"`
|
||||
Pushover int64 `json:"pushover"`
|
||||
Slack int64 `json:"slack"`
|
||||
Telegram int64 `json:"telegram"`
|
||||
Webhook int64 `json:"webhook"`
|
||||
Webpush int64 `json:"webpush"`
|
||||
}
|
||||
|
||||
/* func (nt *NotificationTypes) Empty() bool {
|
||||
return nt.Discord == 0 && nt.Email == 0 && nt.Pushbullet == 0 && nt.Pushover == 0 && nt.Slack == 0 && nt.Telegram == 0 && nt.Webhook == 0 && nt.Webpush == 0
|
||||
} */
|
||||
|
||||
type NotificationsField string
|
||||
|
||||
const (
|
||||
FieldDiscord NotificationsField = "discordId"
|
||||
FieldTelegram NotificationsField = "telegramChatId"
|
||||
FieldEmailEnabled NotificationsField = "emailEnabled"
|
||||
FieldDiscordEnabled NotificationsField = "discordEnabled"
|
||||
FieldTelegramEnabled NotificationsField = "telegramEnabled"
|
||||
)
|
||||
|
||||
type Notifications struct {
|
||||
NotificationsTemplate
|
||||
PgpKey any `json:"pgpKey,omitempty"`
|
||||
DiscordID string `json:"discordId,omitempty"`
|
||||
PushbulletAccessToken any `json:"pushbulletAccessToken,omitempty"`
|
||||
PushoverApplicationToken any `json:"pushoverApplicationToken,omitempty"`
|
||||
PushoverUserKey any `json:"pushoverUserKey,omitempty"`
|
||||
TelegramChatID string `json:"telegramChatId,omitempty"`
|
||||
}
|
||||
|
||||
type NotificationsTemplate struct {
|
||||
EmailEnabled bool `json:"emailEnabled,omitempty"`
|
||||
DiscordEnabled bool `json:"discordEnabled,omitempty"`
|
||||
DiscordEnabledTypes int64 `json:"discordEnabledTypes,omitempty"`
|
||||
PushoverSound any `json:"pushoverSound,omitempty"`
|
||||
TelegramEnabled bool `json:"telegramEnabled,omitempty"`
|
||||
TelegramSendSilently any `json:"telegramSendSilently,omitempty"`
|
||||
WebPushEnabled bool `json:"webPushEnabled,omitempty"`
|
||||
NotifTypes NotificationTypes `json:"notificationTypes"`
|
||||
}
|
||||
|
||||
type MainUserSettings struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
DiscordID string `json:"discordId,omitempty"`
|
||||
Locale string `json:"locale,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
OriginalLanguage any `json:"originalLanguage,omitempty"`
|
||||
MovieQuotaLimit any `json:"movieQuotaLimit,omitempty"`
|
||||
MovieQuotaDays any `json:"movieQuotaDays,omitempty"`
|
||||
TvQuotaLimit any `json:"tvQuotaLimit,omitempty"`
|
||||
TvQuotaDays any `json:"tvQuotaDays,omitempty"`
|
||||
WatchlistSyncMovies any `json:"watchlistSyncMovies,omitempty"`
|
||||
WatchlistSyncTv any `json:"watchlistSyncTv,omitempty"`
|
||||
}
|
||||
|
||||
type ErrorDTO struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
81
jellyseerrdaemon.go
Normal file
81
jellyseerrdaemon.go
Normal file
@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/jellyseerr"
|
||||
)
|
||||
|
||||
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||
user, imported, err := app.js.GetOrImportUser(jfID)
|
||||
if err != nil {
|
||||
app.debug.Printf("Failed to get or trigger import for Jellyseerr (user \"%s\"): %v", jfID, err)
|
||||
return
|
||||
}
|
||||
if imported {
|
||||
app.debug.Printf("Jellyseerr: Triggered import for Jellyfin user \"%s\" (ID %d)", jfID, user.ID)
|
||||
}
|
||||
notif, err := app.js.GetNotificationPreferencesByID(user.ID)
|
||||
if err != nil {
|
||||
app.debug.Printf("Failed to get notification prefs for Jellyseerr (user \"%s\"): %v", jfID, err)
|
||||
return
|
||||
}
|
||||
|
||||
contactMethods := map[jellyseerr.NotificationsField]any{}
|
||||
email, ok := app.storage.GetEmailsKey(jfID)
|
||||
if ok && email.Addr != "" && user.Email != email.Addr {
|
||||
err = app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: email.Addr})
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
|
||||
} else {
|
||||
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
|
||||
}
|
||||
}
|
||||
if discordEnabled {
|
||||
dcUser, ok := app.storage.GetDiscordKey(jfID)
|
||||
if ok && dcUser.ID != "" && notif.DiscordID != dcUser.ID {
|
||||
contactMethods[jellyseerr.FieldDiscord] = dcUser.ID
|
||||
contactMethods[jellyseerr.FieldDiscordEnabled] = dcUser.Contact
|
||||
}
|
||||
}
|
||||
if telegramEnabled {
|
||||
tgUser, ok := app.storage.GetTelegramKey(jfID)
|
||||
chatID, _ := strconv.ParseInt(notif.TelegramChatID, 10, 64)
|
||||
if ok && tgUser.ChatID != 0 && chatID != tgUser.ChatID {
|
||||
u, _ := app.storage.GetTelegramKey(jfID)
|
||||
contactMethods[jellyseerr.FieldTelegram] = u.ChatID
|
||||
contactMethods[jellyseerr.FieldTelegramEnabled] = tgUser.Contact
|
||||
}
|
||||
}
|
||||
if len(contactMethods) != 0 {
|
||||
err := app.js.ModifyNotifications(jfID, contactMethods)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) SynchronizeJellyseerrUsers() {
|
||||
users, status, err := app.jf.GetUsers(false)
|
||||
if err != nil || status != 200 {
|
||||
app.err.Printf("Failed to get users (%d): %s", status, err)
|
||||
return
|
||||
}
|
||||
// I'm sure Jellyseerr can handle it,
|
||||
// but past issues with the Jellyfin db scare me from
|
||||
// running these concurrently. W/e, its a bg task anyway.
|
||||
for _, user := range users {
|
||||
app.SynchronizeJellyseerrUser(user.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func newJellyseerrDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||
d := NewGenericDaemon(interval, app,
|
||||
func(app *appContext) {
|
||||
app.SynchronizeJellyseerrUsers()
|
||||
},
|
||||
)
|
||||
d.Name("Jellyseerr import daemon")
|
||||
return d
|
||||
}
|
1
lang.go
1
lang.go
@ -101,6 +101,7 @@ 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"`
|
||||
|
@ -81,6 +81,9 @@
|
||||
"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",
|
||||
@ -99,6 +102,8 @@
|
||||
"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",
|
||||
@ -137,6 +142,8 @@
|
||||
"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?",
|
||||
@ -206,6 +213,7 @@
|
||||
"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.",
|
||||
@ -222,6 +230,7 @@
|
||||
"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.",
|
||||
|
@ -45,6 +45,13 @@
|
||||
"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",
|
||||
|
83
mail/expiry-adjusted.mjml
Normal file
83
mail/expiry-adjusted.mjml
Normal file
@ -0,0 +1,83 @@
|
||||
<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>
|
11
mail/expiry-adjusted.txt
Normal file
11
mail/expiry-adjusted.txt
Normal file
@ -0,0 +1,11 @@
|
||||
{{ .helloUser }}
|
||||
|
||||
{{ .yourExpiryWasAdjusted }}
|
||||
|
||||
{{ .ifPreviouslyDisabled }}
|
||||
|
||||
{{ .newExpiry }}
|
||||
|
||||
{{ .reasonString }}: {{ .reason }}
|
||||
|
||||
{{ .message }}
|
29
main.go
29
main.go
@ -25,6 +25,7 @@ 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"
|
||||
@ -101,6 +102,7 @@ type appContext struct {
|
||||
jf *mediabrowser.MediaBrowser
|
||||
authJf *mediabrowser.MediaBrowser
|
||||
ombi *ombi.Ombi
|
||||
js *jellyseerr.Jellyseerr
|
||||
datePattern string
|
||||
timePattern string
|
||||
storage Storage
|
||||
@ -359,6 +361,19 @@ func start(asDaemon, firstCall bool) {
|
||||
|
||||
}
|
||||
|
||||
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
||||
app.debug.Printf("Connecting to Jellyseerr")
|
||||
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
|
||||
app.js = jellyseerr.NewJellyseerr(
|
||||
jellyseerrServer,
|
||||
app.config.Section("jellyseerr").Key("api_key").String(),
|
||||
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
|
||||
)
|
||||
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
|
||||
// app.js.LogRequestBodies = true
|
||||
|
||||
}
|
||||
|
||||
app.storage.db_path = filepath.Join(app.dataPath, "db")
|
||||
app.loadPendingBackup()
|
||||
app.ConnectDB()
|
||||
@ -466,13 +481,21 @@ func start(asDaemon, firstCall bool) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
invDaemon := newInviteDaemon(time.Duration(60*time.Second), app)
|
||||
invDaemon := newHousekeepingDaemon(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()
|
||||
defer userDaemon.Shutdown()
|
||||
|
||||
var jellyseerrDaemon *GenericDaemon
|
||||
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) && app.config.Section("jellyseerr").Key("import_existing").MustBool(false) {
|
||||
// jellyseerrDaemon = newJellyseerrDaemon(time.Duration(30*time.Second), app)
|
||||
jellyseerrDaemon = newJellyseerrDaemon(time.Duration(10*time.Minute), app)
|
||||
go jellyseerrDaemon.run()
|
||||
defer jellyseerrDaemon.Shutdown()
|
||||
}
|
||||
|
||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
|
||||
go app.StartPWR()
|
||||
@ -482,7 +505,7 @@ func start(asDaemon, firstCall bool) {
|
||||
go app.checkForUpdates()
|
||||
}
|
||||
|
||||
var backupDaemon *housekeepingDaemon
|
||||
var backupDaemon *GenericDaemon
|
||||
if app.config.Section("backups").Key("enabled").MustBool(false) {
|
||||
backupDaemon = newBackupDaemon(app)
|
||||
go backupDaemon.run()
|
||||
|
@ -17,6 +17,7 @@ func runMigrations(app *appContext) {
|
||||
linkExistingOmbiDiscordTelegram(app)
|
||||
// migrateHyphens(app)
|
||||
migrateToBadger(app)
|
||||
intialiseCustomContent(app)
|
||||
}
|
||||
|
||||
// Migrate pre-0.2.0 user templates to profiles
|
||||
@ -329,6 +330,8 @@ 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)
|
||||
@ -336,6 +339,57 @@ 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 {
|
||||
|
10
models.go
10
models.go
@ -16,6 +16,7 @@ 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
|
||||
@ -76,6 +77,7 @@ 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.
|
||||
}
|
||||
|
||||
@ -176,7 +178,12 @@ 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 {
|
||||
@ -241,6 +248,7 @@ type emailListDTO map[string]emailListEl
|
||||
type emailListEl struct {
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type emailSetDTO struct {
|
||||
@ -267,6 +275,8 @@ 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 {
|
||||
|
14
package-lock.json
generated
14
package-lock.json
generated
@ -22,7 +22,7 @@
|
||||
"mjml": "^4.14.1",
|
||||
"nightwind": "^1.1.13",
|
||||
"perl-regex": "^1.0.4",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss": "^8.4.31",
|
||||
"remixicon": "^3.3.0",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
@ -4274,9 +4274,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.24",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
|
||||
"integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -9991,9 +9991,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.24",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
|
||||
"integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"requires": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
|
@ -30,7 +30,7 @@
|
||||
"mjml": "^4.14.1",
|
||||
"nightwind": "^1.1.13",
|
||||
"perl-regex": "^1.0.4",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss": "^8.4.31",
|
||||
"remixicon": "^3.3.0",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
|
@ -238,6 +238,11 @@ 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)
|
||||
|
11
storage.go
11
storage.go
@ -11,6 +11,7 @@ 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"
|
||||
@ -616,6 +617,7 @@ 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"`
|
||||
@ -649,9 +651,16 @@ 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"`
|
||||
@ -1225,6 +1234,7 @@ 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)
|
||||
@ -1239,6 +1249,7 @@ 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)
|
||||
|
@ -50,6 +50,9 @@ 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"));
|
||||
@ -184,6 +187,7 @@ login.onLogin = () => {
|
||||
console.log("Logged in.");
|
||||
window.updater = new Updater();
|
||||
// FIXME: Decide whether to autoload activity or not
|
||||
window.invites.reload()
|
||||
setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000);
|
||||
const currentTab = window.tabs.current;
|
||||
switch (currentTab) {
|
||||
|
14
ts/form.ts
14
ts/form.ts
@ -38,6 +38,7 @@ interface formWindow extends Window {
|
||||
reCAPTCHASiteKey: string;
|
||||
userPageEnabled: boolean;
|
||||
userPageAddress: string;
|
||||
customSuccessCard: boolean;
|
||||
}
|
||||
|
||||
loadLangSelector("form");
|
||||
@ -223,6 +224,7 @@ if (window.emailRequired) {
|
||||
interface sendDTO {
|
||||
code: string;
|
||||
email: string;
|
||||
email_contact?: boolean;
|
||||
username: string;
|
||||
password: string;
|
||||
telegram_pin?: string;
|
||||
@ -251,8 +253,9 @@ 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;
|
||||
@ -274,6 +277,10 @@ 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();
|
||||
@ -296,7 +303,10 @@ 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.userPageEnabled) {
|
||||
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) {
|
||||
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);
|
||||
|
@ -795,6 +795,11 @@ 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[] = [];
|
||||
@ -1459,6 +1464,7 @@ export class accountsList {
|
||||
const modalHeader = document.getElementById("header-modify-user");
|
||||
modalHeader.textContent = window.lang.quantity("modifySettingsFor", this._collectUsers().length)
|
||||
let list = this._collectUsers();
|
||||
|
||||
(() => {
|
||||
let innerHTML = "";
|
||||
for (const profile of window.availableProfiles) {
|
||||
@ -1477,6 +1483,7 @@ export class accountsList {
|
||||
|
||||
const form = document.getElementById("form-modify-user") as HTMLFormElement;
|
||||
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
||||
|
||||
this._modifySettingsProfile.checked = true;
|
||||
this._modifySettingsUser.checked = false;
|
||||
form.onsubmit = (event: Event) => {
|
||||
@ -1484,7 +1491,10 @@ export class accountsList {
|
||||
toggleLoader(button);
|
||||
let send = {
|
||||
"apply_to": list,
|
||||
"homescreen": (document.getElementById("modify-user-homescreen") as HTMLInputElement).checked
|
||||
"homescreen": this._applyHomescreen.checked,
|
||||
"configuration": this._applyConfiguration.checked,
|
||||
"ombi": this._applyOmbi.checked,
|
||||
"jellyseerr": this._applyJellyseerr.checked
|
||||
};
|
||||
if (this._modifySettingsProfile.checked && !this._modifySettingsUser.checked) {
|
||||
send["from"] = "profile";
|
||||
@ -1684,22 +1694,25 @@ export class accountsList {
|
||||
applyList.push(id);
|
||||
}
|
||||
this._enableExpiryReason.classList.add("unfocused");
|
||||
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 = "";
|
||||
let header: string;
|
||||
if (enableUser) {
|
||||
header = window.lang.quantity("reEnableUsers", list.length);
|
||||
} 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 }
|
||||
let send = { "users": applyList, "timestamp": 0, "notify": this._enableExpiryNotify.checked }
|
||||
if (this._enableExpiryNotify.checked) {
|
||||
send["reason"] = this._enableExpiryReason.value;
|
||||
}
|
||||
if (this._usingExtendExpiryTextInput) {
|
||||
let date = (Date as any).fromString(this._extendExpiryTextInput.value) as Date;
|
||||
send["timestamp"] = Math.floor(date.getTime() / 1000);
|
||||
@ -1728,7 +1741,7 @@ export class accountsList {
|
||||
this._extendExpiryForm.onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (enableUser) {
|
||||
this._enableDisableUsers(applyList, true, this._enableExpiryNotify.checked, this._enableExpiryNotify ? this._enableExpiryReason.value : null, (req: XMLHttpRequest) => {
|
||||
this._enableDisableUsers(applyList, true, this._enableExpiryNotify.checked, this._enableExpiryNotify.checked ? this._enableExpiryReason.value : null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200 && req.status != 204) {
|
||||
window.modals.extendExpiry.close();
|
||||
@ -1818,6 +1831,16 @@ export class accountsList {
|
||||
};
|
||||
this._modifySettings.onclick = this.modifyUsers;
|
||||
this._modifySettings.classList.add("unfocused");
|
||||
|
||||
if (window.ombiEnabled)
|
||||
this._applyOmbi.parentElement.classList.remove("unfocused");
|
||||
else
|
||||
this._applyOmbi.parentElement.classList.add("unfocused");
|
||||
if (window.jellyseerrEnabled)
|
||||
this._applyJellyseerr.parentElement.classList.remove("unfocused");
|
||||
else
|
||||
this._applyJellyseerr.parentElement.classList.add("unfocused");
|
||||
|
||||
const checkSource = () => {
|
||||
const profileSpan = this._modifySettingsProfile.nextElementSibling as HTMLSpanElement;
|
||||
const userSpan = this._modifySettingsUser.nextElementSibling as HTMLSpanElement;
|
||||
@ -1828,6 +1851,8 @@ export class accountsList {
|
||||
profileSpan.classList.remove("@low");
|
||||
userSpan.classList.remove("@high");
|
||||
userSpan.classList.add("@low");
|
||||
this._applyOmbi.parentElement.classList.remove("unfocused");
|
||||
this._applyJellyseerr.parentElement.classList.remove("unfocused");
|
||||
} else {
|
||||
this._userSelect.parentElement.classList.remove("unfocused");
|
||||
this._profileSelect.parentElement.classList.add("unfocused");
|
||||
@ -1835,6 +1860,8 @@ export class accountsList {
|
||||
userSpan.classList.remove("@low");
|
||||
profileSpan.classList.remove("@high");
|
||||
profileSpan.classList.add("@low");
|
||||
this._applyOmbi.parentElement.classList.add("unfocused");
|
||||
this._applyJellyseerr.parentElement.classList.add("unfocused");
|
||||
}
|
||||
};
|
||||
this._modifySettingsProfile.onchange = checkSource;
|
||||
|
@ -5,6 +5,7 @@ interface Profile {
|
||||
libraries: string;
|
||||
fromUser: string;
|
||||
ombi: boolean;
|
||||
jellyseerr: boolean;
|
||||
referrals_enabled: boolean;
|
||||
}
|
||||
|
||||
@ -14,9 +15,11 @@ 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;
|
||||
|
||||
@ -52,6 +55,21 @@ class profile implements Profile {
|
||||
}
|
||||
}
|
||||
|
||||
get jellyseerr(): boolean { return this._jellyseerr; }
|
||||
set jellyseerr(v: boolean) {
|
||||
if (!window.jellyseerrEnabled) return;
|
||||
this._jellyseerr = v;
|
||||
if (v) {
|
||||
this._jellyseerrButton.textContent = window.lang.strings("delete");
|
||||
this._jellyseerrButton.classList.add("~critical");
|
||||
this._jellyseerrButton.classList.remove("~neutral");
|
||||
} else {
|
||||
this._jellyseerrButton.textContent = window.lang.strings("add");
|
||||
this._jellyseerrButton.classList.add("~neutral");
|
||||
this._jellyseerrButton.classList.remove("~critical");
|
||||
}
|
||||
}
|
||||
|
||||
get fromUser(): string { return this._fromUser.textContent; }
|
||||
set fromUser(v: string) { this._fromUser.textContent = v; }
|
||||
|
||||
@ -82,6 +100,9 @@ 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>
|
||||
`;
|
||||
@ -96,6 +117,8 @@ 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;
|
||||
@ -112,10 +135,12 @@ 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(); }
|
||||
@ -144,6 +169,7 @@ 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;
|
||||
@ -181,7 +207,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) => {
|
||||
@ -198,7 +224,26 @@ export class ProfileEditor {
|
||||
this._ombiProfiles.load(name);
|
||||
}
|
||||
});
|
||||
if (window.referralsEnabled)
|
||||
}
|
||||
if (window.jellyseerrEnabled) {
|
||||
this._profiles[name].setJellyseerrFunc((jellyseerr: boolean) => {
|
||||
if (jellyseerr) {
|
||||
this._jellyseerrProfiles.delete(name, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 204) {
|
||||
window.notifications.customError("errorDeleteJellyseerr", window.lang.notif("errorUnknown"));
|
||||
return;
|
||||
}
|
||||
this._profiles[name].jellyseerr = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
window.modals.profiles.close();
|
||||
this._jellyseerrProfiles.load(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (window.referralsEnabled) {
|
||||
this._profiles[name].setReferralFunc((enabled: boolean) => {
|
||||
if (enabled) {
|
||||
this.disableReferrals(name);
|
||||
@ -206,6 +251,7 @@ export class ProfileEditor {
|
||||
this.enableReferrals(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
this._table.appendChild(this._profiles[name].asElement());
|
||||
}
|
||||
}
|
||||
@ -299,6 +345,8 @@ 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) {
|
||||
@ -366,7 +414,7 @@ export class ombiProfiles {
|
||||
let resp = {} as ombiUser;
|
||||
resp.id = this._select.value;
|
||||
resp.name = this._users[resp.id];
|
||||
_post("/profiles/ombi/" + this._currentProfile, resp, (req: XMLHttpRequest) => {
|
||||
_post("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(this._currentProfile)), resp, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
@ -379,7 +427,7 @@ export class ombiProfiles {
|
||||
});
|
||||
}
|
||||
|
||||
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/ombi/" + profile, null, post);
|
||||
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/ombi/" + encodeURIComponent(encodeURIComponent(profile)), null, post);
|
||||
|
||||
load = (profile: string) => {
|
||||
this._currentProfile = profile;
|
||||
@ -401,3 +449,54 @@ export class ombiProfiles {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class jellyseerrProfiles {
|
||||
private _form: HTMLFormElement;
|
||||
private _select: HTMLSelectElement;
|
||||
private _users: { [id: string]: string } = {};
|
||||
private _currentProfile: string;
|
||||
|
||||
constructor() {
|
||||
this._form = document.getElementById("form-jellyseerr-defaults") as HTMLFormElement;
|
||||
this._form.onsubmit = this.send;
|
||||
this._select = this._form.querySelector("select") as HTMLSelectElement;
|
||||
}
|
||||
send = () => {
|
||||
const button = this._form.querySelector("span.submit") as HTMLSpanElement;
|
||||
toggleLoader(button);
|
||||
let encodedProfile = encodeURIComponent(encodeURIComponent(this._currentProfile));
|
||||
_post("/profiles/jellyseerr/" + encodedProfile + "/" + this._select.value, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
if (req.status == 200 || req.status == 204) {
|
||||
window.notifications.customSuccess("jellyseerrDefaults", window.lang.notif("savedProfile"));
|
||||
} else {
|
||||
window.notifications.customError("jellyseerrDefaults", window.lang.notif("errorSavedProfile"));
|
||||
}
|
||||
window.modals.jellyseerrProfile.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
delete = (profile: string, post?: (req: XMLHttpRequest) => void) => _delete("/profiles/jellyseerr/" + encodeURIComponent(encodeURIComponent(profile)), null, post);
|
||||
|
||||
load = (profile: string) => {
|
||||
this._currentProfile = profile;
|
||||
_get("/jellyseerr/users", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 && "users" in req.response) {
|
||||
const users = req.response["users"] as ombiUser[];
|
||||
let innerHTML = "";
|
||||
for (let user of users) {
|
||||
this._users[user.id] = user.name;
|
||||
innerHTML += `<option value="${user.id}">${user.name}</option>`;
|
||||
}
|
||||
this._select.innerHTML = innerHTML;
|
||||
window.modals.jellyseerrProfile.show();
|
||||
} else {
|
||||
window.notifications.customError("jellyseerrLoadError", window.lang.notif("errorLoadUsers"))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1083,6 +1083,7 @@ export interface templateEmail {
|
||||
interface emailListEl {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
class MessageEditor {
|
||||
@ -1092,6 +1093,7 @@ 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;
|
||||
@ -1113,6 +1115,12 @@ 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 == "") {
|
||||
@ -1212,11 +1220,22 @@ class MessageEditor {
|
||||
if (this._names[id].enabled) {
|
||||
resetButton = `<i class="icon ri-restart-line" title="${window.lang.get("strings", "reset")}"></i>`;
|
||||
}
|
||||
tr.innerHTML = `
|
||||
<td>${this._names[id].name}</td>
|
||||
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>
|
||||
<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);
|
||||
|
@ -24,6 +24,7 @@ declare interface Window {
|
||||
discordEnabled: boolean;
|
||||
matrixEnabled: boolean;
|
||||
ombiEnabled: boolean;
|
||||
jellyseerrEnabled: boolean;
|
||||
usernameEnabled: boolean;
|
||||
linkResetEnabled: boolean;
|
||||
token: string;
|
||||
@ -101,6 +102,7 @@ declare interface Modals {
|
||||
settingsRestart: Modal;
|
||||
settingsRefresh: Modal;
|
||||
ombiProfile?: Modal;
|
||||
jellyseerrProfile?: Modal;
|
||||
profiles: Modal;
|
||||
addProfile: Modal;
|
||||
announce: Modal;
|
||||
|
@ -7,47 +7,14 @@ import (
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
)
|
||||
|
||||
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 newUserDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||
d := NewGenericDaemon(interval, app,
|
||||
func(app *appContext) {
|
||||
app.checkUsers()
|
||||
},
|
||||
)
|
||||
d.Name("User daemon")
|
||||
return d
|
||||
}
|
||||
|
||||
func (app *appContext) checkUsers() {
|
||||
|
23
views.go
23
views.go
@ -133,6 +133,7 @@ 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
|
||||
@ -164,6 +165,7 @@ 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,
|
||||
@ -192,6 +194,7 @@ 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,
|
||||
@ -203,6 +206,7 @@ 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,
|
||||
@ -278,6 +282,8 @@ 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 {
|
||||
@ -734,6 +740,7 @@ 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,
|
||||
@ -766,6 +773,22 @@ 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 := ""
|
||||
|
Loading…
Reference in New Issue
Block a user