mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-29 12:30:11 +00:00
Compare commits
No commits in common. "62e27c394d8f2b301ab5b69a2e13cd6760b6fbf8" and "2d066ea7cd3251b49e2772126e34ef607b18cbe2" have entirely different histories.
62e27c394d
...
2d066ea7cd
172
.drone.yml.old
Normal file
172
.drone.yml.old
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
name: jfa-go
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: fetch
|
||||||
|
image: docker:git
|
||||||
|
commands:
|
||||||
|
- git fetch --tags
|
||||||
|
- name: release
|
||||||
|
image: hrfee/jfa-go-build-docker:latest
|
||||||
|
volumes:
|
||||||
|
- name: ssh_key
|
||||||
|
path: /id_rsa
|
||||||
|
environment:
|
||||||
|
BUILDRONE_KEY:
|
||||||
|
from_secret: BUILDRONE_KEY
|
||||||
|
GITHUB_TOKEN:
|
||||||
|
from_secret: github_token
|
||||||
|
JFA_GO_BUILT_BY:
|
||||||
|
from_secret: BUILT_BY
|
||||||
|
commands:
|
||||||
|
- curl -sL https://git.io/goreleaser > ../goreleaser
|
||||||
|
- chmod +x ../goreleaser
|
||||||
|
- ./scripts/version.sh ../goreleaser
|
||||||
|
- wget https://builds.hrfee.pw/upload.py -P ../
|
||||||
|
- pip3 install requests
|
||||||
|
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
|
||||||
|
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
|
||||||
|
bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "rm /repo/incoming/*.deb"'
|
||||||
|
- bash -c 'python3 ../upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'
|
||||||
|
volumes:
|
||||||
|
- name: ssh_key
|
||||||
|
host:
|
||||||
|
path: /root/.ssh/id_rsa_packaging
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
---
|
||||||
|
name: docker-buildx
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build-deploy
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
environment:
|
||||||
|
BUILDRONE_KEY:
|
||||||
|
from_secret: BUILDRONE_KEY
|
||||||
|
settings:
|
||||||
|
host:
|
||||||
|
from_secret: ssh2_host
|
||||||
|
username:
|
||||||
|
from_secret: ssh2_username
|
||||||
|
port:
|
||||||
|
from_secret: ssh2_port
|
||||||
|
envs:
|
||||||
|
- buildrone_key
|
||||||
|
key:
|
||||||
|
from_secret: ssh2_key
|
||||||
|
command_timeout: 50m
|
||||||
|
script:
|
||||||
|
- /mnt/buildx/jfa-go/build.sh stable
|
||||||
|
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||||
|
- pip3 install requests
|
||||||
|
- bash -c 'cd /mnt/buildx/jfa-go/jfa-go && python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-stable=true'
|
||||||
|
- rm -f /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
---
|
||||||
|
name: jfa-go-git
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: hrfee/jfa-go-build-docker:latest
|
||||||
|
volumes:
|
||||||
|
- name: ssh_key
|
||||||
|
path: /id_rsa
|
||||||
|
- name: ssh_key2
|
||||||
|
path: /id_rsa2
|
||||||
|
commands:
|
||||||
|
- curl -sL https://git.io/goreleaser > goreleaser
|
||||||
|
- chmod +x goreleaser
|
||||||
|
- ./scripts/version.sh ./goreleaser --snapshot --skip=publish --clean
|
||||||
|
- wget https://builds.hrfee.pw/upload.py
|
||||||
|
- pip3 install requests
|
||||||
|
- bash -c 'sftp -i /id_rsa2 -o StrictHostKeyChecking=no root@161.97.102.153:/mnt/redoc <<< $"put docs/swagger.json jfa-go.json"'
|
||||||
|
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
|
||||||
|
# - bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "reprepro -Vb /repo remove trusty-unstable jfa-go"'
|
||||||
|
# - bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "reprepro -Vb /repo remove trusty-unstable jfa-go-tray"'
|
||||||
|
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
|
||||||
|
bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "rm /repo/incoming/*.deb"'
|
||||||
|
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.zip ./dist/*.rpm ./dist/*.apk --tag internal-git=true'
|
||||||
|
environment:
|
||||||
|
BUILDRONE_KEY:
|
||||||
|
from_secret: BUILDRONE_KEY
|
||||||
|
JFA_GO_BUILT_BY:
|
||||||
|
from_secret: BUILT_BY
|
||||||
|
JFA_GO_SNAPSHOT: y
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: ssh_key
|
||||||
|
host:
|
||||||
|
path: /root/.ssh/id_rsa_packaging
|
||||||
|
- name: ssh_key2
|
||||||
|
host:
|
||||||
|
path: /root/.ssh/docker-build
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
- go1.16
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- pull_request
|
||||||
|
|
||||||
|
---
|
||||||
|
name: docker-buildx-unstable
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build-deploy
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
environment:
|
||||||
|
BUILDRONE_KEY:
|
||||||
|
from_secret: BUILDRONE_KEY
|
||||||
|
settings:
|
||||||
|
host:
|
||||||
|
from_secret: ssh2_host
|
||||||
|
username:
|
||||||
|
from_secret: ssh2_username
|
||||||
|
port:
|
||||||
|
from_secret: ssh2_port
|
||||||
|
envs:
|
||||||
|
- buildrone_key
|
||||||
|
key:
|
||||||
|
from_secret: ssh2_key
|
||||||
|
command_timeout: 50m
|
||||||
|
script:
|
||||||
|
- /mnt/buildx/jfa-go/build.sh
|
||||||
|
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||||
|
- pip3 install requests
|
||||||
|
- bash -c 'cd /mnt/buildx/jfa-go/jfa-go && python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-unstable=true'
|
||||||
|
- rm -f /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- pull_request
|
||||||
|
|
||||||
|
---
|
||||||
|
name: jfa-go-pr
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: hrfee/jfa-go-build-docker:latest
|
||||||
|
commands:
|
||||||
|
- curl -sL https://git.io/goreleaser > goreleaser
|
||||||
|
- chmod +x goreleaser
|
||||||
|
- ./scripts/version.sh ./goreleaser --snapshot --skip=publish --clean
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
include:
|
||||||
|
- pull_request
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,4 +25,3 @@ scripts/langmover/lang
|
|||||||
scripts/langmover/lang2
|
scripts/langmover/lang2
|
||||||
scripts/langmover/out
|
scripts/langmover/out
|
||||||
tinyproxy.conf
|
tinyproxy.conf
|
||||||
static/banner.svg
|
|
||||||
|
@ -11,7 +11,6 @@ before:
|
|||||||
- go mod download
|
- go mod download
|
||||||
- rm -rf data/web
|
- rm -rf data/web
|
||||||
- mkdir -p data/web/css
|
- mkdir -p data/web/css
|
||||||
- cp images/banner.svg static/banner.svg
|
|
||||||
- bash -c 'cp -r static/* data/web/'
|
- bash -c 'cp -r static/* data/web/'
|
||||||
- npm install
|
- npm install
|
||||||
- npm install esbuild
|
- npm install esbuild
|
||||||
|
1
Makefile
1
Makefile
@ -167,7 +167,6 @@ copy:
|
|||||||
mv $(DATA)/crash.html $(DATA)/html/
|
mv $(DATA)/crash.html $(DATA)/html/
|
||||||
$(info copying static data)
|
$(info copying static data)
|
||||||
mkdir -p $(DATA)/web
|
mkdir -p $(DATA)/web
|
||||||
cp images/banner.svg static/banner.svg
|
|
||||||
cp -r static/* $(DATA)/web/
|
cp -r static/* $(DATA)/web/
|
||||||
$(info copying systemd service)
|
$(info copying systemd service)
|
||||||
cp jfa-go.service $(DATA)/
|
cp jfa-go.service $(DATA)/
|
||||||
|
@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -121,7 +120,7 @@ func (app *appContext) GetActivities(gc *gin.Context) {
|
|||||||
err := app.storage.db.Find(&results, query)
|
err := app.storage.db.Find(&results, query)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedDBReadActivities, err)
|
app.err.Printf("Failed to read activities from DB: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := GetActivitiesRespDTO{
|
resp := GetActivitiesRespDTO{
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Summary Creates a backup of the database.
|
// @Summary Creates a backup of the database.
|
||||||
@ -36,7 +35,7 @@ func (app *appContext) GetBackup(gc *gin.Context) {
|
|||||||
ok := (strings.HasPrefix(fname, BACKUP_PREFIX) || strings.HasPrefix(fname, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX)) && strings.HasSuffix(fname, BACKUP_SUFFIX)
|
ok := (strings.HasPrefix(fname, BACKUP_PREFIX) || strings.HasPrefix(fname, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX)) && strings.HasSuffix(fname, BACKUP_SUFFIX)
|
||||||
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(fname, BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX))
|
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(fname, BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX))
|
||||||
if !ok || err != nil || t.IsZero() {
|
if !ok || err != nil || t.IsZero() {
|
||||||
app.debug.Printf(lm.IgnoreInvalidFilename, fname, err)
|
app.debug.Printf("Ignoring backup DL request due to fname: %v\n", err)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -84,7 +83,7 @@ func (app *appContext) RestoreLocalBackup(gc *gin.Context) {
|
|||||||
ok := strings.HasPrefix(fname, BACKUP_PREFIX) && strings.HasSuffix(fname, BACKUP_SUFFIX)
|
ok := strings.HasPrefix(fname, BACKUP_PREFIX) && strings.HasSuffix(fname, BACKUP_SUFFIX)
|
||||||
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(fname, BACKUP_PREFIX), BACKUP_SUFFIX))
|
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(fname, BACKUP_PREFIX), BACKUP_SUFFIX))
|
||||||
if !ok || err != nil || t.IsZero() {
|
if !ok || err != nil || t.IsZero() {
|
||||||
app.debug.Printf(lm.IgnoreInvalidFilename, fname, err)
|
app.debug.Printf("Ignoring backup DL request due to fname: %v\n", err)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -104,15 +103,15 @@ func (app *appContext) RestoreLocalBackup(gc *gin.Context) {
|
|||||||
func (app *appContext) RestoreBackup(gc *gin.Context) {
|
func (app *appContext) RestoreBackup(gc *gin.Context) {
|
||||||
file, err := gc.FormFile("backups-file")
|
file, err := gc.FormFile("backups-file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGetUpload, err)
|
app.err.Printf("Failed to get file from form data: %v\n", err)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.debug.Printf(lm.GetUpload, file.Filename)
|
app.debug.Printf("Got uploaded file \"%s\"\n", file.Filename)
|
||||||
path := app.config.Section("backups").Key("path").String()
|
path := app.config.Section("backups").Key("path").String()
|
||||||
fullpath := filepath.Join(path, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX+time.Now().Local().Format(BACKUP_DATEFMT)+BACKUP_SUFFIX)
|
fullpath := filepath.Join(path, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX+time.Now().Local().Format(BACKUP_DATEFMT)+BACKUP_SUFFIX)
|
||||||
gc.SaveUploadedFile(file, fullpath)
|
gc.SaveUploadedFile(file, fullpath)
|
||||||
app.debug.Printf(lm.Write, fullpath)
|
app.debug.Printf("Saved to \"%s\"\n", fullpath)
|
||||||
LOADBAK = fullpath
|
LOADBAK = fullpath
|
||||||
app.restart(gc)
|
app.restart(gc)
|
||||||
}
|
}
|
||||||
|
221
api-invites.go
221
api-invites.go
@ -8,7 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/itchyny/timefmt-go"
|
"github.com/itchyny/timefmt-go"
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
@ -30,7 +29,6 @@ func GenerateInviteCode() string {
|
|||||||
return inviteCode
|
return inviteCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkInvites performs general housekeeping on invites, i.e. deleting expired ones and cleaning captcha data.
|
|
||||||
func (app *appContext) checkInvites() {
|
func (app *appContext) checkInvites() {
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
for _, data := range app.storage.GetInvites() {
|
for _, data := range app.storage.GetInvites() {
|
||||||
@ -54,11 +52,60 @@ func (app *appContext) checkInvites() {
|
|||||||
if !currentTime.After(expiry) {
|
if !currentTime.After(expiry) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
app.deleteExpiredInvite(data)
|
|
||||||
|
app.debug.Printf("Housekeeping: Deleting old invite %s", data.Code)
|
||||||
|
|
||||||
|
// Disable referrals for the user if UseReferralExpiry is enabled, so no new ones are made.
|
||||||
|
if data.IsReferral && data.UseReferralExpiry && data.ReferrerJellyfinID != "" {
|
||||||
|
user, ok := app.storage.GetEmailsKey(data.ReferrerJellyfinID)
|
||||||
|
if ok {
|
||||||
|
user.ReferralTemplateKey = ""
|
||||||
|
app.storage.SetEmailsKey(data.ReferrerJellyfinID, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notify := data.Notify
|
||||||
|
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||||
|
app.debug.Printf("%s: Expiry notification", data.Code)
|
||||||
|
var wait sync.WaitGroup
|
||||||
|
for address, settings := range notify {
|
||||||
|
if !settings["notify-expiry"] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wait.Add(1)
|
||||||
|
go func(addr string) {
|
||||||
|
defer wait.Done()
|
||||||
|
msg, err := app.email.constructExpiry(data.Code, data, app, false)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("%s: Failed to construct expiry notification: %v", data.Code, err)
|
||||||
|
} else {
|
||||||
|
// Check whether notify "address" is an email address of Jellyfin ID
|
||||||
|
if strings.Contains(addr, "@") {
|
||||||
|
err = app.email.send(msg, addr)
|
||||||
|
} else {
|
||||||
|
err = app.sendByID(msg, addr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("%s: Failed to send expiry notification: %v", data.Code, err)
|
||||||
|
} else {
|
||||||
|
app.info.Printf("Sent expiry notification to %s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(address)
|
||||||
|
}
|
||||||
|
wait.Wait()
|
||||||
|
}
|
||||||
|
app.storage.DeleteInvitesKey(data.Code)
|
||||||
|
|
||||||
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
|
Type: ActivityDeleteInvite,
|
||||||
|
SourceType: ActivityDaemon,
|
||||||
|
InviteCode: data.Code,
|
||||||
|
Value: data.Label,
|
||||||
|
Time: time.Now(),
|
||||||
|
}, nil, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkInvite checks the validity of a specific invite, optionally removing it if invalid(ated).
|
|
||||||
func (app *appContext) checkInvite(code string, used bool, username string) bool {
|
func (app *appContext) checkInvite(code string, used bool, username string) bool {
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
inv, match := app.storage.GetInvitesKey(code)
|
inv, match := app.storage.GetInvitesKey(code)
|
||||||
@ -67,8 +114,54 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
|||||||
}
|
}
|
||||||
expiry := inv.ValidTill
|
expiry := inv.ValidTill
|
||||||
if currentTime.After(expiry) {
|
if currentTime.After(expiry) {
|
||||||
app.deleteExpiredInvite(inv)
|
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
||||||
|
notify := inv.Notify
|
||||||
|
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||||
|
app.debug.Printf("%s: Expiry notification", code)
|
||||||
|
var wait sync.WaitGroup
|
||||||
|
for address, settings := range notify {
|
||||||
|
if !settings["notify-expiry"] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wait.Add(1)
|
||||||
|
go func(addr string) {
|
||||||
|
defer wait.Done()
|
||||||
|
msg, err := app.email.constructExpiry(code, inv, app, false)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
|
||||||
|
} else {
|
||||||
|
// Check whether notify "address" is an email address of Jellyfin ID
|
||||||
|
if strings.Contains(addr, "@") {
|
||||||
|
err = app.email.send(msg, addr)
|
||||||
|
} else {
|
||||||
|
err = app.sendByID(msg, addr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
|
||||||
|
} else {
|
||||||
|
app.info.Printf("Sent expiry notification to %s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(address)
|
||||||
|
}
|
||||||
|
wait.Wait()
|
||||||
|
}
|
||||||
|
if inv.IsReferral && inv.ReferrerJellyfinID != "" && inv.UseReferralExpiry {
|
||||||
|
user, ok := app.storage.GetEmailsKey(inv.ReferrerJellyfinID)
|
||||||
|
if ok {
|
||||||
|
user.ReferralTemplateKey = ""
|
||||||
|
app.storage.SetEmailsKey(inv.ReferrerJellyfinID, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
match = false
|
match = false
|
||||||
|
app.storage.DeleteInvitesKey(code)
|
||||||
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
|
Type: ActivityDeleteInvite,
|
||||||
|
SourceType: ActivityDaemon,
|
||||||
|
InviteCode: code,
|
||||||
|
Value: inv.Label,
|
||||||
|
Time: time.Now(),
|
||||||
|
}, nil, false)
|
||||||
} else if used {
|
} else if used {
|
||||||
del := false
|
del := false
|
||||||
newInv := inv
|
newInv := inv
|
||||||
@ -94,67 +187,6 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
|||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) deleteExpiredInvite(data Invite) {
|
|
||||||
app.debug.Printf(lm.DeleteOldInvite, data.Code)
|
|
||||||
|
|
||||||
// Disable referrals for the user if UseReferralExpiry is enabled, so no new ones are made.
|
|
||||||
if data.IsReferral && data.UseReferralExpiry && data.ReferrerJellyfinID != "" {
|
|
||||||
user, ok := app.storage.GetEmailsKey(data.ReferrerJellyfinID)
|
|
||||||
if ok {
|
|
||||||
user.ReferralTemplateKey = ""
|
|
||||||
app.storage.SetEmailsKey(data.ReferrerJellyfinID, user)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wait := app.sendAdminExpiryNotification(data)
|
|
||||||
app.storage.DeleteInvitesKey(data.Code)
|
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
|
||||||
Type: ActivityDeleteInvite,
|
|
||||||
SourceType: ActivityDaemon,
|
|
||||||
InviteCode: data.Code,
|
|
||||||
Value: data.Label,
|
|
||||||
Time: time.Now(),
|
|
||||||
}, nil, false)
|
|
||||||
|
|
||||||
if wait != nil {
|
|
||||||
wait.Wait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) sendAdminExpiryNotification(data Invite) *sync.WaitGroup {
|
|
||||||
notify := data.Notify
|
|
||||||
if !emailEnabled || !app.config.Section("notifications").Key("enabled").MustBool(false) || len(notify) != 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var wait sync.WaitGroup
|
|
||||||
for address, settings := range notify {
|
|
||||||
if !settings["notify-expiry"] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
wait.Add(1)
|
|
||||||
go func(addr string) {
|
|
||||||
defer wait.Done()
|
|
||||||
msg, err := app.email.constructExpiry(data.Code, data, app, false)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedConstructExpiryAdmin, data.Code, err)
|
|
||||||
} else {
|
|
||||||
// Check whether notify "address" is an email address or Jellyfin ID
|
|
||||||
if strings.Contains(addr, "@") {
|
|
||||||
err = app.email.send(msg, addr)
|
|
||||||
} else {
|
|
||||||
err = app.sendByID(msg, addr)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedSendExpiryAdmin, data.Code, addr, err)
|
|
||||||
} else {
|
|
||||||
app.info.Printf(lm.SentExpiryAdmin, data.Code, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(address)
|
|
||||||
}
|
|
||||||
return &wait
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Create a new invite.
|
// @Summary Create a new invite.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param generateInviteDTO body generateInviteDTO true "New invite request object"
|
// @Param generateInviteDTO body generateInviteDTO true "New invite request object"
|
||||||
@ -164,7 +196,7 @@ func (app *appContext) sendAdminExpiryNotification(data Invite) *sync.WaitGroup
|
|||||||
// @tags Invites
|
// @tags Invites
|
||||||
func (app *appContext) GenerateInvite(gc *gin.Context) {
|
func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||||
var req generateInviteDTO
|
var req generateInviteDTO
|
||||||
app.debug.Println(lm.GenerateInvite)
|
app.debug.Println("Generating new invite")
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
validTill := currentTime.AddDate(0, req.Months, req.Days)
|
validTill := currentTime.AddDate(0, req.Months, req.Days)
|
||||||
@ -198,12 +230,13 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||||
addressValid := false
|
addressValid := false
|
||||||
discord := ""
|
discord := ""
|
||||||
|
app.debug.Printf("%s: Sending invite message", invite.Code)
|
||||||
if discordEnabled && (!strings.Contains(req.SendTo, "@") || strings.HasPrefix(req.SendTo, "@")) {
|
if discordEnabled && (!strings.Contains(req.SendTo, "@") || strings.HasPrefix(req.SendTo, "@")) {
|
||||||
users := app.discord.GetUsers(req.SendTo)
|
users := app.discord.GetUsers(req.SendTo)
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipNoUser, req.SendTo)
|
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
|
||||||
} else if len(users) > 1 {
|
} else if len(users) > 1 {
|
||||||
invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipMultiUser, req.SendTo)
|
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
|
||||||
} else {
|
} else {
|
||||||
invite.SendTo = req.SendTo
|
invite.SendTo = req.SendTo
|
||||||
addressValid = true
|
addressValid = true
|
||||||
@ -216,10 +249,8 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
if addressValid {
|
if addressValid {
|
||||||
msg, err := app.email.constructInvite(invite.Code, invite, app, false)
|
msg, err := app.email.constructInvite(invite.Code, invite, app, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Slight misuse of the template
|
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||||
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, req.SendTo, err)
|
app.err.Printf("%s: Failed to construct invite message: %v", invite.Code, err)
|
||||||
|
|
||||||
app.err.Printf(lm.FailedConstructInviteMessage, invite.Code, err)
|
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
if discord != "" {
|
if discord != "" {
|
||||||
@ -228,10 +259,10 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
err = app.email.send(msg, req.SendTo)
|
err = app.email.send(msg, req.SendTo)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, req.SendTo, err)
|
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||||
app.err.Println(invite.SendTo)
|
app.err.Printf("%s: %s: %v", invite.Code, invite.SendTo, err)
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf(lm.SentInviteMessage, invite.Code, req.SendTo)
|
app.info.Printf("%s: Sent invite email to \"%s\"", invite.Code, req.SendTo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,6 +297,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Invites
|
// @tags Invites
|
||||||
func (app *appContext) GetInvites(gc *gin.Context) {
|
func (app *appContext) GetInvites(gc *gin.Context) {
|
||||||
|
app.debug.Println("Invites requested")
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
app.checkInvites()
|
app.checkInvites()
|
||||||
var invites []inviteDTO
|
var invites []inviteDTO
|
||||||
@ -300,7 +332,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
date, err := timefmt.Parse(pair[1], app.datePattern+" "+app.timePattern)
|
date, err := timefmt.Parse(pair[1], app.datePattern+" "+app.timePattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedParseTime, err)
|
app.err.Printf("Failed to parse usedBy time: %v", err)
|
||||||
}
|
}
|
||||||
unix = date.Unix()
|
unix = date.Unix()
|
||||||
}
|
}
|
||||||
@ -315,6 +347,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
|||||||
invite.SendTo = inv.SendTo
|
invite.SendTo = inv.SendTo
|
||||||
}
|
}
|
||||||
if len(inv.Notify) != 0 {
|
if len(inv.Notify) != 0 {
|
||||||
|
// app.err.Printf("%s has notify section: %+v, you are %s\n", inv.Code, inv.Notify, gc.GetString("jfId"))
|
||||||
var addressOrID string
|
var addressOrID string
|
||||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||||
addressOrID = gc.GetString("jfId")
|
addressOrID = gc.GetString("jfId")
|
||||||
@ -364,9 +397,10 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
|||||||
func (app *appContext) SetProfile(gc *gin.Context) {
|
func (app *appContext) SetProfile(gc *gin.Context) {
|
||||||
var req inviteProfileDTO
|
var req inviteProfileDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
|
app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile)
|
||||||
// "" means "Don't apply profile"
|
// "" means "Don't apply profile"
|
||||||
if _, ok := app.storage.GetProfileKey(req.Profile); !ok && req.Profile != "" {
|
if _, ok := app.storage.GetProfileKey(req.Profile); !ok && req.Profile != "" {
|
||||||
app.err.Printf(lm.FailedGetProfile, req.Profile)
|
app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile)
|
||||||
respond(500, "Profile not found", gc)
|
respond(500, "Profile not found", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -390,11 +424,11 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
|||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
changed := false
|
changed := false
|
||||||
for code, settings := range req {
|
for code, settings := range req {
|
||||||
|
app.debug.Printf("%s: Notification settings change requested", code)
|
||||||
invite, ok := app.storage.GetInvitesKey(code)
|
invite, ok := app.storage.GetInvitesKey(code)
|
||||||
if !ok {
|
if !ok {
|
||||||
msg := fmt.Sprintf(lm.InvalidInviteCode, code)
|
app.err.Printf("%s Notification setting change failed: Invalid code", code)
|
||||||
app.err.Println(msg)
|
respond(400, "Invalid invite code", gc)
|
||||||
respond(400, msg, gc)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var address string
|
var address string
|
||||||
@ -402,8 +436,9 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
|||||||
if jellyfinLogin {
|
if jellyfinLogin {
|
||||||
var addressAvailable bool = app.getAddressOrName(gc.GetString("jfId")) != ""
|
var addressAvailable bool = app.getAddressOrName(gc.GetString("jfId")) != ""
|
||||||
if !addressAvailable {
|
if !addressAvailable {
|
||||||
app.err.Printf(lm.FailedGetContactMethod, gc.GetString("jfId"))
|
app.err.Printf("%s: Couldn't find contact method for admin. Make sure one is set.", code)
|
||||||
respond(500, fmt.Sprintf(lm.FailedGetContactMethod, "admin"), gc)
|
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
||||||
|
respond(500, "Missing user contact method", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
address = gc.GetString("jfId")
|
address = gc.GetString("jfId")
|
||||||
@ -418,12 +453,15 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
|||||||
} /*else {
|
} /*else {
|
||||||
if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
|
if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
|
||||||
*/
|
*/
|
||||||
for _, notifyType := range []string{"notify-expiry", "notify-creation"} {
|
if _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] {
|
||||||
if _, ok := settings[notifyType]; ok && invite.Notify[address][notifyType] != settings[notifyType] {
|
invite.Notify[address]["notify-expiry"] = settings["notify-expiry"]
|
||||||
invite.Notify[address][notifyType] = settings[notifyType]
|
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address)
|
||||||
app.debug.Printf(lm.SetAdminNotify, notifyType, settings[notifyType], address)
|
changed = true
|
||||||
changed = true
|
}
|
||||||
}
|
if _, ok := settings["notify-creation"]; ok && invite.Notify[address]["notify-creation"] != settings["notify-creation"] {
|
||||||
|
invite.Notify[address]["notify-creation"] = settings["notify-creation"]
|
||||||
|
app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings["notify-creation"], address)
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
app.storage.SetInvitesKey(code, invite)
|
app.storage.SetInvitesKey(code, invite)
|
||||||
@ -442,6 +480,7 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
|||||||
func (app *appContext) DeleteInvite(gc *gin.Context) {
|
func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||||
var req deleteInviteDTO
|
var req deleteInviteDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
|
app.debug.Printf("%s: Deletion requested", req.Code)
|
||||||
inv, ok := app.storage.GetInvitesKey(req.Code)
|
inv, ok := app.storage.GetInvitesKey(req.Code)
|
||||||
if ok {
|
if ok {
|
||||||
app.storage.DeleteInvitesKey(req.Code)
|
app.storage.DeleteInvitesKey(req.Code)
|
||||||
@ -456,10 +495,10 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
|||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
}, gc, false)
|
}, gc, false)
|
||||||
|
|
||||||
app.info.Printf(lm.DeleteInvite, req.Code)
|
app.info.Printf("%s: Invite deleted", req.Code)
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.err.Printf(lm.FailedDeleteInvite, req.Code, "invalid code")
|
app.err.Printf("%s: Deletion failed: Invalid code", req.Code)
|
||||||
respond(400, "Code doesn't exist", gc)
|
respond(400, "Code doesn't exist", gc)
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/hrfee/jfa-go/jellyseerr"
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Summary Get a list of Jellyseerr users.
|
// @Summary Get a list of Jellyseerr users.
|
||||||
@ -18,12 +15,14 @@ import (
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Jellyseerr
|
// @tags Jellyseerr
|
||||||
func (app *appContext) JellyseerrUsers(gc *gin.Context) {
|
func (app *appContext) JellyseerrUsers(gc *gin.Context) {
|
||||||
|
app.debug.Println("Jellyseerr users requested")
|
||||||
users, err := app.js.GetUsers()
|
users, err := app.js.GetUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyseerr, err)
|
app.err.Printf("Failed to get users from Jellyseerr: %v", err)
|
||||||
respond(500, "Couldn't get users", gc)
|
respond(500, "Couldn't get users", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
app.debug.Printf("Jellyseerr users retrieved: %d", len(users))
|
||||||
userlist := make([]ombiUser, len(users))
|
userlist := make([]ombiUser, len(users))
|
||||||
i := 0
|
i := 0
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
@ -61,14 +60,14 @@ func (app *appContext) SetJellyseerrProfile(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
u, err := app.js.UserByID(jellyseerrID)
|
u, err := app.js.UserByID(jellyseerrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyseerr, err)
|
app.err.Printf("Couldn't get user from Jellyseerr: %v", err)
|
||||||
respond(500, "Couldn't get user", gc)
|
respond(500, "Couldn't get user", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
profile.Jellyseerr.User = u.UserTemplate
|
profile.Jellyseerr.User = u.UserTemplate
|
||||||
n, err := app.js.GetNotificationPreferencesByID(jellyseerrID)
|
n, err := app.js.GetNotificationPreferencesByID(jellyseerrID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGetJellyseerrNotificationPrefs, gc.Param("id"), err)
|
app.err.Printf("Couldn't get user's notification prefs from Jellyseerr: %v", err)
|
||||||
respond(500, "Couldn't get user notification prefs", gc)
|
respond(500, "Couldn't get user notification prefs", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -99,67 +98,3 @@ func (app *appContext) DeleteJellyseerrProfile(gc *gin.Context) {
|
|||||||
app.storage.SetProfileKey(profileName, profile)
|
app.storage.SetProfileKey(profileName, profile)
|
||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
type JellyseerrWrapper struct {
|
|
||||||
*jellyseerr.Jellyseerr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (js *JellyseerrWrapper) ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool) {
|
|
||||||
// Gets existing user (not possible) or imports the given user.
|
|
||||||
_, err = js.MustGetUser(jellyfinID)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ok = true
|
|
||||||
err = js.ApplyTemplateToUser(jellyfinID, profile.Jellyseerr.User)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, jellyfinID, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = js.ApplyNotificationsTemplateToUser(jellyfinID, profile.Jellyseerr.Notifications)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf(lm.FailedApplyTemplate, "notifications", lm.Jellyseerr, jellyfinID, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (js *JellyseerrWrapper) AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error) {
|
|
||||||
_, err = js.MustGetUser(jellyfinID)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
contactMethods := map[jellyseerr.NotificationsField]any{}
|
|
||||||
if emailEnabled {
|
|
||||||
err = js.ModifyMainUserSettings(jellyfinID, jellyseerr.MainUserSettings{Email: req.Email})
|
|
||||||
if err != nil {
|
|
||||||
// FIXME: This is a little ugly, considering all other errors are unformatted
|
|
||||||
err = fmt.Errorf(lm.FailedSetEmailAddress, lm.Jellyseerr, jellyfinID, err)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
contactMethods[jellyseerr.FieldEmailEnabled] = req.EmailContact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if discordEnabled && discord != nil {
|
|
||||||
contactMethods[jellyseerr.FieldDiscord] = discord.ID
|
|
||||||
contactMethods[jellyseerr.FieldDiscordEnabled] = req.DiscordContact
|
|
||||||
}
|
|
||||||
if telegramEnabled && discord != nil {
|
|
||||||
contactMethods[jellyseerr.FieldTelegram] = telegram.ChatID
|
|
||||||
contactMethods[jellyseerr.FieldTelegramEnabled] = req.TelegramContact
|
|
||||||
}
|
|
||||||
if len(contactMethods) > 0 {
|
|
||||||
err = js.ModifyNotifications(jellyfinID, contactMethods)
|
|
||||||
if err != nil {
|
|
||||||
// app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (js *JellyseerrWrapper) Name() string { return lm.Jellyseerr }
|
|
||||||
|
|
||||||
func (js *JellyseerrWrapper) Enabled(app *appContext, profile *Profile) bool {
|
|
||||||
return profile != nil && profile.Jellyseerr.Enabled && app.config.Section("jellyseerr").Key("enabled").MustBool(false)
|
|
||||||
}
|
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/hrfee/jfa-go/jellyseerr"
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
@ -135,7 +134,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
|||||||
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
|
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
|
||||||
customMessage, ok := app.storage.GetCustomContentKey(id)
|
customMessage, ok := app.storage.GetCustomContentKey(id)
|
||||||
if !ok && id != "Announcement" {
|
if !ok && id != "Announcement" {
|
||||||
app.err.Printf(lm.FailedGetCustomMessage, id)
|
app.err.Printf("Failed to get custom message with ID \"%s\"", id)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -329,7 +328,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
|||||||
jellyseerr.FieldTelegram: tgUser.ChatID,
|
jellyseerr.FieldTelegram: tgUser.ChatID,
|
||||||
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
|
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
linkExistingOmbiDiscordTelegram(app)
|
linkExistingOmbiDiscordTelegram(app)
|
||||||
@ -362,7 +361,11 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
|||||||
tgUser.Contact = req.Telegram
|
tgUser.Contact = req.Telegram
|
||||||
app.storage.SetTelegramKey(req.ID, tgUser)
|
app.storage.SetTelegramKey(req.ID, tgUser)
|
||||||
if change {
|
if change {
|
||||||
app.debug.Printf(lm.SetContactPrefForService, lm.Telegram, tgUser.Username, req.Telegram)
|
msg := ""
|
||||||
|
if !req.Telegram {
|
||||||
|
msg = " not"
|
||||||
|
}
|
||||||
|
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
|
||||||
jsPrefs[jellyseerr.FieldTelegramEnabled] = req.Telegram
|
jsPrefs[jellyseerr.FieldTelegramEnabled] = req.Telegram
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -371,7 +374,11 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
|||||||
dcUser.Contact = req.Discord
|
dcUser.Contact = req.Discord
|
||||||
app.storage.SetDiscordKey(req.ID, dcUser)
|
app.storage.SetDiscordKey(req.ID, dcUser)
|
||||||
if change {
|
if change {
|
||||||
app.debug.Printf(lm.SetContactPrefForService, lm.Discord, dcUser.Username, req.Discord)
|
msg := ""
|
||||||
|
if !req.Discord {
|
||||||
|
msg = " not"
|
||||||
|
}
|
||||||
|
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||||
jsPrefs[jellyseerr.FieldDiscordEnabled] = req.Discord
|
jsPrefs[jellyseerr.FieldDiscordEnabled] = req.Discord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -380,7 +387,11 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
|||||||
mxUser.Contact = req.Matrix
|
mxUser.Contact = req.Matrix
|
||||||
app.storage.SetMatrixKey(req.ID, mxUser)
|
app.storage.SetMatrixKey(req.ID, mxUser)
|
||||||
if change {
|
if change {
|
||||||
app.debug.Printf(lm.SetContactPrefForService, lm.Matrix, mxUser.UserID, req.Matrix)
|
msg := ""
|
||||||
|
if !req.Matrix {
|
||||||
|
msg = " not"
|
||||||
|
}
|
||||||
|
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if email, ok := app.storage.GetEmailsKey(req.ID); ok {
|
if email, ok := app.storage.GetEmailsKey(req.ID); ok {
|
||||||
@ -388,14 +399,18 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
|||||||
email.Contact = req.Email
|
email.Contact = req.Email
|
||||||
app.storage.SetEmailsKey(req.ID, email)
|
app.storage.SetEmailsKey(req.ID, email)
|
||||||
if change {
|
if change {
|
||||||
app.debug.Printf(lm.SetContactPrefForService, lm.Email, email.Addr, req.Email)
|
msg := ""
|
||||||
|
if !req.Email {
|
||||||
|
msg = " not"
|
||||||
|
}
|
||||||
|
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
|
||||||
jsPrefs[jellyseerr.FieldEmailEnabled] = req.Email
|
jsPrefs[jellyseerr.FieldEmailEnabled] = req.Email
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
||||||
err := app.js.ModifyNotifications(req.ID, jsPrefs)
|
err := app.js.ModifyNotifications(req.ID, jsPrefs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
app.err.Printf("Failed to sync contact prefs with Jellyseerr: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
@ -431,7 +446,7 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
|||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
token, ok := app.telegram.TokenVerified(pin)
|
token, ok := app.telegram.TokenVerified(pin)
|
||||||
if ok && app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
|
if ok && app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
|
||||||
app.discord.DeleteVerifiedToken(pin)
|
app.discord.DeleteVerifiedUser(pin)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -454,7 +469,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
user, ok := app.discord.UserVerified(pin)
|
user, ok := app.discord.UserVerified(pin)
|
||||||
if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.MethodID().(string)) {
|
if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.ID) {
|
||||||
delete(app.discord.verifiedTokens, pin)
|
delete(app.discord.verifiedTokens, pin)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
@ -472,7 +487,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
|||||||
// @Router /invite/{invCode}/discord/invite [get]
|
// @Router /invite/{invCode}/discord/invite [get]
|
||||||
// @tags Other
|
// @tags Other
|
||||||
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||||
if app.discord.InviteChannel.Name == "" {
|
if app.discord.inviteChannelName == "" {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -540,7 +555,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
|||||||
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||||
code := gc.Param("invCode")
|
code := gc.Param("invCode")
|
||||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||||
app.debug.Printf(lm.InvalidInviteCode, code)
|
app.debug.Println("Matrix: Invite code was invalid")
|
||||||
respondBool(401, false, gc)
|
respondBool(401, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -548,12 +563,12 @@ func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
|||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
user, ok := app.matrix.tokens[pin]
|
user, ok := app.matrix.tokens[pin]
|
||||||
if !ok {
|
if !ok {
|
||||||
app.debug.Printf(lm.InvalidPIN, pin)
|
app.debug.Println("Matrix: PIN not found")
|
||||||
respondBool(200, false, gc)
|
respondBool(200, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if user.User.UserID != userID {
|
if user.User.UserID != userID {
|
||||||
app.debug.Printf(lm.UnauthorizedPIN, pin)
|
app.debug.Println("Matrix: User ID of PIN didn't match")
|
||||||
respondBool(200, false, gc)
|
respondBool(200, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -581,7 +596,7 @@ func (app *appContext) MatrixLogin(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password)
|
token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGenerateToken, err)
|
app.err.Printf("Matrix: Failed to generate token: %v", err)
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -592,7 +607,7 @@ func (app *appContext) MatrixLogin(gc *gin.Context) {
|
|||||||
matrix.Key("token").SetValue(token)
|
matrix.Key("token").SetValue(token)
|
||||||
matrix.Key("user_id").SetValue(req.Username)
|
matrix.Key("user_id").SetValue(req.Username)
|
||||||
if err := tempConfig.SaveTo(app.configPath); err != nil {
|
if err := tempConfig.SaveTo(app.configPath); err != nil {
|
||||||
app.err.Printf(lm.FailedWriting, app.configPath, err)
|
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -616,7 +631,7 @@ func (app *appContext) MatrixConnect(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
|
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedCreateRoom, err)
|
app.err.Printf("Matrix: Failed to create room: %v", err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -686,7 +701,7 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
|
|||||||
jellyseerr.FieldDiscord: req.DiscordID,
|
jellyseerr.FieldDiscord: req.DiscordID,
|
||||||
jellyseerr.FieldDiscordEnabled: true,
|
jellyseerr.FieldDiscordEnabled: true,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -724,7 +739,7 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
|
|||||||
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
|
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
|
||||||
jellyseerr.FieldDiscordEnabled: false,
|
jellyseerr.FieldDiscordEnabled: false,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -760,7 +775,7 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
|
|||||||
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
|
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
|
||||||
jellyseerr.FieldTelegramEnabled: false,
|
jellyseerr.FieldTelegramEnabled: false,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
|
95
api-ombi.go
95
api-ombi.go
@ -1,18 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/jfa-go/ombi"
|
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
|
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
|
||||||
|
ombiUsers, code, err := app.ombi.GetUsers()
|
||||||
|
if err != nil || code != 200 {
|
||||||
|
return nil, code, err
|
||||||
|
}
|
||||||
jfUser, code, err := app.jf.UserByID(jfID, false)
|
jfUser, code, err := app.jf.UserByID(jfID, false)
|
||||||
if err != nil || code != 200 {
|
if err != nil || code != 200 {
|
||||||
return nil, code, err
|
return nil, code, err
|
||||||
@ -22,14 +22,6 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
|
|||||||
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
||||||
email = e.Addr
|
email = e.Addr
|
||||||
}
|
}
|
||||||
return app.ombi.getUser(username, email)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ombi *OmbiWrapper) getUser(username string, email string) (map[string]interface{}, int, error) {
|
|
||||||
ombiUsers, code, err := ombi.GetUsers()
|
|
||||||
if err != nil || code != 200 {
|
|
||||||
return nil, code, err
|
|
||||||
}
|
|
||||||
for _, ombiUser := range ombiUsers {
|
for _, ombiUser := range ombiUsers {
|
||||||
ombiAddr := ""
|
ombiAddr := ""
|
||||||
if a, ok := ombiUser["emailAddress"]; ok && a != nil {
|
if a, ok := ombiUser["emailAddress"]; ok && a != nil {
|
||||||
@ -39,13 +31,13 @@ func (ombi *OmbiWrapper) getUser(username string, email string) (map[string]inte
|
|||||||
return ombiUser, code, err
|
return ombiUser, code, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, 400, errors.New(lm.NotFound)
|
return nil, 400, fmt.Errorf("couldn't find user")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a user with the given name who has been imported from Jellyfin/Emby by Ombi
|
// Returns a user with the given name who has been imported from Jellyfin/Emby by Ombi
|
||||||
func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, int, error) {
|
func (app *appContext) getOmbiImportedUser(name string) (map[string]interface{}, int, error) {
|
||||||
// Ombi User Types: 3/4 = Emby, 5 = Jellyfin
|
// Ombi User Types: 3/4 = Emby, 5 = Jellyfin
|
||||||
ombiUsers, code, err := ombi.GetUsers()
|
ombiUsers, code, err := app.ombi.GetUsers()
|
||||||
if err != nil || code != 200 {
|
if err != nil || code != 200 {
|
||||||
return nil, code, err
|
return nil, code, err
|
||||||
}
|
}
|
||||||
@ -74,9 +66,10 @@ func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, i
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Ombi
|
// @tags Ombi
|
||||||
func (app *appContext) OmbiUsers(gc *gin.Context) {
|
func (app *appContext) OmbiUsers(gc *gin.Context) {
|
||||||
|
app.debug.Println("Ombi users requested")
|
||||||
users, status, err := app.ombi.GetUsers()
|
users, status, err := app.ombi.GetUsers()
|
||||||
if err != nil || status != 200 {
|
if err != nil || status != 200 {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
|
app.err.Printf("Failed to get users from Ombi (%d): %v", status, err)
|
||||||
respond(500, "Couldn't get users", gc)
|
respond(500, "Couldn't get users", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -112,7 +105,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
template, code, err := app.ombi.TemplateByID(req.ID)
|
template, code, err := app.ombi.TemplateByID(req.ID)
|
||||||
if err != nil || code != 200 || len(template) == 0 {
|
if err != nil || code != 200 || len(template) == 0 {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
|
app.err.Printf("Couldn't get user from Ombi (%d): %v", code, err)
|
||||||
respond(500, "Couldn't get user", gc)
|
respond(500, "Couldn't get user", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -143,11 +136,7 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
|
|||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
type OmbiWrapper struct {
|
func (app *appContext) applyOmbiProfile(user map[string]interface{}, profile map[string]interface{}) (status int, err error) {
|
||||||
*ombi.Ombi
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[string]interface{}) (status int, err error) {
|
|
||||||
for k, v := range profile {
|
for k, v := range profile {
|
||||||
switch v.(type) {
|
switch v.(type) {
|
||||||
case map[string]interface{}, []interface{}:
|
case map[string]interface{}, []interface{}:
|
||||||
@ -158,66 +147,6 @@ func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
status, err = ombi.ModifyUser(user)
|
status, err = app.ombi.ModifyUser(user)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ombi *OmbiWrapper) ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool) {
|
|
||||||
errors, code, err := ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi)
|
|
||||||
var ombiUser map[string]interface{}
|
|
||||||
var status int
|
|
||||||
if err != nil || code != 200 {
|
|
||||||
// Check if on the off chance, Ombi's user importer has already added the account.
|
|
||||||
ombiUser, status, err = ombi.getImportedUser(req.Username)
|
|
||||||
if status == 200 && err == nil {
|
|
||||||
// app.info.Println(lm.Ombi + " " + lm.UserExists)
|
|
||||||
profile.Ombi["password"] = req.Password
|
|
||||||
status, err = ombi.applyProfile(ombiUser, profile.Ombi)
|
|
||||||
if status != 200 || err != nil {
|
|
||||||
err = fmt.Errorf(lm.FailedApplyProfile, lm.Ombi, req.Username, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if len(errors) != 0 {
|
|
||||||
err = fmt.Errorf("%v, %s", err, strings.Join(errors, ", "))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ok = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ombi *OmbiWrapper) AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error) {
|
|
||||||
var ombiUser map[string]interface{}
|
|
||||||
var status int
|
|
||||||
ombiUser, status, err = ombi.getUser(req.Username, req.Email)
|
|
||||||
if status != 200 || err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if discordEnabled || telegramEnabled {
|
|
||||||
dID := ""
|
|
||||||
tUser := ""
|
|
||||||
if discord != nil {
|
|
||||||
dID = discord.ID
|
|
||||||
}
|
|
||||||
if telegram != nil {
|
|
||||||
tUser = telegram.Username
|
|
||||||
}
|
|
||||||
var resp string
|
|
||||||
var status int
|
|
||||||
resp, status, err = ombi.SetNotificationPrefs(ombiUser, dID, tUser)
|
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
|
||||||
if resp != "" {
|
|
||||||
err = fmt.Errorf("%v, %s", err, resp)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ombi *OmbiWrapper) Name() string { return lm.Ombi }
|
|
||||||
|
|
||||||
func (ombi *OmbiWrapper) Enabled(app *appContext, profile *Profile) bool {
|
|
||||||
return profile != nil && profile.Ombi != nil && len(profile.Ombi) != 0 && app.config.Section("ombi").Key("enabled").MustBool(false)
|
|
||||||
}
|
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,6 +14,7 @@ import (
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Profiles & Settings
|
// @tags Profiles & Settings
|
||||||
func (app *appContext) GetProfiles(gc *gin.Context) {
|
func (app *appContext) GetProfiles(gc *gin.Context) {
|
||||||
|
app.debug.Println("Profiles requested")
|
||||||
out := getProfilesDTO{
|
out := getProfilesDTO{
|
||||||
DefaultProfile: app.storage.GetDefaultProfile().Name,
|
DefaultProfile: app.storage.GetDefaultProfile().Name,
|
||||||
Profiles: map[string]profileDTO{},
|
Profiles: map[string]profileDTO{},
|
||||||
@ -53,11 +52,10 @@ func (app *appContext) GetProfiles(gc *gin.Context) {
|
|||||||
func (app *appContext) SetDefaultProfile(gc *gin.Context) {
|
func (app *appContext) SetDefaultProfile(gc *gin.Context) {
|
||||||
req := profileChangeDTO{}
|
req := profileChangeDTO{}
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
app.info.Printf(lm.SetDefaultProfile, req.Name)
|
app.info.Printf("Setting default profile to \"%s\"", req.Name)
|
||||||
if _, ok := app.storage.GetProfileKey(req.Name); !ok {
|
if _, ok := app.storage.GetProfileKey(req.Name); !ok {
|
||||||
msg := fmt.Sprintf(lm.FailedGetProfile, req.Name)
|
app.err.Printf("Profile not found: \"%s\"", req.Name)
|
||||||
app.err.Println(msg)
|
respond(500, "Profile not found", gc)
|
||||||
respond(500, msg, gc)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.storage.db.ForEach(&badgerhold.Query{}, func(profile *Profile) error {
|
app.storage.db.ForEach(&badgerhold.Query{}, func(profile *Profile) error {
|
||||||
@ -81,12 +79,13 @@ func (app *appContext) SetDefaultProfile(gc *gin.Context) {
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Profiles & Settings
|
// @tags Profiles & Settings
|
||||||
func (app *appContext) CreateProfile(gc *gin.Context) {
|
func (app *appContext) CreateProfile(gc *gin.Context) {
|
||||||
|
app.info.Println("Profile creation requested")
|
||||||
var req newProfileDTO
|
var req newProfileDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
app.jf.CacheExpiry = time.Now()
|
app.jf.CacheExpiry = time.Now()
|
||||||
user, status, err := app.jf.UserByID(req.ID, false)
|
user, status, err := app.jf.UserByID(req.ID, false)
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf("Failed to get user from Jellyfin (%d): %v", status, err)
|
||||||
respond(500, "Couldn't get user", gc)
|
respond(500, "Couldn't get user", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -95,12 +94,12 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
|
|||||||
Policy: user.Policy,
|
Policy: user.Policy,
|
||||||
Homescreen: req.Homescreen,
|
Homescreen: req.Homescreen,
|
||||||
}
|
}
|
||||||
app.debug.Printf(lm.CreateProfileFromUser, user.Name)
|
app.debug.Printf("Creating profile from user \"%s\"", user.Name)
|
||||||
if req.Homescreen {
|
if req.Homescreen {
|
||||||
profile.Configuration = user.Configuration
|
profile.Configuration = user.Configuration
|
||||||
profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
|
profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err)
|
app.err.Printf("Failed to get DisplayPrefs (%d): %v", status, err)
|
||||||
respond(500, "Couldn't get displayprefs", gc)
|
respond(500, "Couldn't get displayprefs", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -146,13 +145,13 @@ func (app *appContext) EnableReferralForProfile(gc *gin.Context) {
|
|||||||
inv, ok := app.storage.GetInvitesKey(invCode)
|
inv, ok := app.storage.GetInvitesKey(invCode)
|
||||||
if !ok {
|
if !ok {
|
||||||
respond(400, "Invalid invite code", gc)
|
respond(400, "Invalid invite code", gc)
|
||||||
app.err.Printf(lm.InvalidInviteCode, invCode)
|
app.err.Printf("\"%s\": Failed to enable referrals: invite not found", profileName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
profile, ok := app.storage.GetProfileKey(profileName)
|
profile, ok := app.storage.GetProfileKey(profileName)
|
||||||
if !ok {
|
if !ok {
|
||||||
respond(400, "Invalid profile", gc)
|
respond(400, "Invalid profile", gc)
|
||||||
app.err.Printf(lm.FailedGetProfile, profileName)
|
app.err.Printf("\"%s\": Failed to enable referrals: profile not found", profileName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@ -10,7 +9,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/hrfee/jfa-go/jellyseerr"
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
)
|
)
|
||||||
@ -31,7 +29,7 @@ func (app *appContext) MyDetails(gc *gin.Context) {
|
|||||||
|
|
||||||
user, status, err := app.jf.UserByID(resp.Id, false)
|
user, status, err := app.jf.UserByID(resp.Id, false)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf("Failed to get Jellyfin user (%d): %+v\n", status, err)
|
||||||
respond(500, "Failed to get user", gc)
|
respond(500, "Failed to get user", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -135,9 +133,8 @@ func (app *appContext) SetMyContactMethods(gc *gin.Context) {
|
|||||||
func (app *appContext) LogoutUser(gc *gin.Context) {
|
func (app *appContext) LogoutUser(gc *gin.Context) {
|
||||||
cookie, err := gc.Cookie("user-refresh")
|
cookie, err := gc.Cookie("user-refresh")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf(lm.FailedGetCookies, "user-refresh", err)
|
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||||
app.debug.Println(msg)
|
respond(500, "Couldn't fetch cookies", gc)
|
||||||
respond(500, msg, gc)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||||
@ -177,21 +174,21 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
|||||||
}
|
}
|
||||||
token, err := jwt.Parse(key, checkToken)
|
token, err := jwt.Parse(key, checkToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedParseJWT, err)
|
app.err.Printf("Failed to parse key: %s", err)
|
||||||
fail()
|
fail()
|
||||||
// respond(500, "unknownError", gc)
|
// respond(500, "unknownError", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
if !ok {
|
if !ok {
|
||||||
app.err.Println(lm.FailedCastJWT)
|
app.err.Printf("Failed to parse key: %s", err)
|
||||||
fail()
|
fail()
|
||||||
// respond(500, "unknownError", gc)
|
// respond(500, "unknownError", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
|
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
|
||||||
if !(ok && token.Valid && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
if !(ok && token.Valid && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
||||||
app.err.Println(lm.InvalidJWT)
|
app.err.Printf("Invalid key")
|
||||||
fail()
|
fail()
|
||||||
// respond(400, "invalidKey", gc)
|
// respond(400, "invalidKey", gc)
|
||||||
return
|
return
|
||||||
@ -215,7 +212,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
|||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
}, gc, true)
|
}, gc, true)
|
||||||
|
|
||||||
app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId"))
|
app.info.Println("Email list modified")
|
||||||
gc.Redirect(http.StatusSeeOther, "/my/account")
|
gc.Redirect(http.StatusSeeOther, "/my/account")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -234,6 +231,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
|||||||
func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
||||||
var req ModifyMyEmailDTO
|
var req ModifyMyEmailDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
|
app.debug.Println("Email modification requested")
|
||||||
if !strings.ContainsRune(req.Email, '@') {
|
if !strings.ContainsRune(req.Email, '@') {
|
||||||
respond(400, "Invalid Email Address", gc)
|
respond(400, "Invalid Email Address", gc)
|
||||||
return
|
return
|
||||||
@ -253,7 +251,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
|||||||
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedSignJWT, err)
|
app.err.Printf("Failed to generate confirmation token: %v", err)
|
||||||
respond(500, "errorUnknown", gc)
|
respond(500, "errorUnknown", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -264,15 +262,15 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
|||||||
if status == 200 && err == nil {
|
if status == 200 && err == nil {
|
||||||
name = user.Name
|
name = user.Name
|
||||||
}
|
}
|
||||||
app.debug.Printf(lm.EmailConfirmationRequired, id)
|
app.debug.Printf("%s: Email confirmation required", id)
|
||||||
respond(401, "confirmEmail", gc)
|
respond(401, "confirmEmail", gc)
|
||||||
msg, err := app.email.constructConfirmation("", name, key, app, false)
|
msg, err := app.email.constructConfirmation("", name, key, app, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructConfirmationEmail, id, err)
|
app.err.Printf("%s: Failed to construct confirmation email: %v", name, err)
|
||||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||||
app.err.Printf(lm.FailedSendConfirmationEmail, id, req.Email, err)
|
app.err.Printf("%s: Failed to send user confirmation email: %v", name, err)
|
||||||
} else {
|
} else {
|
||||||
app.err.Printf(lm.SentConfirmationEmail, id, req.Email)
|
app.info.Printf("%s: Sent user confirmation email to \"%s\"", name, req.Email)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -292,7 +290,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags User Page
|
// @tags User Page
|
||||||
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
|
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
|
||||||
if app.discord.InviteChannel.Name == "" {
|
if app.discord.inviteChannelName == "" {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -340,7 +338,7 @@ func (app *appContext) GetMyPIN(gc *gin.Context) {
|
|||||||
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
dcUser, ok := app.discord.AssignedUserVerified(pin, gc.GetString("jfId"))
|
dcUser, ok := app.discord.AssignedUserVerified(pin, gc.GetString("jfId"))
|
||||||
app.discord.DeleteVerifiedToken(pin)
|
app.discord.DeleteVerifiedUser(pin)
|
||||||
if !ok {
|
if !ok {
|
||||||
respondBool(200, false, gc)
|
respondBool(200, false, gc)
|
||||||
return
|
return
|
||||||
@ -360,7 +358,7 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
|||||||
jellyseerr.FieldDiscord: dcUser.ID,
|
jellyseerr.FieldDiscord: dcUser.ID,
|
||||||
jellyseerr.FieldDiscordEnabled: dcUser.Contact,
|
jellyseerr.FieldDiscordEnabled: dcUser.Contact,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -415,7 +413,7 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
|
|||||||
jellyseerr.FieldTelegram: tgUser.ChatID,
|
jellyseerr.FieldTelegram: tgUser.ChatID,
|
||||||
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
|
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -479,12 +477,12 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
|
|||||||
pin := gc.Param("pin")
|
pin := gc.Param("pin")
|
||||||
user, ok := app.matrix.tokens[pin]
|
user, ok := app.matrix.tokens[pin]
|
||||||
if !ok {
|
if !ok {
|
||||||
app.debug.Printf(lm.InvalidPIN, pin)
|
app.debug.Println("Matrix: PIN not found")
|
||||||
respondBool(200, false, gc)
|
respondBool(200, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if user.User.UserID != userID {
|
if user.User.UserID != userID {
|
||||||
app.debug.Printf(lm.UnauthorizedPIN, pin)
|
app.debug.Println("Matrix: User ID of PIN didn't match")
|
||||||
respondBool(200, false, gc)
|
respondBool(200, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -525,7 +523,7 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
|
|||||||
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
|
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
|
||||||
jellyseerr.FieldDiscordEnabled: false,
|
jellyseerr.FieldDiscordEnabled: false,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -553,7 +551,7 @@ func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
|
|||||||
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
|
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
|
||||||
jellyseerr.FieldTelegramEnabled: false,
|
jellyseerr.FieldTelegramEnabled: false,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -608,6 +606,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
|||||||
contactMethodAllowed := app.config.Section("user_page").Key("allow_pwr_contact_method").MustBool(true)
|
contactMethodAllowed := app.config.Section("user_page").Key("allow_pwr_contact_method").MustBool(true)
|
||||||
address := gc.Param("address")
|
address := gc.Param("address")
|
||||||
if address == "" {
|
if address == "" {
|
||||||
|
app.debug.Println("Ignoring empty request for PWR")
|
||||||
cancel.Stop()
|
cancel.Stop()
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
@ -617,7 +616,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
|||||||
|
|
||||||
jfUser, ok := app.ReverseUserSearch(address, usernameAllowed, emailAllowed, contactMethodAllowed)
|
jfUser, ok := app.ReverseUserSearch(address, usernameAllowed, emailAllowed, contactMethodAllowed)
|
||||||
if !ok {
|
if !ok {
|
||||||
app.debug.Printf(lm.FailedGetUsers, lm.Jellyfin, "no results")
|
app.debug.Printf("Ignoring PWR request: User not found")
|
||||||
|
|
||||||
for range timerWait {
|
for range timerWait {
|
||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
@ -627,7 +626,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
pwr, err = app.GenInternalReset(jfUser.ID)
|
pwr, err = app.GenInternalReset(jfUser.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf("Failed to get user from Jellyfin: %v", err)
|
||||||
for range timerWait {
|
for range timerWait {
|
||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
return
|
return
|
||||||
@ -648,16 +647,16 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
|||||||
}, app, false,
|
}, app, false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
|
app.err.Printf("Failed to construct password reset message for \"%s\": %v", pwr.Username, err)
|
||||||
for range timerWait {
|
for range timerWait {
|
||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
} else if err := app.sendByID(msg, jfUser.ID); err != nil {
|
} else if err := app.sendByID(msg, jfUser.ID); err != nil {
|
||||||
app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, "?", err)
|
app.err.Printf("Failed to send password reset message to \"%s\": %v", address, err)
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf(lm.SentPWRMessage, pwr.Username, "?")
|
app.info.Printf("Sent password reset message to \"%s\"", address)
|
||||||
}
|
}
|
||||||
for range timerWait {
|
for range timerWait {
|
||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
@ -684,13 +683,14 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
|||||||
validation := app.validator.validate(req.New)
|
validation := app.validator.validate(req.New)
|
||||||
for _, val := range validation {
|
for _, val := range validation {
|
||||||
if !val {
|
if !val {
|
||||||
|
app.debug.Printf("%s: Change password failed: Invalid password", gc.GetString("jfId"))
|
||||||
gc.JSON(400, validation)
|
gc.JSON(400, validation)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user, status, err := app.jf.UserByID(gc.GetString("jfId"), false)
|
user, status, err := app.jf.UserByID(gc.GetString("jfId"), false)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, gc.GetString("jfId"), lm.Jellyfin, err)
|
app.err.Printf("Failed to change password: couldn't find user (%d): %+v", status, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -718,16 +718,16 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
|||||||
func() {
|
func() {
|
||||||
ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId"))
|
ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId"))
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, user.Name, lm.Ombi, err)
|
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", user.Name, status, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ombiUser["password"] = req.New
|
ombiUser["password"] = req.New
|
||||||
status, err = app.ombi.ModifyUser(ombiUser)
|
status, err = app.ombi.ModifyUser(ombiUser)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
|
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.debug.Printf(lm.ChangePassword, lm.Ombi, ombiUser["userName"])
|
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
cookie, err := gc.Cookie("user-refresh")
|
cookie, err := gc.Cookie("user-refresh")
|
||||||
@ -735,7 +735,7 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
|||||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||||
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
|
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
|
||||||
} else {
|
} else {
|
||||||
app.debug.Printf(lm.FailedGetCookies, "user-refresh", err)
|
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||||
}
|
}
|
||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
}
|
}
|
||||||
@ -761,7 +761,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
|
|||||||
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
|
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
|
||||||
err = app.storage.db.Get(user.ReferralTemplateKey, &inv)
|
err = app.storage.db.Get(user.ReferralTemplateKey, &inv)
|
||||||
if !ok || err != nil || user.ReferralTemplateKey == "" {
|
if !ok || err != nil || user.ReferralTemplateKey == "" {
|
||||||
app.debug.Printf(lm.FailedGetReferralTemplate, user.ReferralTemplateKey, err)
|
app.debug.Printf("Ignoring referral request, couldn't find template.")
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -782,7 +782,6 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
|
|||||||
// If UseReferralExpiry is enabled, we delete it and return nothing.
|
// If UseReferralExpiry is enabled, we delete it and return nothing.
|
||||||
app.storage.DeleteInvitesKey(inv.Code)
|
app.storage.DeleteInvitesKey(inv.Code)
|
||||||
if inv.UseReferralExpiry {
|
if inv.UseReferralExpiry {
|
||||||
app.debug.Printf(lm.DeleteOldReferral, inv.Code)
|
|
||||||
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
|
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
|
||||||
if ok {
|
if ok {
|
||||||
user.ReferralTemplateKey = ""
|
user.ReferralTemplateKey = ""
|
||||||
@ -792,7 +791,6 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
|
|||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.debug.Printf(lm.RenewOldReferral, inv.Code)
|
|
||||||
inv.Code = GenerateInviteCode()
|
inv.Code = GenerateInviteCode()
|
||||||
inv.Created = time.Now()
|
inv.Created = time.Now()
|
||||||
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
|
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
|
||||||
|
963
api-users.go
963
api-users.go
File diff suppressed because it is too large
Load Diff
63
api.go
63
api.go
@ -1,12 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/itchyny/timefmt-go"
|
"github.com/itchyny/timefmt-go"
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
@ -124,14 +122,14 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !valid || req.PIN == "" {
|
if !valid || req.PIN == "" {
|
||||||
app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", lm.InvalidPassword)
|
app.info.Printf("%s: Password reset failed: Invalid password", req.PIN)
|
||||||
gc.JSON(400, validation)
|
gc.JSON(400, validation)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isInternal := false
|
isInternal := false
|
||||||
|
|
||||||
if captcha && !app.verifyCaptcha(req.PIN, req.PIN, req.CaptchaText, true) {
|
if captcha && !app.verifyCaptcha(req.PIN, req.PIN, req.CaptchaText, true) {
|
||||||
app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", lm.IncorrectCaptcha)
|
app.info.Printf("%s: PWR Failed: Captcha Incorrect", req.PIN)
|
||||||
respond(400, "errorCaptcha", gc)
|
respond(400, "errorCaptcha", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -140,7 +138,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
if reset, ok := app.internalPWRs[req.PIN]; ok {
|
if reset, ok := app.internalPWRs[req.PIN]; ok {
|
||||||
isInternal = true
|
isInternal = true
|
||||||
if time.Now().After(reset.Expiry) {
|
if time.Now().After(reset.Expiry) {
|
||||||
app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, reset.PIN))
|
app.info.Printf("Password reset failed: PIN \"%s\" has expired", reset.PIN)
|
||||||
respondBool(401, false, gc)
|
respondBool(401, false, gc)
|
||||||
delete(app.internalPWRs, req.PIN)
|
delete(app.internalPWRs, req.PIN)
|
||||||
return
|
return
|
||||||
@ -150,7 +148,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
|
|
||||||
status, err := app.jf.ResetPasswordAdmin(userID)
|
status, err := app.jf.ResetPasswordAdmin(userID)
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
|
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
||||||
respondBool(status, false, gc)
|
respondBool(status, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -158,7 +156,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
} else {
|
} else {
|
||||||
resp, status, err := app.jf.ResetPassword(req.PIN)
|
resp, status, err := app.jf.ResetPassword(req.PIN)
|
||||||
if status != 200 || err != nil || !resp.Success {
|
if status != 200 || err != nil || !resp.Success {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
|
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
||||||
respondBool(status, false, gc)
|
respondBool(status, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -178,7 +176,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
user, status, err = app.jf.UserByName(username, false)
|
user, status, err = app.jf.UserByName(username, false)
|
||||||
}
|
}
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
|
app.err.Printf("Failed to get user \"%s\" (%d): %v", username, status, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -197,33 +195,31 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
status, err = app.jf.SetPassword(user.ID, prevPassword, req.Password)
|
status, err = app.jf.SetPassword(user.ID, prevPassword, req.Password)
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, user.ID, err)
|
app.err.Printf("Failed to change password for \"%s\" (%d): %v", username, status, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
// This makes no sense so has been commented out.
|
// Silently fail for changing ombi passwords
|
||||||
// It probably did at some point in the past.
|
|
||||||
/* Silently fail for changing ombi passwords
|
|
||||||
if (status != 200 && status != 204) || err != nil {
|
if (status != 200 && status != 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err)
|
app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err)
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
return
|
return
|
||||||
} */
|
}
|
||||||
ombiUser, status, err := app.getOmbiUser(user.ID)
|
ombiUser, status, err := app.getOmbiUser(user.ID)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, user.ID, lm.Ombi, err)
|
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", username, status, err)
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ombiUser["password"] = req.Password
|
ombiUser["password"] = req.Password
|
||||||
status, err = app.ombi.ModifyUser(ombiUser)
|
status, err = app.ombi.ModifyUser(ombiUser)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Ombi, user.ID, err)
|
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.debug.Printf(lm.ChangePassword, lm.Ombi, user.ID)
|
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
||||||
}
|
}
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
@ -235,6 +231,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Configuration
|
// @tags Configuration
|
||||||
func (app *appContext) GetConfig(gc *gin.Context) {
|
func (app *appContext) GetConfig(gc *gin.Context) {
|
||||||
|
app.info.Println("Config requested")
|
||||||
resp := app.configBase
|
resp := app.configBase
|
||||||
// Load language options
|
// Load language options
|
||||||
formOptions := app.storage.lang.User.getOptions()
|
formOptions := app.storage.lang.User.getOptions()
|
||||||
@ -344,6 +341,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Configuration
|
// @tags Configuration
|
||||||
func (app *appContext) ModifyConfig(gc *gin.Context) {
|
func (app *appContext) ModifyConfig(gc *gin.Context) {
|
||||||
|
app.info.Println("Config modification requested")
|
||||||
var req configDTO
|
var req configDTO
|
||||||
gc.BindJSON(&req)
|
gc.BindJSON(&req)
|
||||||
// Load a new config, as we set various default values in app.config that shouldn't be stored.
|
// Load a new config, as we set various default values in app.config that shouldn't be stored.
|
||||||
@ -368,18 +366,26 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
tempConfig.Section("").Key("first_run").SetValue("false")
|
tempConfig.Section("").Key("first_run").SetValue("false")
|
||||||
if err := tempConfig.SaveTo(app.configPath); err != nil {
|
if err := tempConfig.SaveTo(app.configPath); err != nil {
|
||||||
app.err.Printf(lm.FailedWriting, app.configPath, err)
|
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
|
||||||
respond(500, err.Error(), gc)
|
respond(500, err.Error(), gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.ModifyConfig, app.configPath)
|
app.debug.Println("Config saved")
|
||||||
gc.JSON(200, map[string]bool{"success": true})
|
gc.JSON(200, map[string]bool{"success": true})
|
||||||
if req["restart-program"] != nil && req["restart-program"].(bool) {
|
if req["restart-program"] != nil && req["restart-program"].(bool) {
|
||||||
app.Restart()
|
app.info.Println("Restarting...")
|
||||||
|
if TRAY {
|
||||||
|
TRAYRESTART <- true
|
||||||
|
} else {
|
||||||
|
RESTART <- true
|
||||||
|
}
|
||||||
|
// Safety Sleep (Ensure shutdown tasks get done)
|
||||||
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
app.loadConfig()
|
app.loadConfig()
|
||||||
// Reinitialize password validator on config change, as opposed to every applicable request like in python.
|
// Reinitialize password validator on config change, as opposed to every applicable request like in python.
|
||||||
if _, ok := req["password_validation"]; ok {
|
if _, ok := req["password_validation"]; ok {
|
||||||
|
app.debug.Println("Reinitializing validator")
|
||||||
validatorConf := ValidatorConf{
|
validatorConf := ValidatorConf{
|
||||||
"length": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
"length": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
||||||
"uppercase": app.config.Section("password_validation").Key("upper").MustInt(0),
|
"uppercase": app.config.Section("password_validation").Key("upper").MustInt(0),
|
||||||
@ -419,13 +425,12 @@ func (app *appContext) CheckUpdate(gc *gin.Context) {
|
|||||||
// @tags Configuration
|
// @tags Configuration
|
||||||
func (app *appContext) ApplyUpdate(gc *gin.Context) {
|
func (app *appContext) ApplyUpdate(gc *gin.Context) {
|
||||||
if !app.update.CanUpdate {
|
if !app.update.CanUpdate {
|
||||||
app.info.Printf(lm.FailedApplyUpdate, lm.UpdateManual)
|
respond(400, "Update is manual", gc)
|
||||||
respond(400, lm.UpdateManual, gc)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := app.update.update()
|
err := app.update.update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedApplyUpdate, err)
|
app.err.Printf("Failed to apply update: %v", err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -447,9 +452,8 @@ func (app *appContext) ApplyUpdate(gc *gin.Context) {
|
|||||||
func (app *appContext) Logout(gc *gin.Context) {
|
func (app *appContext) Logout(gc *gin.Context) {
|
||||||
cookie, err := gc.Cookie("refresh")
|
cookie, err := gc.Cookie("refresh")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf(lm.FailedGetCookies, "refresh", err)
|
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||||
app.debug.Println(msg)
|
respond(500, "Couldn't fetch cookies", gc)
|
||||||
respond(500, msg, gc)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||||
@ -522,7 +526,11 @@ func (app *appContext) ServeLang(gc *gin.Context) {
|
|||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
// @tags Other
|
// @tags Other
|
||||||
func (app *appContext) restart(gc *gin.Context) {
|
func (app *appContext) restart(gc *gin.Context) {
|
||||||
app.Restart()
|
app.info.Println("Restarting...")
|
||||||
|
err := app.Restart()
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Couldn't restart, try restarting manually: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Returns the last 100 lines of the log.
|
// @Summary Returns the last 100 lines of the log.
|
||||||
@ -536,7 +544,6 @@ func (app *appContext) GetLog(gc *gin.Context) {
|
|||||||
|
|
||||||
// no need to syscall.exec anymore!
|
// no need to syscall.exec anymore!
|
||||||
func (app *appContext) Restart() error {
|
func (app *appContext) Restart() error {
|
||||||
app.info.Println(lm.Restarting)
|
|
||||||
if TRAY {
|
if TRAY {
|
||||||
TRAYRESTART <- true
|
TRAYRESTART <- true
|
||||||
} else {
|
} else {
|
||||||
|
65
auth.go
65
auth.go
@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
)
|
)
|
||||||
@ -42,8 +41,6 @@ func (app *appContext) webAuth() gin.HandlerFunc {
|
|||||||
return app.authenticate
|
return app.authenticate
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) authLog(v any) { app.debug.Printf(lm.FailedAuthRequest, v) }
|
|
||||||
|
|
||||||
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
|
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
|
||||||
func CreateToken(userId, jfId string, admin bool) (string, string, error) {
|
func CreateToken(userId, jfId string, admin bool) (string, string, error) {
|
||||||
var token, refresh string
|
var token, refresh string
|
||||||
@ -75,26 +72,32 @@ func (app *appContext) decodeValidateAuthHeader(gc *gin.Context) (claims jwt.Map
|
|||||||
ok = false
|
ok = false
|
||||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||||
if header[0] != "Bearer" {
|
if header[0] != "Bearer" {
|
||||||
app.authLog(lm.InvalidAuthHeader)
|
app.debug.Println("Invalid authorization header")
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
token, err := jwt.Parse(string(header[1]), checkToken)
|
token, err := jwt.Parse(string(header[1]), checkToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.authLog(fmt.Sprintf(lm.FailedParseJWT, err))
|
app.debug.Printf("Auth denied: %s", err)
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
claims, ok = token.Claims.(jwt.MapClaims)
|
claims, ok = token.Claims.(jwt.MapClaims)
|
||||||
if !ok {
|
if !ok {
|
||||||
app.authLog(lm.FailedCastJWT)
|
app.debug.Println("Invalid JWT")
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
expiryUnix := int64(claims["exp"].(float64))
|
expiryUnix := int64(claims["exp"].(float64))
|
||||||
|
if err != nil {
|
||||||
|
app.debug.Printf("Auth denied: %s", err)
|
||||||
|
respond(401, "Unauthorized", gc)
|
||||||
|
ok = false
|
||||||
|
return
|
||||||
|
}
|
||||||
expiry := time.Unix(expiryUnix, 0)
|
expiry := time.Unix(expiryUnix, 0)
|
||||||
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
|
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
|
||||||
app.authLog(lm.InvalidJWT)
|
app.debug.Printf("Auth denied: Invalid token")
|
||||||
// app.debug.Printf("Expiry: %+v, OK: %t, Valid: %t, ClaimType: %s\n", expiry, ok, token.Valid, claims["type"].(string))
|
// app.debug.Printf("Expiry: %+v, OK: %t, Valid: %t, ClaimType: %s\n", expiry, ok, token.Valid, claims["type"].(string))
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
ok = false
|
ok = false
|
||||||
@ -112,7 +115,7 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
isAdminToken := claims["admin"].(bool)
|
isAdminToken := claims["admin"].(bool)
|
||||||
if !isAdminToken {
|
if !isAdminToken {
|
||||||
app.authLog(lm.NonAdminToken)
|
app.debug.Printf("Auth denied: Token was not for admin access")
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -127,13 +130,14 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !match {
|
if !match {
|
||||||
app.authLog(fmt.Sprintf(lm.NonAdminUser, userID))
|
app.debug.Printf("Couldn't find user ID \"%s\"", userID)
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gc.Set("jfId", jfID)
|
gc.Set("jfId", jfID)
|
||||||
gc.Set("userId", userID)
|
gc.Set("userId", userID)
|
||||||
gc.Set("userMode", false)
|
gc.Set("userMode", false)
|
||||||
|
app.debug.Println("Auth succeeded")
|
||||||
gc.Next()
|
gc.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +160,7 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context, userpage bool)
|
|||||||
password = creds[1]
|
password = creds[1]
|
||||||
ok = false
|
ok = false
|
||||||
if username == "" || password == "" {
|
if username == "" || password == "" {
|
||||||
app.logIpDebug(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.EmptyUserOrPass))
|
app.logIpDebug(gc, userpage, "Auth denied: blank username/password")
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -169,16 +173,16 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
|
|||||||
user, status, err := app.authJf.Authenticate(username, password)
|
user, status, err := app.authJf.Authenticate(username, password)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
if status == 401 || status == 400 {
|
if status == 401 || status == 400 {
|
||||||
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
|
app.logIpInfo(gc, userpage, "Auth denied: Invalid username/password (Jellyfin)")
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if status == 403 {
|
if status == 403 {
|
||||||
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.UserDisabled))
|
app.logIpInfo(gc, userpage, "Auth denied: Jellyfin account disabled")
|
||||||
respond(403, "yourAccountWasDisabled", gc)
|
respond(403, "yourAccountWasDisabled", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.authLog(fmt.Sprintf(lm.FailedAuthJellyfin, app.jf.Server, status, err))
|
app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
|
||||||
respond(500, "Jellyfin error", gc)
|
respond(500, "Jellyfin error", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -195,7 +199,7 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
|
|||||||
// @tags Auth
|
// @tags Auth
|
||||||
// @Security getTokenAuth
|
// @Security getTokenAuth
|
||||||
func (app *appContext) getTokenLogin(gc *gin.Context) {
|
func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||||
app.logIpInfo(gc, false, fmt.Sprintf(lm.RequestingToken, lm.TokenLoginAttempt))
|
app.logIpInfo(gc, false, "Token requested (login attempt)")
|
||||||
username, password, ok := app.decodeValidateLoginHeader(gc, false)
|
username, password, ok := app.decodeValidateLoginHeader(gc, false)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@ -205,12 +209,13 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|||||||
for _, user := range app.adminUsers {
|
for _, user := range app.adminUsers {
|
||||||
if user.Username == username && user.Password == password {
|
if user.Username == username && user.Password == password {
|
||||||
match = true
|
match = true
|
||||||
|
app.debug.Println("Found existing user")
|
||||||
userID = user.UserID
|
userID = user.UserID
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !app.jellyfinLogin && !match {
|
if !app.jellyfinLogin && !match {
|
||||||
app.logIpInfo(gc, false, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
|
app.logIpInfo(gc, false, "Auth denied: Invalid username/password")
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -228,7 +233,7 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
|
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
|
||||||
if !accountsAdmin {
|
if !accountsAdmin {
|
||||||
app.authLog(fmt.Sprintf(lm.NonAdminUser, username))
|
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", username)
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -238,12 +243,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|||||||
newUser := User{
|
newUser := User{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
app.debug.Printf(lm.GenerateToken, username)
|
app.debug.Printf("Token generated for user \"%s\"", username)
|
||||||
app.adminUsers = append(app.adminUsers, newUser)
|
app.adminUsers = append(app.adminUsers, newUser)
|
||||||
}
|
}
|
||||||
token, refresh, err := CreateToken(userID, jfID, true)
|
token, refresh, err := CreateToken(userID, jfID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGenerateToken, err)
|
app.err.Printf("getToken failed: Couldn't generate token (%s)", err)
|
||||||
respond(500, "Couldn't generate token", gc)
|
respond(500, "Couldn't generate token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -256,29 +261,35 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
|
|||||||
ok = false
|
ok = false
|
||||||
cookie, err := gc.Cookie(cookieName)
|
cookie, err := gc.Cookie(cookieName)
|
||||||
if err != nil || cookie == "" {
|
if err != nil || cookie == "" {
|
||||||
app.authLog(fmt.Sprintf(lm.FailedGetCookies, cookieName, err))
|
app.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err)
|
||||||
respond(400, "Couldn't get token", gc)
|
respond(400, "Couldn't get token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, token := range app.invalidTokens {
|
for _, token := range app.invalidTokens {
|
||||||
if cookie == token {
|
if cookie == token {
|
||||||
app.authLog(lm.LocallyInvalidatedJWT)
|
app.debug.Println("getTokenRefresh: Invalid token")
|
||||||
respond(401, lm.InvalidJWT, gc)
|
respond(401, "Invalid token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
token, err := jwt.Parse(cookie, checkToken)
|
token, err := jwt.Parse(cookie, checkToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.authLog(lm.FailedParseJWT)
|
app.debug.Println("getTokenRefresh: Invalid token")
|
||||||
respond(400, lm.InvalidJWT, gc)
|
respond(400, "Invalid token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
claims, ok = token.Claims.(jwt.MapClaims)
|
claims, ok = token.Claims.(jwt.MapClaims)
|
||||||
expiryUnix := int64(claims["exp"].(float64))
|
expiryUnix := int64(claims["exp"].(float64))
|
||||||
|
if err != nil {
|
||||||
|
app.debug.Printf("getTokenRefresh: Invalid token expiry: %s", err)
|
||||||
|
respond(401, "Invalid token", gc)
|
||||||
|
ok = false
|
||||||
|
return
|
||||||
|
}
|
||||||
expiry := time.Unix(expiryUnix, 0)
|
expiry := time.Unix(expiryUnix, 0)
|
||||||
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
||||||
app.authLog(lm.InvalidJWT)
|
app.debug.Printf("getTokenRefresh: Invalid token: %+v", err)
|
||||||
respond(401, lm.InvalidJWT, gc)
|
respond(401, "Invalid token", gc)
|
||||||
ok = false
|
ok = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -293,7 +304,7 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
|
|||||||
// @Router /token/refresh [get]
|
// @Router /token/refresh [get]
|
||||||
// @tags Auth
|
// @tags Auth
|
||||||
func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||||
app.logIpInfo(gc, false, fmt.Sprintf(lm.RequestingToken, lm.TokenRefresh))
|
app.logIpInfo(gc, false, "Token requested (refresh token)")
|
||||||
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
|
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@ -302,7 +313,7 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
|||||||
jfID := claims["jfid"].(string)
|
jfID := claims["jfid"].(string)
|
||||||
jwt, refresh, err := CreateToken(userID, jfID, true)
|
jwt, refresh, err := CreateToken(userID, jfID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGenerateToken, err)
|
app.err.Printf("getTokenRefresh failed: Couldn't generate token (%s)", err)
|
||||||
respond(500, "Couldn't generate token", gc)
|
respond(500, "Couldn't generate token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
31
backups.go
31
backups.go
@ -7,8 +7,6 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -62,12 +60,12 @@ func (app *appContext) getBackups() *BackupList {
|
|||||||
path := app.config.Section("backups").Key("path").String()
|
path := app.config.Section("backups").Key("path").String()
|
||||||
err := os.MkdirAll(path, 0755)
|
err := os.MkdirAll(path, 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedCreateDir, path, err)
|
app.err.Printf("Failed to create backup directory \"%s\": %v\n", path, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
items, err := os.ReadDir(path)
|
items, err := os.ReadDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedReading, path, err)
|
app.err.Printf("Failed to read backup directory \"%s\": %v\n", path, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
backups := &BackupList{}
|
backups := &BackupList{}
|
||||||
@ -80,7 +78,7 @@ func (app *appContext) getBackups() *BackupList {
|
|||||||
}
|
}
|
||||||
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(item.Name(), BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX))
|
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(item.Name(), BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.debug.Printf(lm.FailedParseTime, err)
|
app.debug.Printf("Failed to parse backup filename \"%s\": %v\n", item.Name(), err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
backups.dates[i] = t
|
backups.dates[i] = t
|
||||||
@ -103,36 +101,36 @@ func (app *appContext) makeBackup() (fileDetails CreateBackupDTO) {
|
|||||||
sort.Sort(backups)
|
sort.Sort(backups)
|
||||||
for _, item := range backups.files[:toDelete] {
|
for _, item := range backups.files[:toDelete] {
|
||||||
fullpath := filepath.Join(path, item.Name())
|
fullpath := filepath.Join(path, item.Name())
|
||||||
|
app.debug.Printf("Deleting old backup \"%s\"\n", item.Name())
|
||||||
err := os.Remove(fullpath)
|
err := os.Remove(fullpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedDeleteOldBackup, fullpath, err)
|
app.err.Printf("Failed to delete old backup \"%s\": %v\n", fullpath, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.debug.Printf(lm.DeleteOldBackup, fullpath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fullpath := filepath.Join(path, fname)
|
fullpath := filepath.Join(path, fname)
|
||||||
f, err := os.Create(fullpath)
|
f, err := os.Create(fullpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedOpen, fullpath, err)
|
app.err.Printf("Failed to open backup file \"%s\": %v\n", fullpath, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
_, err = app.storage.db.Badger().Backup(f, 0)
|
_, err = app.storage.db.Badger().Backup(f, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedCreateBackup, err)
|
app.err.Printf("Failed to create backup: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fstat, err := f.Stat()
|
fstat, err := f.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedStat, fullpath, err)
|
app.err.Printf("Failed to get info on new backup: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileDetails.Size = fileSize(fstat.Size())
|
fileDetails.Size = fileSize(fstat.Size())
|
||||||
fileDetails.Name = fname
|
fileDetails.Name = fname
|
||||||
fileDetails.Path = fullpath
|
fileDetails.Path = fullpath
|
||||||
app.debug.Printf(lm.CreateBackup, fileDetails)
|
// fmt.Printf("Created backup %+v\n", fileDetails)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,25 +139,25 @@ func (app *appContext) loadPendingBackup() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
oldPath := filepath.Join(app.dataPath, "db-"+string(time.Now().Unix())+"-pre-"+filepath.Base(LOADBAK))
|
oldPath := filepath.Join(app.dataPath, "db-"+string(time.Now().Unix())+"-pre-"+filepath.Base(LOADBAK))
|
||||||
|
app.info.Printf("Moving existing database to \"%s\"\n", oldPath)
|
||||||
err := os.Rename(app.storage.db_path, oldPath)
|
err := os.Rename(app.storage.db_path, oldPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedMoveOldDB, oldPath, err)
|
app.err.Fatalf("Failed to move existing database: %v\n", err)
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.MoveOldDB, oldPath)
|
|
||||||
|
|
||||||
app.ConnectDB()
|
app.ConnectDB()
|
||||||
defer app.storage.db.Close()
|
defer app.storage.db.Close()
|
||||||
|
|
||||||
f, err := os.Open(LOADBAK)
|
f, err := os.Open(LOADBAK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedOpen, LOADBAK, err)
|
app.err.Fatalf("Failed to open backup file \"%s\": %v\n", LOADBAK, err)
|
||||||
}
|
}
|
||||||
err = app.storage.db.Badger().Load(f, 256)
|
err = app.storage.db.Badger().Load(f, 256)
|
||||||
f.Close()
|
f.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedRestoreDB, LOADBAK, err)
|
app.err.Fatalf("Failed to restore backup file \"%s\": %v\n", LOADBAK, err)
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.RestoreDB, LOADBAK)
|
app.info.Printf("Restored backup \"%s\".", LOADBAK)
|
||||||
LOADBAK = ""
|
LOADBAK = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,6 +165,7 @@ func newBackupDaemon(app *appContext) *GenericDaemon {
|
|||||||
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
|
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
|
||||||
d := NewGenericDaemon(interval, app,
|
d := NewGenericDaemon(interval, app,
|
||||||
func(app *appContext) {
|
func(app *appContext) {
|
||||||
|
app.debug.Println("Backups: Creating backup")
|
||||||
app.makeBackup()
|
app.makeBackup()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
14
config.go
14
config.go
@ -7,10 +7,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hrfee/jfa-go/easyproxy"
|
"github.com/hrfee/jfa-go/easyproxy"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -142,7 +140,7 @@ func (app *appContext) loadConfig() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allDisabled {
|
if allDisabled {
|
||||||
app.info.Println(lm.EnableAllPWRMethods)
|
fmt.Println("SETALLTRUE")
|
||||||
for _, v := range pwrMethods {
|
for _, v := range pwrMethods {
|
||||||
app.config.Section("user_page").Key(v).SetValue("true")
|
app.config.Section("user_page").Key(v).SetValue("true")
|
||||||
}
|
}
|
||||||
@ -177,15 +175,9 @@ func (app *appContext) loadConfig() error {
|
|||||||
app.proxyConfig.Password = app.config.Section("advanced").Key("proxy_password").MustString("")
|
app.proxyConfig.Password = app.config.Section("advanced").Key("proxy_password").MustString("")
|
||||||
app.proxyTransport, err = easyproxy.NewTransport(app.proxyConfig)
|
app.proxyTransport, err = easyproxy.NewTransport(app.proxyConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedInitProxy, app.proxyConfig.Addr, err)
|
app.err.Printf("Failed to initialize Proxy: %v\n", err)
|
||||||
// As explained in lm.FailedInitProxy, sleep here might grab the admin's attention,
|
|
||||||
// Since we don't crash on this failing.
|
|
||||||
time.Sleep(15 * time.Second)
|
|
||||||
app.proxyEnabled = false
|
|
||||||
} else {
|
|
||||||
app.proxyEnabled = true
|
|
||||||
app.info.Printf(lm.InitProxy, app.proxyConfig.Addr)
|
|
||||||
}
|
}
|
||||||
|
app.proxyEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
app.MustSetValue("updates", "enabled", "true")
|
app.MustSetValue("updates", "enabled", "true")
|
||||||
|
@ -1120,15 +1120,6 @@
|
|||||||
"value": "",
|
"value": "",
|
||||||
"description": "Add the selected role to a user when they sign up."
|
"description": "Add the selected role to a user when they sign up."
|
||||||
},
|
},
|
||||||
"disable_enable_role": {
|
|
||||||
"name": "Remove/add role on user enable/disable/deletion",
|
|
||||||
"required": false,
|
|
||||||
"requires_restart": true,
|
|
||||||
"depends_true": "apply_role",
|
|
||||||
"type": "bool",
|
|
||||||
"value": false,
|
|
||||||
"description": "When a user is disabled or deleted, remove the Discord role, and when re-enabled, add it back."
|
|
||||||
},
|
|
||||||
"language": {
|
"language": {
|
||||||
"name": "Language",
|
"name": "Language",
|
||||||
"required": false,
|
"required": false,
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dgraph-io/badger/v3"
|
"github.com/dgraph-io/badger/v3"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
)
|
)
|
||||||
@ -13,7 +12,7 @@ import (
|
|||||||
// meant to be called with other such housekeeping functions, so assumes
|
// meant to be called with other such housekeeping functions, so assumes
|
||||||
// the user cache is fresh.
|
// the user cache is fresh.
|
||||||
func (app *appContext) clearEmails() {
|
func (app *appContext) clearEmails() {
|
||||||
app.debug.Println(lm.HousekeepingEmail)
|
app.debug.Println("Housekeeping: removing unused email addresses")
|
||||||
emails := app.storage.GetEmails()
|
emails := app.storage.GetEmails()
|
||||||
for _, email := range emails {
|
for _, email := range emails {
|
||||||
_, _, err := app.jf.UserByID(email.JellyfinID, false)
|
_, _, err := app.jf.UserByID(email.JellyfinID, false)
|
||||||
@ -29,20 +28,15 @@ func (app *appContext) clearEmails() {
|
|||||||
|
|
||||||
// clearDiscord does the same as clearEmails, but for Discord Users.
|
// clearDiscord does the same as clearEmails, but for Discord Users.
|
||||||
func (app *appContext) clearDiscord() {
|
func (app *appContext) clearDiscord() {
|
||||||
app.debug.Println(lm.HousekeepingDiscord)
|
app.debug.Println("Housekeeping: removing unused Discord IDs")
|
||||||
discordUsers := app.storage.GetDiscord()
|
discordUsers := app.storage.GetDiscord()
|
||||||
for _, discordUser := range discordUsers {
|
for _, discordUser := range discordUsers {
|
||||||
user, _, err := app.jf.UserByID(discordUser.JellyfinID, false)
|
_, _, err := app.jf.UserByID(discordUser.JellyfinID, false)
|
||||||
// Make sure the user doesn't exist, and no other error has occured
|
// Make sure the user doesn't exist, and no other error has occured
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
case mediabrowser.ErrUserNotFound:
|
case mediabrowser.ErrUserNotFound:
|
||||||
// Remove role in case their account was deleted oustide of jfa-go
|
|
||||||
app.discord.RemoveRole(discordUser.MethodID().(string))
|
|
||||||
app.storage.DeleteDiscordKey(discordUser.JellyfinID)
|
app.storage.DeleteDiscordKey(discordUser.JellyfinID)
|
||||||
default:
|
default:
|
||||||
if user.Policy.IsDisabled {
|
|
||||||
app.discord.RemoveRole(discordUser.MethodID().(string))
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,7 +44,7 @@ func (app *appContext) clearDiscord() {
|
|||||||
|
|
||||||
// clearMatrix does the same as clearEmails, but for Matrix Users.
|
// clearMatrix does the same as clearEmails, but for Matrix Users.
|
||||||
func (app *appContext) clearMatrix() {
|
func (app *appContext) clearMatrix() {
|
||||||
app.debug.Println(lm.HousekeepingMatrix)
|
app.debug.Println("Housekeeping: removing unused Matrix IDs")
|
||||||
matrixUsers := app.storage.GetMatrix()
|
matrixUsers := app.storage.GetMatrix()
|
||||||
for _, matrixUser := range matrixUsers {
|
for _, matrixUser := range matrixUsers {
|
||||||
_, _, err := app.jf.UserByID(matrixUser.JellyfinID, false)
|
_, _, err := app.jf.UserByID(matrixUser.JellyfinID, false)
|
||||||
@ -66,7 +60,7 @@ func (app *appContext) clearMatrix() {
|
|||||||
|
|
||||||
// clearTelegram does the same as clearEmails, but for Telegram Users.
|
// clearTelegram does the same as clearEmails, but for Telegram Users.
|
||||||
func (app *appContext) clearTelegram() {
|
func (app *appContext) clearTelegram() {
|
||||||
app.debug.Println(lm.HousekeepingTelegram)
|
app.debug.Println("Housekeeping: removing unused Telegram IDs")
|
||||||
telegramUsers := app.storage.GetTelegram()
|
telegramUsers := app.storage.GetTelegram()
|
||||||
for _, telegramUser := range telegramUsers {
|
for _, telegramUser := range telegramUsers {
|
||||||
_, _, err := app.jf.UserByID(telegramUser.JellyfinID, false)
|
_, _, err := app.jf.UserByID(telegramUser.JellyfinID, false)
|
||||||
@ -81,7 +75,7 @@ func (app *appContext) clearTelegram() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) clearPWRCaptchas() {
|
func (app *appContext) clearPWRCaptchas() {
|
||||||
app.debug.Println(lm.HousekeepingCaptcha)
|
app.debug.Println("Housekeeping: Clearing old PWR Captchas")
|
||||||
captchas := map[string]Captcha{}
|
captchas := map[string]Captcha{}
|
||||||
for k, capt := range app.pwrCaptchas {
|
for k, capt := range app.pwrCaptchas {
|
||||||
if capt.Generated.Add(CAPTCHA_VALIDITY * time.Second).After(time.Now()) {
|
if capt.Generated.Add(CAPTCHA_VALIDITY * time.Second).After(time.Now()) {
|
||||||
@ -92,7 +86,7 @@ func (app *appContext) clearPWRCaptchas() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) clearActivities() {
|
func (app *appContext) clearActivities() {
|
||||||
app.debug.Println(lm.HousekeepingActivity)
|
app.debug.Println("Housekeeping: Cleaning up Activity log...")
|
||||||
keepCount := app.config.Section("activity_log").Key("keep_n_records").MustInt(1000)
|
keepCount := app.config.Section("activity_log").Key("keep_n_records").MustInt(1000)
|
||||||
maxAgeDays := app.config.Section("activity_log").Key("delete_after_days").MustInt(90)
|
maxAgeDays := app.config.Section("activity_log").Key("delete_after_days").MustInt(90)
|
||||||
minAge := time.Now().AddDate(0, 0, -maxAgeDays)
|
minAge := time.Now().AddDate(0, 0, -maxAgeDays)
|
||||||
@ -109,7 +103,7 @@ func (app *appContext) clearActivities() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == badger.ErrTxnTooBig {
|
if err == badger.ErrTxnTooBig {
|
||||||
app.debug.Printf(lm.ActivityLogTxnTooBig)
|
app.debug.Printf("Activities: Delete txn was too big, doing it manually.")
|
||||||
list := []Activity{}
|
list := []Activity{}
|
||||||
if errorSource == 0 {
|
if errorSource == 0 {
|
||||||
app.storage.db.Find(&list, badgerhold.Where("Time").Lt(minAge))
|
app.storage.db.Find(&list, badgerhold.Where("Time").Lt(minAge))
|
||||||
@ -125,7 +119,7 @@ func (app *appContext) clearActivities() {
|
|||||||
func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||||
d := NewGenericDaemon(interval, app,
|
d := NewGenericDaemon(interval, app,
|
||||||
func(app *appContext) {
|
func(app *appContext) {
|
||||||
app.debug.Println(lm.HousekeepingInvites)
|
app.debug.Println("Housekeeping: Checking for expired invites")
|
||||||
app.checkInvites()
|
app.checkInvites()
|
||||||
},
|
},
|
||||||
func(app *appContext) { app.clearActivities() },
|
func(app *appContext) { app.clearActivities() },
|
||||||
@ -134,7 +128,7 @@ func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaem
|
|||||||
d.Name("Housekeeping daemon")
|
d.Name("Housekeeping daemon")
|
||||||
|
|
||||||
clearEmail := app.config.Section("email").Key("require_unique").MustBool(false)
|
clearEmail := app.config.Section("email").Key("require_unique").MustBool(false)
|
||||||
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false) || app.config.Section("discord").Key("disable_enable_role").MustBool(false)
|
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
|
||||||
clearTelegram := app.config.Section("telegram").Key("require_unique").MustBool(false)
|
clearTelegram := app.config.Section("telegram").Key("require_unique").MustBool(false)
|
||||||
clearMatrix := app.config.Section("matrix").Key("require_unique").MustBool(false)
|
clearMatrix := app.config.Section("matrix").Key("require_unique").MustBool(false)
|
||||||
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
347
discord.go
347
discord.go
@ -6,26 +6,25 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
dg "github.com/bwmarrin/discordgo"
|
dg "github.com/bwmarrin/discordgo"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DiscordDaemon struct {
|
type DiscordDaemon struct {
|
||||||
Stopped bool
|
Stopped bool
|
||||||
ShutdownChannel chan string
|
ShutdownChannel chan string
|
||||||
bot *dg.Session
|
bot *dg.Session
|
||||||
username string
|
username string
|
||||||
tokens map[string]VerifToken // Map of pins to tokens.
|
tokens map[string]VerifToken // Map of pins to tokens.
|
||||||
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
|
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
|
||||||
Channel, InviteChannel struct{ ID, Name string }
|
channelID, channelName, inviteChannelID, inviteChannelName string
|
||||||
guildID string
|
guildID string
|
||||||
serverChannelName, serverName string
|
serverChannelName, serverName string
|
||||||
users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start.
|
users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start.
|
||||||
roleID string
|
roleID string
|
||||||
app *appContext
|
app *appContext
|
||||||
commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string)
|
commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string)
|
||||||
commandIDs []string
|
commandIDs []string
|
||||||
commandDescriptions []*dg.ApplicationCommand
|
commandDescriptions []*dg.ApplicationCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
||||||
@ -93,11 +92,13 @@ func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DiscordDaemon) run() {
|
func (d *DiscordDaemon) run() {
|
||||||
|
d.bot.AddHandler(d.messageHandler)
|
||||||
|
|
||||||
d.bot.AddHandler(d.commandHandler)
|
d.bot.AddHandler(d.commandHandler)
|
||||||
|
|
||||||
d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers | dg.IntentsGuildInvites
|
d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers | dg.IntentsGuildInvites
|
||||||
if err := d.bot.Open(); err != nil {
|
if err := d.bot.Open(); err != nil {
|
||||||
d.app.err.Printf(lm.FailedStartDaemon, lm.Discord, err)
|
d.app.err.Printf("Discord: Failed to start daemon: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Wait for everything to populate, it's slow sometimes.
|
// Wait for everything to populate, it's slow sometimes.
|
||||||
@ -115,17 +116,17 @@ func (d *DiscordDaemon) run() {
|
|||||||
d.guildID = d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID
|
d.guildID = d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID
|
||||||
guild, err := d.bot.Guild(d.guildID)
|
guild, err := d.bot.Guild(d.guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedGetDiscordGuild, err)
|
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
||||||
}
|
}
|
||||||
d.serverChannelName = guild.Name
|
d.serverChannelName = guild.Name
|
||||||
d.serverName = guild.Name
|
d.serverName = guild.Name
|
||||||
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
|
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
|
||||||
d.Channel.Name = channel
|
d.channelName = channel
|
||||||
d.serverChannelName += "/" + channel
|
d.serverChannelName += "/" + channel
|
||||||
}
|
}
|
||||||
if d.app.config.Section("discord").Key("provide_invite").MustBool(false) {
|
if d.app.config.Section("discord").Key("provide_invite").MustBool(false) {
|
||||||
if invChannel := d.app.config.Section("discord").Key("invite_channel").String(); invChannel != "" {
|
if invChannel := d.app.config.Section("discord").Key("invite_channel").String(); invChannel != "" {
|
||||||
d.InviteChannel.Name = invChannel
|
d.inviteChannelName = invChannel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = d.bot.UpdateGameStatus(0, "/"+d.app.config.Section("discord").Key("start_command").MustString("start"))
|
err = d.bot.UpdateGameStatus(0, "/"+d.app.config.Section("discord").Key("start_command").MustString("start"))
|
||||||
@ -144,7 +145,7 @@ func (d *DiscordDaemon) ListRoles() (roles [][2]string, err error) {
|
|||||||
var r []*dg.Role
|
var r []*dg.Role
|
||||||
r, err = d.bot.GuildRoles(d.guildID)
|
r, err = d.bot.GuildRoles(d.guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedGetDiscordRoles, err)
|
d.app.err.Printf("Discord: Failed to get roles: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, role := range r {
|
for _, role := range r {
|
||||||
@ -167,62 +168,44 @@ func (d *DiscordDaemon) ApplyRole(userID string) error {
|
|||||||
return d.bot.GuildMemberRoleAdd(d.guildID, userID, d.roleID)
|
return d.bot.GuildMemberRoleAdd(d.guildID, userID, d.roleID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveRole removes the member role to the given user if set.
|
|
||||||
func (d *DiscordDaemon) RemoveRole(userID string) error {
|
|
||||||
if d.roleID == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return d.bot.GuildMemberRoleRemove(d.guildID, userID, d.roleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRoleDisabled removes the role if "disabled", and applies if "!disabled".
|
|
||||||
func (d *DiscordDaemon) SetRoleDisabled(userID string, disabled bool) (err error) {
|
|
||||||
if disabled {
|
|
||||||
err = d.RemoveRole(userID)
|
|
||||||
} else {
|
|
||||||
err = d.ApplyRole(userID)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTempInvite creates an invite link, and returns the invite URL, as well as the URL for the server icon.
|
// NewTempInvite creates an invite link, and returns the invite URL, as well as the URL for the server icon.
|
||||||
func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconURL string) {
|
func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconURL string) {
|
||||||
var inv *dg.Invite
|
var inv *dg.Invite
|
||||||
var err error
|
var err error
|
||||||
if d.InviteChannel.Name == "" {
|
if d.inviteChannelName == "" {
|
||||||
d.app.err.Println(lm.FailedCreateDiscordInviteChannel, lm.InviteChannelEmpty)
|
d.app.err.Println("Discord: Cannot create invite without channel specified in settings.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if d.InviteChannel.ID == "" {
|
if d.inviteChannelID == "" {
|
||||||
channels, err := d.bot.GuildChannels(d.guildID)
|
channels, err := d.bot.GuildChannels(d.guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedGetDiscordChannels, err)
|
d.app.err.Printf("Discord: Couldn't get channel list: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
found := false
|
found := false
|
||||||
for _, channel := range channels {
|
for _, channel := range channels {
|
||||||
// channel, err := d.bot.Channel(ch.ID)
|
// channel, err := d.bot.Channel(ch.ID)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
// d.app.err.Printf(lm.FailedGetDiscordChannel, ch.ID, err)
|
// d.app.err.Printf("Discord: Couldn't get channel: %v", err)
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
if channel.Name == d.InviteChannel.Name {
|
if channel.Name == d.inviteChannelName {
|
||||||
d.InviteChannel.ID = channel.ID
|
d.inviteChannelID = channel.ID
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
d.app.err.Printf(lm.FailedGetDiscordChannel, d.InviteChannel.Name, lm.NotFound)
|
d.app.err.Printf("Discord: Couldn't find invite channel \"%s\"", d.inviteChannelName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// channel, err := d.bot.Channel(d.inviteChannelID)
|
// channel, err := d.bot.Channel(d.inviteChannelID)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
// d.app.err.Printf(lm.FailedGetDiscordChannel, d.inviteChannelID, err)
|
// d.app.err.Printf("Discord: Couldn't get invite channel: %v", err)
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
inv, err = d.bot.ChannelInviteCreate(d.InviteChannel.ID, dg.Invite{
|
inv, err = d.bot.ChannelInviteCreate(d.inviteChannelID, dg.Invite{
|
||||||
// Guild: d.bot.State.Guilds[len(d.bot.State.Guilds)-1],
|
// Guild: d.bot.State.Guilds[len(d.bot.State.Guilds)-1],
|
||||||
// Channel: channel,
|
// Channel: channel,
|
||||||
// Inviter: d.bot.State.User,
|
// Inviter: d.bot.State.User,
|
||||||
@ -231,13 +214,13 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
|
|||||||
Temporary: false,
|
Temporary: false,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedGenerateDiscordInvite, err)
|
d.app.err.Printf("Discord: Failed to create invite: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
inviteURL = "https://discord.gg/" + inv.Code
|
inviteURL = "https://discord.gg/" + inv.Code
|
||||||
guild, err := d.bot.Guild(d.guildID)
|
guild, err := d.bot.Guild(d.guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedGetDiscordGuild, err)
|
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
iconURL = guild.IconURL("256")
|
iconURL = guild.IconURL("256")
|
||||||
@ -272,7 +255,7 @@ func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
|
|||||||
1000,
|
1000,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedGetDiscordGuildMembers, err)
|
d.app.err.Printf("Discord: Failed to get members: %v", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
hasDiscriminator := strings.Contains(username, "#")
|
hasDiscriminator := strings.Contains(username, "#")
|
||||||
@ -302,7 +285,7 @@ func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
|
|||||||
func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
|
func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
|
||||||
u, err := d.bot.User(ID)
|
u, err := d.bot.User(ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedGetUser, ID, lm.Discord, err)
|
d.app.err.Printf("Discord: Failed to get user: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.ID = ID
|
user.ID = ID
|
||||||
@ -311,7 +294,7 @@ func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
|
|||||||
user.Discriminator = u.Discriminator
|
user.Discriminator = u.Discriminator
|
||||||
channel, err := d.bot.UserChannelCreate(ID)
|
channel, err := d.bot.UserChannelCreate(ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, ID, err)
|
d.app.err.Printf("Discord: Failed to create DM channel: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.ChannelID = channel.ID
|
user.ChannelID = channel.ID
|
||||||
@ -398,7 +381,7 @@ func (d *DiscordDaemon) registerCommands() {
|
|||||||
d.commandDescriptions[1].Options[0].Choices = make([]*dg.ApplicationCommandOptionChoice, len(d.app.storage.lang.Telegram))
|
d.commandDescriptions[1].Options[0].Choices = make([]*dg.ApplicationCommandOptionChoice, len(d.app.storage.lang.Telegram))
|
||||||
i := 0
|
i := 0
|
||||||
for code := range d.app.storage.lang.Telegram {
|
for code := range d.app.storage.lang.Telegram {
|
||||||
d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Lang, d.app.storage.lang.Telegram[code].Meta.Name+":"+code)
|
d.app.debug.Printf("Discord: registering lang choice \"%s\":\"%s\"\n", d.app.storage.lang.Telegram[code].Meta.Name, code)
|
||||||
d.commandDescriptions[1].Options[0].Choices[i] = &dg.ApplicationCommandOptionChoice{
|
d.commandDescriptions[1].Options[0].Choices[i] = &dg.ApplicationCommandOptionChoice{
|
||||||
Name: d.app.storage.lang.Telegram[code].Meta.Name,
|
Name: d.app.storage.lang.Telegram[code].Meta.Name,
|
||||||
Value: code,
|
Value: code,
|
||||||
@ -409,7 +392,7 @@ func (d *DiscordDaemon) registerCommands() {
|
|||||||
profiles := d.app.storage.GetProfiles()
|
profiles := d.app.storage.GetProfiles()
|
||||||
d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(profiles))
|
d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(profiles))
|
||||||
for i, profile := range profiles {
|
for i, profile := range profiles {
|
||||||
d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Profile, profile.Name)
|
d.app.debug.Printf("Discord: registering profile choice \"%s\"", profile.Name)
|
||||||
d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{
|
d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{
|
||||||
Name: profile.Name,
|
Name: profile.Name,
|
||||||
Value: profile.Name,
|
Value: profile.Name,
|
||||||
@ -426,9 +409,9 @@ func (d *DiscordDaemon) registerCommands() {
|
|||||||
for i, cmd := range d.commandDescriptions {
|
for i, cmd := range d.commandDescriptions {
|
||||||
command, err := d.bot.ApplicationCommandCreate(d.bot.State.User.ID, d.guildID, cmd)
|
command, err := d.bot.ApplicationCommandCreate(d.bot.State.User.ID, d.guildID, cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedRegisterDiscordCommand, cmd.Name, err)
|
d.app.err.Printf("Discord: Cannot create command \"%s\": %v", cmd.Name, err)
|
||||||
} else {
|
} else {
|
||||||
d.app.debug.Printf(lm.RegisterDiscordCommand, cmd.Name)
|
d.app.debug.Printf("Discord: registered command \"%s\"", cmd.Name)
|
||||||
d.commandIDs[i] = command.ID
|
d.commandIDs[i] = command.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -437,12 +420,12 @@ func (d *DiscordDaemon) registerCommands() {
|
|||||||
func (d *DiscordDaemon) deregisterCommands() {
|
func (d *DiscordDaemon) deregisterCommands() {
|
||||||
existingCommands, err := d.bot.ApplicationCommands(d.bot.State.User.ID, d.guildID)
|
existingCommands, err := d.bot.ApplicationCommands(d.bot.State.User.ID, d.guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedGetDiscordCommands, err)
|
d.app.err.Printf("Discord: Failed to get commands: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, cmd := range existingCommands {
|
for _, cmd := range existingCommands {
|
||||||
if err := d.bot.ApplicationCommandDelete(d.bot.State.User.ID, d.guildID, cmd.ID); err != nil {
|
if err := d.bot.ApplicationCommandDelete(d.bot.State.User.ID, d.guildID, cmd.ID); err != nil {
|
||||||
d.app.err.Printf(lm.FailedDeregDiscordCommand, cmd.Name, err)
|
d.app.err.Printf("Discord: Failed to deregister command: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -453,7 +436,7 @@ func (d *DiscordDaemon) UpdateCommands() {
|
|||||||
profiles := d.app.storage.GetProfiles()
|
profiles := d.app.storage.GetProfiles()
|
||||||
d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(profiles))
|
d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(profiles))
|
||||||
for i, profile := range profiles {
|
for i, profile := range profiles {
|
||||||
d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Profile, profile.Name)
|
d.app.debug.Printf("Discord: registering profile choice \"%s\"", profile.Name)
|
||||||
d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{
|
d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{
|
||||||
Name: profile.Name,
|
Name: profile.Name,
|
||||||
Value: profile.Name,
|
Value: profile.Name,
|
||||||
@ -461,7 +444,7 @@ func (d *DiscordDaemon) UpdateCommands() {
|
|||||||
}
|
}
|
||||||
cmd, err := d.bot.ApplicationCommandEdit(d.bot.State.User.ID, d.guildID, d.commandIDs[3], d.commandDescriptions[3])
|
cmd, err := d.bot.ApplicationCommandEdit(d.bot.State.User.ID, d.guildID, d.commandIDs[3], d.commandDescriptions[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedRegisterDiscordChoices, lm.Profile, err)
|
d.app.err.Printf("Discord: Failed to update profile list: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
d.commandIDs[3] = cmd.ID
|
d.commandIDs[3] = cmd.ID
|
||||||
}
|
}
|
||||||
@ -469,20 +452,19 @@ func (d *DiscordDaemon) UpdateCommands() {
|
|||||||
|
|
||||||
func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
|
func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
|
||||||
if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok {
|
if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok {
|
||||||
if i.GuildID != "" && d.Channel.Name != "" {
|
if i.GuildID != "" && d.channelName != "" {
|
||||||
if d.Channel.ID == "" {
|
if d.channelID == "" {
|
||||||
channel, err := s.Channel(i.ChannelID)
|
channel, err := s.Channel(i.ChannelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedGetDiscordChannel, i.ChannelID, err)
|
d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
|
||||||
d.app.err.Println(lm.MonitorAllDiscordChannels)
|
d.channelName = ""
|
||||||
d.Channel.Name = ""
|
|
||||||
}
|
}
|
||||||
if channel.Name == d.Channel.Name {
|
if channel.Name == d.channelName {
|
||||||
d.Channel.ID = channel.ID
|
d.channelID = channel.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if d.Channel.ID != i.ChannelID {
|
if d.channelID != i.ChannelID {
|
||||||
d.app.debug.Printf(lm.IgnoreOutOfChannelMessage, lm.Discord)
|
d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -504,7 +486,7 @@ func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
|
|||||||
func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
||||||
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
|
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, i.Interaction.Member.User.ID, err)
|
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username)
|
user := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username)
|
||||||
@ -521,7 +503,7 @@ func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang st
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
|
d.app.err.Printf("Discord: Failed to send reply: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -539,7 +521,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||||
}
|
}
|
||||||
delete(d.tokens, pin)
|
delete(d.tokens, pin)
|
||||||
return
|
return
|
||||||
@ -553,7 +535,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||||
}
|
}
|
||||||
dcUser := d.users[i.Interaction.Member.User.ID]
|
dcUser := d.users[i.Interaction.Member.User.ID]
|
||||||
dcUser.JellyfinID = user.JellyfinID
|
dcUser.JellyfinID = user.JellyfinID
|
||||||
@ -584,7 +566,7 @@ func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang str
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
|
d.app.err.Printf("Discord: Failed to send reply: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -593,7 +575,7 @@ func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang str
|
|||||||
func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
||||||
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
|
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, i.Interaction.Member.User.ID, err)
|
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requester := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username)
|
requester := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username)
|
||||||
@ -608,9 +590,12 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
|||||||
//}
|
//}
|
||||||
// Check whether requestor is linked to the admin account
|
// Check whether requestor is linked to the admin account
|
||||||
requesterEmail, ok := d.app.storage.GetEmailsKey(requester.JellyfinID)
|
requesterEmail, ok := d.app.storage.GetEmailsKey(requester.JellyfinID)
|
||||||
if !(ok && requesterEmail.Admin) {
|
if !ok {
|
||||||
d.app.err.Printf(lm.FailedGenerateInvite, fmt.Sprintf(lm.NonAdminUser, requester.JellyfinID))
|
d.app.err.Printf("Failed to verify admin")
|
||||||
// FIXME: add response message
|
}
|
||||||
|
if !requesterEmail.Admin {
|
||||||
|
d.app.err.Printf("User is not admin")
|
||||||
|
//add response message
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -644,7 +629,7 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
|||||||
ValidTill: validTill,
|
ValidTill: validTill,
|
||||||
UserLabel: userLabel,
|
UserLabel: userLabel,
|
||||||
Profile: "Default",
|
Profile: "Default",
|
||||||
Label: fmt.Sprintf("%s: %s", lm.Discord, RenderDiscordUsername(recipient)),
|
Label: fmt.Sprintf("Discord: %s", RenderDiscordUsername(recipient)),
|
||||||
}
|
}
|
||||||
if profileName != "" {
|
if profileName != "" {
|
||||||
if _, ok := d.app.storage.GetProfileKey(profileName); ok {
|
if _, ok := d.app.storage.GetProfileKey(profileName); ok {
|
||||||
@ -653,12 +638,13 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if recipient != nil && d.app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
if recipient != nil && d.app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||||
|
d.app.debug.Printf("%s: Sending invite message", invite.Code)
|
||||||
invname, err := d.bot.GuildMember(d.guildID, recipient.ID)
|
invname, err := d.bot.GuildMember(d.guildID, recipient.ID)
|
||||||
invite.SendTo = invname.User.Username
|
invite.SendTo = invname.User.Username
|
||||||
msg, err := d.app.email.constructInvite(invite.Code, invite, d.app, false)
|
msg, err := d.app.email.constructInvite(invite.Code, invite, d.app, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, invite.Code, err)
|
invite.SendTo = fmt.Sprintf("Failed to send to %s", RenderDiscordUsername(recipient))
|
||||||
d.app.err.Println(invite.SendTo)
|
d.app.err.Printf("%s: Failed to construct invite message: %v", invite.Code, err)
|
||||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||||
Data: &dg.InteractionResponseData{
|
Data: &dg.InteractionResponseData{
|
||||||
@ -667,14 +653,14 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err)
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
err = d.app.discord.SendDM(msg, recipient.ID)
|
err = d.app.discord.SendDM(msg, recipient.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, RenderDiscordUsername(recipient), err)
|
invite.SendTo = fmt.Sprintf("Failed to send to %s", RenderDiscordUsername(recipient))
|
||||||
d.app.err.Println(invite.SendTo)
|
d.app.err.Printf("%s: %s: %v", invite.Code, invite.SendTo, err)
|
||||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||||
Data: &dg.InteractionResponseData{
|
Data: &dg.InteractionResponseData{
|
||||||
@ -683,10 +669,10 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err)
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
d.app.info.Printf(lm.SentInviteMessage, invite.Code, RenderDiscordUsername(recipient))
|
d.app.info.Printf("%s: Sent invite email to \"%s\"", invite.Code, RenderDiscordUsername(recipient))
|
||||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||||
Data: &dg.InteractionResponseData{
|
Data: &dg.InteractionResponseData{
|
||||||
@ -695,7 +681,7 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err)
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -704,6 +690,140 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
|||||||
d.app.storage.SetInvitesKey(invite.Code, invite)
|
d.app.storage.SetInvitesKey(invite.Code, invite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
|
||||||
|
if m.GuildID != "" && d.channelName != "" {
|
||||||
|
if d.channelID == "" {
|
||||||
|
channel, err := s.Channel(m.ChannelID)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
|
||||||
|
d.channelName = ""
|
||||||
|
}
|
||||||
|
if channel.Name == d.channelName {
|
||||||
|
d.channelID = channel.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.channelID != m.ChannelID {
|
||||||
|
d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.Author.ID == s.State.User.ID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sects := strings.Split(m.Content, " ")
|
||||||
|
if len(sects) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lang := d.app.storage.lang.chosenTelegramLang
|
||||||
|
if user, ok := d.users[m.Author.ID]; ok {
|
||||||
|
if _, ok := d.app.storage.lang.Telegram[user.Lang]; ok {
|
||||||
|
lang = user.Lang
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch msg := sects[0]; msg {
|
||||||
|
case "!" + d.app.config.Section("discord").Key("start_command").MustString("start"):
|
||||||
|
d.msgStart(s, m, lang)
|
||||||
|
case "!lang":
|
||||||
|
d.msgLang(s, m, sects, lang)
|
||||||
|
default:
|
||||||
|
d.msgPIN(s, m, sects, lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) msgStart(s *dg.Session, m *dg.MessageCreate, lang string) {
|
||||||
|
channel, err := s.UserChannelCreate(m.Author.ID)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", m.Author.Username, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := d.MustGetUser(channel.ID, m.Author.ID, m.Author.Discriminator, m.Author.Username)
|
||||||
|
d.users[m.Author.ID] = user
|
||||||
|
|
||||||
|
_, err = d.bot.ChannelMessageSendReply(m.ChannelID, d.app.storage.lang.Telegram[lang].Strings.get("discordDMs"), m.Reference())
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to send reply to \"%s\": %v", m.Author.Username, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := d.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
||||||
|
content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"})
|
||||||
|
_, err = s.ChannelMessageSend(channel.ID, content)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) msgLang(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
|
||||||
|
if len(sects) == 1 {
|
||||||
|
list := "!lang <lang>\n"
|
||||||
|
for code := range d.app.storage.lang.Telegram {
|
||||||
|
list += fmt.Sprintf("%s: %s\n", code, d.app.storage.lang.Telegram[code].Meta.Name)
|
||||||
|
}
|
||||||
|
_, err := s.ChannelMessageSendReply(
|
||||||
|
m.ChannelID,
|
||||||
|
list,
|
||||||
|
m.Reference(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
|
||||||
|
var user DiscordUser
|
||||||
|
for _, u := range d.app.storage.GetDiscord() {
|
||||||
|
if u.ID == m.Author.ID {
|
||||||
|
u.Lang = sects[1]
|
||||||
|
d.app.storage.SetDiscordKey(u.JellyfinID, u)
|
||||||
|
user = u
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.users[m.Author.ID] = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) msgPIN(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
|
||||||
|
if _, ok := d.users[m.Author.ID]; ok {
|
||||||
|
channel, err := s.Channel(m.ChannelID)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to get channel: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if channel.Type != dg.ChannelTypeDM {
|
||||||
|
d.app.debug.Println("Discord: Ignoring message as not a DM")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
d.app.debug.Println("Discord: Ignoring message as user was not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, ok := d.tokens[sects[0]]
|
||||||
|
if !ok || time.Now().After(user.Expiry) {
|
||||||
|
_, err := s.ChannelMessageSend(
|
||||||
|
m.ChannelID,
|
||||||
|
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
|
}
|
||||||
|
delete(d.tokens, sects[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err := s.ChannelMessageSend(
|
||||||
|
m.ChannelID,
|
||||||
|
d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||||
|
}
|
||||||
|
dcUser := d.users[m.Author.ID]
|
||||||
|
dcUser.JellyfinID = user.JellyfinID
|
||||||
|
d.verifiedTokens[sects[0]] = dcUser
|
||||||
|
delete(d.tokens, sects[0])
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error {
|
func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error {
|
||||||
channels := make([]string, len(userID))
|
channels := make([]string, len(userID))
|
||||||
for i, id := range userID {
|
for i, id := range userID {
|
||||||
@ -757,10 +877,10 @@ func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserVerified returns whether or not a token with the given PIN has been verified, and the user itself.
|
// UserVerified returns whether or not a token with the given PIN has been verified, and the user itself.
|
||||||
func (d *DiscordDaemon) UserVerified(pin string) (ContactMethodUser, bool) {
|
func (d *DiscordDaemon) UserVerified(pin string) (user DiscordUser, ok bool) {
|
||||||
u, ok := d.verifiedTokens[pin]
|
user, ok = d.verifiedTokens[pin]
|
||||||
// delete(d.verifiedTokens, pin)
|
// delete(d.verifiedTokens, pin)
|
||||||
return &u, ok
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssignedUserVerified returns whether or not a user with the given PIN has been verified, and the token itself.
|
// AssignedUserVerified returns whether or not a user with the given PIN has been verified, and the token itself.
|
||||||
@ -780,44 +900,7 @@ func (d *DiscordDaemon) UserExists(id string) bool {
|
|||||||
return err != nil || c > 0
|
return err != nil || c > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exists returns whether or not the given user exists.
|
// DeleteVerifiedUser removes the token with the given PIN.
|
||||||
func (d *DiscordDaemon) Exists(user ContactMethodUser) bool {
|
func (d *DiscordDaemon) DeleteVerifiedUser(pin string) {
|
||||||
return d.UserExists(user.MethodID().(string))
|
delete(d.verifiedTokens, pin)
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteVerifiedToken removes the token with the given PIN.
|
|
||||||
func (d *DiscordDaemon) DeleteVerifiedToken(PIN string) {
|
|
||||||
delete(d.verifiedTokens, PIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DiscordDaemon) PIN(req newUserDTO) string { return req.DiscordPIN }
|
|
||||||
|
|
||||||
func (d *DiscordDaemon) Name() string { return lm.Discord }
|
|
||||||
|
|
||||||
func (d *DiscordDaemon) Required() bool {
|
|
||||||
return d.app.config.Section("discord").Key("required").MustBool(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DiscordDaemon) UniqueRequired() bool {
|
|
||||||
return d.app.config.Section("discord").Key("require_unique").MustBool(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DiscordDaemon) PostVerificationTasks(PIN string, u ContactMethodUser) error {
|
|
||||||
err := d.ApplyRole(u.MethodID().(string))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(lm.FailedSetDiscordMemberRole, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DiscordUser) Name() string { return RenderDiscordUsername(*d) }
|
|
||||||
func (d *DiscordUser) SetMethodID(id any) { d.ID = id.(string) }
|
|
||||||
func (d *DiscordUser) MethodID() any { return d.ID }
|
|
||||||
func (d *DiscordUser) SetJellyfin(id string) { d.JellyfinID = id }
|
|
||||||
func (d *DiscordUser) Jellyfin() string { return d.JellyfinID }
|
|
||||||
func (d *DiscordUser) SetAllowContactFromDTO(req newUserDTO) { d.Contact = req.DiscordContact }
|
|
||||||
func (d *DiscordUser) SetAllowContact(contact bool) { d.Contact = contact }
|
|
||||||
func (d *DiscordUser) AllowContact() bool { return d.Contact }
|
|
||||||
func (d *DiscordUser) Store(st *Storage) {
|
|
||||||
st.SetDiscordKey(d.Jellyfin(), *d)
|
|
||||||
}
|
}
|
||||||
|
5
email.go
5
email.go
@ -20,7 +20,6 @@ import (
|
|||||||
"github.com/gomarkdown/markdown"
|
"github.com/gomarkdown/markdown"
|
||||||
"github.com/gomarkdown/markdown/html"
|
"github.com/gomarkdown/markdown/html"
|
||||||
"github.com/hrfee/jfa-go/easyproxy"
|
"github.com/hrfee/jfa-go/easyproxy"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/itchyny/timefmt-go"
|
"github.com/itchyny/timefmt-go"
|
||||||
"github.com/mailgun/mailgun-go/v4"
|
"github.com/mailgun/mailgun-go/v4"
|
||||||
@ -96,7 +95,7 @@ func NewEmailer(app *appContext) *Emailer {
|
|||||||
authType := sMail.AuthType(app.config.Section("smtp").Key("auth_type").MustInt(4))
|
authType := sMail.AuthType(app.config.Section("smtp").Key("auth_type").MustInt(4))
|
||||||
err := emailer.NewSMTP(app.config.Section("smtp").Key("server").String(), app.config.Section("smtp").Key("port").MustInt(465), username, password, sslTLS, app.config.Section("smtp").Key("ssl_cert").MustString(""), app.config.Section("smtp").Key("hello_hostname").String(), app.config.Section("smtp").Key("cert_validation").MustBool(true), authType, proxyConf)
|
err := emailer.NewSMTP(app.config.Section("smtp").Key("server").String(), app.config.Section("smtp").Key("port").MustInt(465), username, password, sslTLS, app.config.Section("smtp").Key("ssl_cert").MustString(""), app.config.Section("smtp").Key("hello_hostname").String(), app.config.Section("smtp").Key("cert_validation").MustBool(true), authType, proxyConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedInitSMTP, err)
|
app.err.Printf("Error while initiating SMTP mailer: %v", err)
|
||||||
}
|
}
|
||||||
} else if method == "mailgun" {
|
} else if method == "mailgun" {
|
||||||
emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String())
|
emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String())
|
||||||
@ -581,7 +580,7 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo
|
|||||||
// Only used in html email.
|
// Only used in html email.
|
||||||
template["pin_code"] = pwr.Pin
|
template["pin_code"] = pwr.Pin
|
||||||
} else {
|
} else {
|
||||||
app.info.Println(lm.FailedGeneratePWRLink, err)
|
app.info.Println("Couldn't generate PWR link: %v", err)
|
||||||
template["pin"] = pwr.Pin
|
template["pin"] = pwr.Pin
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "time"
|
||||||
"time"
|
|
||||||
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
||||||
|
|
||||||
@ -40,7 +36,7 @@ func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app
|
|||||||
func (d *GenericDaemon) Name(name string) { d.name = name }
|
func (d *GenericDaemon) Name(name string) { d.name = name }
|
||||||
|
|
||||||
func (d *GenericDaemon) run() {
|
func (d *GenericDaemon) run() {
|
||||||
d.app.info.Printf(lm.StartDaemon, d.name)
|
d.app.info.Printf("%s started", d.name)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-d.ShutdownChannel:
|
case <-d.ShutdownChannel:
|
7
go.mod
7
go.mod
@ -1,6 +1,6 @@
|
|||||||
module github.com/hrfee/jfa-go
|
module github.com/hrfee/jfa-go
|
||||||
|
|
||||||
go 1.22.4
|
go 1.20
|
||||||
|
|
||||||
replace github.com/hrfee/jfa-go/docs => ./docs
|
replace github.com/hrfee/jfa-go/docs => ./docs
|
||||||
|
|
||||||
@ -10,8 +10,6 @@ replace github.com/hrfee/jfa-go/ombi => ./ombi
|
|||||||
|
|
||||||
replace github.com/hrfee/jfa-go/logger => ./logger
|
replace github.com/hrfee/jfa-go/logger => ./logger
|
||||||
|
|
||||||
replace github.com/hrfee/jfa-go/logmessages => ./logmessages
|
|
||||||
|
|
||||||
replace github.com/hrfee/jfa-go/linecache => ./linecache
|
replace github.com/hrfee/jfa-go/linecache => ./linecache
|
||||||
|
|
||||||
replace github.com/hrfee/jfa-go/api => ./api
|
replace github.com/hrfee/jfa-go/api => ./api
|
||||||
@ -37,7 +35,7 @@ require (
|
|||||||
github.com/hrfee/jfa-go/docs v0.0.0-20230626224816-f72960635dc3
|
github.com/hrfee/jfa-go/docs v0.0.0-20230626224816-f72960635dc3
|
||||||
github.com/hrfee/jfa-go/easyproxy v0.0.0-00010101000000-000000000000
|
github.com/hrfee/jfa-go/easyproxy v0.0.0-00010101000000-000000000000
|
||||||
github.com/hrfee/jfa-go/linecache v0.0.0-20230626224816-f72960635dc3
|
github.com/hrfee/jfa-go/linecache v0.0.0-20230626224816-f72960635dc3
|
||||||
github.com/hrfee/jfa-go/logger v0.0.0-20240731152135-2d066ea7cd32
|
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/jfa-go/ombi v0.0.0-20230626224816-f72960635dc3
|
||||||
github.com/hrfee/mediabrowser v0.3.13
|
github.com/hrfee/mediabrowser v0.3.13
|
||||||
github.com/itchyny/timefmt-go v0.1.5
|
github.com/itchyny/timefmt-go v0.1.5
|
||||||
@ -93,7 +91,6 @@ require (
|
|||||||
github.com/gorilla/mux v1.8.0 // indirect
|
github.com/gorilla/mux v1.8.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
github.com/hrfee/jfa-go/jellyseerr v0.0.0-00010101000000-000000000000 // indirect
|
github.com/hrfee/jfa-go/jellyseerr v0.0.0-00010101000000-000000000000 // indirect
|
||||||
github.com/hrfee/jfa-go/logmessages v0.0.0-00010101000000-000000000000 // indirect
|
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.16.6 // indirect
|
github.com/klauspost/compress v1.16.6 // indirect
|
||||||
|
@ -246,7 +246,6 @@
|
|||||||
<div class="card ~neutral @low mb-2 unfocused">
|
<div class="card ~neutral @low mb-2 unfocused">
|
||||||
<span class="heading">{{ .lang.Ombi.title }}</span>
|
<span class="heading">{{ .lang.Ombi.title }}</span>
|
||||||
<p class="content my-2">{{ .lang.Ombi.description }}</p>
|
<p class="content my-2">{{ .lang.Ombi.description }}</p>
|
||||||
<aside class="aside ~warning my-2" id="ombi-stability-warning">{{ .lang.Ombi.stabilityWarning }}</aside>
|
|
||||||
<label class="row switch pb-4">
|
<label class="row switch pb-4">
|
||||||
<input type="checkbox" class="mr-2" id="ombi-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
<input type="checkbox" class="mr-2" id="ombi-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
||||||
</label>
|
</label>
|
||||||
@ -259,23 +258,6 @@
|
|||||||
<input type="text" class="input ~neutral @low mt-4" id="ombi-api_key">
|
<input type="text" class="input ~neutral @low mt-4" id="ombi-api_key">
|
||||||
<p class="support mb-2 mt-1">{{ .lang.Ombi.apiKeyNotice }}</p>
|
<p class="support mb-2 mt-1">{{ .lang.Ombi.apiKeyNotice }}</p>
|
||||||
</label>
|
</label>
|
||||||
<span class="heading">{{ .lang.Jellyseerr.title }}</span>
|
|
||||||
<p class="content my-2">{{ .lang.Jellyseerr.description }}</p>
|
|
||||||
<label class="row switch pb-4">
|
|
||||||
<input type="checkbox" class="mr-2" id="jellyseerr-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
|
||||||
</label>
|
|
||||||
<label class="label">
|
|
||||||
<span class="mt-4">{{ .lang.Strings.serverAddress }}</span>
|
|
||||||
<input type="url" class="input ~neutral @low mt-4 mb-2" id="jellyseerr-server" placeholder="https://jellyseerr.jellyf.in:5055">
|
|
||||||
</label>
|
|
||||||
<label class="label">
|
|
||||||
<span class="mt-4">{{ .lang.Strings.apiKey }}</span>
|
|
||||||
<input type="text" class="input ~neutral @low mt-4" id="jellyseerr-api_key">
|
|
||||||
</label>
|
|
||||||
<label class="row switch pb-4">
|
|
||||||
<input type="checkbox" class="mr-2" id="jellyseerr-import_existing"><span>{{ .lang.Jellyseerr.importExisting }}</span>
|
|
||||||
<p class="support mb-2 mt-1">{{ .lang.Jellyseerr.importExistingDescription }}</p>
|
|
||||||
</label>
|
|
||||||
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
<section class="section ~neutral banner footer flex flex-row justify-between middle">
|
||||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||||
<div>
|
<div>
|
||||||
@ -441,7 +423,6 @@
|
|||||||
<div id="password-resets" class="card ~neutral @low mb-2 unfocused related-to-email">
|
<div id="password-resets" class="card ~neutral @low mb-2 unfocused related-to-email">
|
||||||
<span class="heading">{{ .lang.PasswordResets.title }}</span>
|
<span class="heading">{{ .lang.PasswordResets.title }}</span>
|
||||||
<p class="content my-2">{{ .lang.PasswordResets.description }}</p>
|
<p class="content my-2">{{ .lang.PasswordResets.description }}</p>
|
||||||
<p class="content my-2" id="password_resets-more-info">{{ .lang.PasswordResets.moreInfo }}</p>
|
|
||||||
<label class="row switch pb-4">
|
<label class="row switch pb-4">
|
||||||
<input type="checkbox" class="mr-2" id="password_resets-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
<input type="checkbox" class="mr-2" id="password_resets-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
||||||
</label>
|
</label>
|
||||||
@ -560,3 +541,4 @@
|
|||||||
<script src="js/setup.js" type="module"></script>
|
<script src="js/setup.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
0
images/jfa-go-icon.png
Normal file → Executable file
0
images/jfa-go-icon.png
Normal file → Executable file
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
0
images/jfa-go-icon.svg
Normal file → Executable file
0
images/jfa-go-icon.svg
Normal file → Executable file
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
@ -5,21 +5,20 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hrfee/jfa-go/jellyseerr"
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||||
user, imported, err := app.js.GetOrImportUser(jfID)
|
user, imported, err := app.js.GetOrImportUser(jfID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.debug.Printf(lm.FailedImportUser, lm.Jellyseerr, jfID, err)
|
app.debug.Printf("Failed to get or trigger import for Jellyseerr (user \"%s\"): %v", jfID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if imported {
|
if imported {
|
||||||
app.debug.Printf(lm.ImportJellyseerrUser, jfID, user.ID)
|
app.debug.Printf("Jellyseerr: Triggered import for Jellyfin user \"%s\" (ID %d)", jfID, user.ID)
|
||||||
}
|
}
|
||||||
notif, err := app.js.GetNotificationPreferencesByID(user.ID)
|
notif, err := app.js.GetNotificationPreferencesByID(user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.debug.Printf(lm.FailedGetJellyseerrNotificationPrefs, jfID, err)
|
app.debug.Printf("Failed to get notification prefs for Jellyseerr (user \"%s\"): %v", jfID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,7 +27,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
|||||||
if ok && email.Addr != "" && user.Email != email.Addr {
|
if ok && email.Addr != "" && user.Email != email.Addr {
|
||||||
err = app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: email.Addr})
|
err = app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: email.Addr})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err)
|
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
|
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
|
||||||
}
|
}
|
||||||
@ -52,7 +51,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
|||||||
if len(contactMethods) != 0 {
|
if len(contactMethods) != 0 {
|
||||||
err := app.js.ModifyNotifications(jfID, contactMethods)
|
err := app.js.ModifyNotifications(jfID, contactMethods)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,7 +59,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
|||||||
func (app *appContext) SynchronizeJellyseerrUsers() {
|
func (app *appContext) SynchronizeJellyseerrUsers() {
|
||||||
users, status, err := app.jf.GetUsers(false)
|
users, status, err := app.jf.GetUsers(false)
|
||||||
if err != nil || status != 200 {
|
if err != nil || status != 200 {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf("Failed to get users (%d): %s", status, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// I'm sure Jellyseerr can handle it,
|
// I'm sure Jellyseerr can handle it,
|
1
lang.go
1
lang.go
@ -122,7 +122,6 @@ type setupLang struct {
|
|||||||
Login langSection `json:"login"`
|
Login langSection `json:"login"`
|
||||||
JellyfinEmby langSection `json:"jellyfinEmby"`
|
JellyfinEmby langSection `json:"jellyfinEmby"`
|
||||||
Ombi langSection `json:"ombi"`
|
Ombi langSection `json:"ombi"`
|
||||||
Jellyseerr langSection `json:"jellyseerr"`
|
|
||||||
Email langSection `json:"email"`
|
Email langSection `json:"email"`
|
||||||
Messages langSection `json:"messages"`
|
Messages langSection `json:"messages"`
|
||||||
Notifications langSection `json:"notifications"`
|
Notifications langSection `json:"notifications"`
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
{
|
|
||||||
"meta": {
|
|
||||||
"name": "کوردی سۆرانی"
|
|
||||||
},
|
|
||||||
"strings": {
|
|
||||||
"pageTitle": "دروستکردنی هەژماری جێڵیفن",
|
|
||||||
"createAccountHeader": "دروستکردنی هەژمار",
|
|
||||||
"accountDetails": "زانیارییەکان",
|
|
||||||
"emailAddress": "ئیمەیل",
|
|
||||||
"username": "ناوی بەکارهێنەر",
|
|
||||||
"oldPassword": "وشەی نهێنی کۆن",
|
|
||||||
"newPassword": "وشەی نهێنی نوێ",
|
|
||||||
"password": "وشەی نهێنی",
|
|
||||||
"reEnterPassword": "دووبارە وشەی نهێنی بنووسەوە",
|
|
||||||
"reEnterPasswordInvalid": "وشە نهێنییەکان یەک ناگرن.",
|
|
||||||
"createAccountButton": "هەژمار دروستبکە",
|
|
||||||
"passwordRequirementsHeader": "داواکارییەکانی وشەی نهێنی",
|
|
||||||
"successHeader": "سەرکەوت!",
|
|
||||||
"confirmationRequired": "دووپاتکردنەوەی ئیمەیل داواکراوە",
|
|
||||||
"confirmationRequiredMessage": "تکایە سەیری نامەکانی ئیمەیلەکەت بکە بۆ دووپاتکردنەوەی ناونیشانەکەت.",
|
|
||||||
"yourAccountIsValidUntil": "هەژمارەکەت تاکو {date} کاردەکات.",
|
|
||||||
"sendPIN": "ئەم ژمارە نهێنییەی خوارەوە بۆ بۆتەکە بنێرە، پاشان وەرەوە بۆ پەیوەستکردنی هەژمارەکەت.",
|
|
||||||
"sendPINDiscord": "{command} لە چەناڵی {server_channel}ی دیسکۆردەکەت بنوسە، پاشان ئەم ژمارە نهێنییەی خوارەوە بنێرە.",
|
|
||||||
"matrixEnterUser": "",
|
|
||||||
"welcomeUser": "{user}، بەخێربێیت!",
|
|
||||||
"addContactMethod": "",
|
|
||||||
"editContactMethod": "",
|
|
||||||
"joinTheServer": "",
|
|
||||||
"customMessagePlaceholderHeader": "",
|
|
||||||
"customMessagePlaceholderContent": "",
|
|
||||||
"userPageSuccessMessage": "",
|
|
||||||
"resetPassword": "",
|
|
||||||
"resetPasswordThroughJellyfin": "",
|
|
||||||
"resetPasswordThroughLink": "",
|
|
||||||
"resetPasswordThroughLinkStart": "",
|
|
||||||
"resetPasswordThroughLinkEnd": "",
|
|
||||||
"resetPasswordUsername": "",
|
|
||||||
"resetPasswordEmail": "",
|
|
||||||
"resetPasswordContactMethod": "",
|
|
||||||
"resetSent": "",
|
|
||||||
"resetSentDescription": "",
|
|
||||||
"changePassword": "",
|
|
||||||
"referralsDescription": "",
|
|
||||||
"referralsWithExpiryDescription": "",
|
|
||||||
"copyReferral": "",
|
|
||||||
"invitedBy": ""
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"errorUserExists": "",
|
|
||||||
"errorInvalidCode": "",
|
|
||||||
"errorAccountLinked": "",
|
|
||||||
"errorEmailLinked": "",
|
|
||||||
"errorTelegramVerification": "",
|
|
||||||
"errorDiscordVerification": "",
|
|
||||||
"errorMatrixVerification": "",
|
|
||||||
"errorInvalidPIN": "",
|
|
||||||
"errorUnknown": "",
|
|
||||||
"errorNoEmail": "",
|
|
||||||
"errorCaptcha": "",
|
|
||||||
"errorPassword": "",
|
|
||||||
"errorNoMatch": "",
|
|
||||||
"errorOldPassword": "",
|
|
||||||
"passwordChanged": "",
|
|
||||||
"verified": ""
|
|
||||||
},
|
|
||||||
"validationStrings": {
|
|
||||||
"length": {
|
|
||||||
"singular": "",
|
|
||||||
"plural": ""
|
|
||||||
},
|
|
||||||
"uppercase": {
|
|
||||||
"singular": "",
|
|
||||||
"plural": ""
|
|
||||||
},
|
|
||||||
"lowercase": {
|
|
||||||
"singular": "",
|
|
||||||
"plural": ""
|
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"singular": "",
|
|
||||||
"plural": ""
|
|
||||||
},
|
|
||||||
"special": {
|
|
||||||
"singular": "",
|
|
||||||
"plural": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,7 +17,7 @@
|
|||||||
"confirmationRequired": "E-Mail Bestätigung erforderlich",
|
"confirmationRequired": "E-Mail Bestätigung erforderlich",
|
||||||
"confirmationRequiredMessage": "Bitte überprüfe deinen Posteingang und bestätige deine E-Mail-Adresse.",
|
"confirmationRequiredMessage": "Bitte überprüfe deinen Posteingang und bestätige deine E-Mail-Adresse.",
|
||||||
"yourAccountIsValidUntil": "Dein Konto wird bis zum {date} gültig sein.",
|
"yourAccountIsValidUntil": "Dein Konto wird bis zum {date} gültig sein.",
|
||||||
"sendPIN": "Sende die PIN unten an den Bot und komm dann hierher zurück, um dein Konto zu verknüpfen.",
|
"sendPIN": "Sende die untenstehende PIN an den Bot und komm dann hierher zurück, um dein Konto zu verbinden.",
|
||||||
"sendPINDiscord": "Gib auf Discord {command} in {server_channel} ein und sende die untenstehende PIN.",
|
"sendPINDiscord": "Gib auf Discord {command} in {server_channel} ein und sende die untenstehende PIN.",
|
||||||
"matrixEnterUser": "Gib deine Benutzer-ID ein und drücke auf Absenden. Anschließend erhälst du ein PIN, die hier eingegeben wird um fortzufahren.",
|
"matrixEnterUser": "Gib deine Benutzer-ID ein und drücke auf Absenden. Anschließend erhälst du ein PIN, die hier eingegeben wird um fortzufahren.",
|
||||||
"oldPassword": "Altes Passwort",
|
"oldPassword": "Altes Passwort",
|
||||||
@ -30,20 +30,7 @@
|
|||||||
"joinTheServer": "Server beitreten:",
|
"joinTheServer": "Server beitreten:",
|
||||||
"userPageSuccessMessage": "Du kannst die Details deines Accounts später in {myAccount} Seite einsehen und ändern.",
|
"userPageSuccessMessage": "Du kannst die Details deines Accounts später in {myAccount} Seite einsehen und ändern.",
|
||||||
"resetPassword": "Passwort zurücksetzen",
|
"resetPassword": "Passwort zurücksetzen",
|
||||||
"resetPasswordThroughJellyfin": "Um das Passwort zurückzusetzen, besuche {jfLink} und drücke auf die Schaltfläche \"Passwort vergessen\".",
|
"resetPasswordThroughJellyfin": "Um das Passwort zurückzusetzen, besuche {jfLink} und drücke auf die Schaltfläche \"Passwort vergessen\"."
|
||||||
"resetPasswordThroughLinkStart": "Um dein Passwort zurückzusetzen, gib eine der folgenden Möglichkeiten ein:",
|
|
||||||
"resetPasswordContactMethod": "Den Benutzernamen einer Kontaktmethode, die mit deinem Konto verknüpft ist",
|
|
||||||
"changePassword": "Passwort ändern",
|
|
||||||
"resetPasswordThroughLink": "Um dein Passwort zurückzusetzen, gib deinen Benutzernamen, deine E-Mail-Adresse oder einer verlinkten Kontaktmethode ein und sende ihn ab. Du erhältst einen Link zum Zurücksetzen deines Passworts.",
|
|
||||||
"resetSent": "Infos zum Zurücksetzen wurden gesendet.",
|
|
||||||
"resetSentDescription": "Wenn ein Konto mit dem angegebenen Benutzernamen/der angegebenen Kontaktmethode existiert, wurde ein Link zum Zurücksetzen des Passworts über alle verfügbaren Kontaktmethoden verschickt. Der Code ist 30 Minuten gültig.",
|
|
||||||
"referralsDescription": "Lade Freunde & Familie mit diesem Link zu Jellyfin ein. Wenn er abläuft, kannst du hier einen neuen Link anfordern.",
|
|
||||||
"copyReferral": "Link kopieren",
|
|
||||||
"invitedBy": "Du wurdest von {user} eingeladen.",
|
|
||||||
"resetPasswordThroughLinkEnd": "Drücke dann auf Abschicken. Du erhältst einen Link, mit dem du dein Passwort zurücksetzen kannst.",
|
|
||||||
"resetPasswordUsername": "Dein Jellyfin-Benutzername",
|
|
||||||
"resetPasswordEmail": "Deine E-Mail Adresse",
|
|
||||||
"referralsWithExpiryDescription": "Lade Freunde & Familie mit diesem Link zu Jellyfin ein. Der Link wird deaktiviert, sobald er abläuft."
|
|
||||||
},
|
},
|
||||||
"validationStrings": {
|
"validationStrings": {
|
||||||
"length": {
|
"length": {
|
||||||
@ -80,10 +67,6 @@
|
|||||||
"errorNoEmail": "E-Mail Adresse erforderlich.",
|
"errorNoEmail": "E-Mail Adresse erforderlich.",
|
||||||
"errorCaptcha": "Captcha falsch.",
|
"errorCaptcha": "Captcha falsch.",
|
||||||
"errorPassword": "Prüfe die Passwortanforderungen.",
|
"errorPassword": "Prüfe die Passwortanforderungen.",
|
||||||
"errorNoMatch": "Passwörter stimmen nicht überein.",
|
"errorNoMatch": "Passwörter stimmen nicht überein."
|
||||||
"errorAccountLinked": "Konto wird bereits verwendet.",
|
|
||||||
"errorEmailLinked": "E-Mail wird bereits verwendet.",
|
|
||||||
"passwordChanged": "Das Passwort wurde geändert.",
|
|
||||||
"errorOldPassword": "Das alte Passwort ist falsch."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
"title": "General",
|
"title": "General",
|
||||||
"listenAddress": "Listen Address",
|
"listenAddress": "Listen Address",
|
||||||
"urlBase": "URL Base",
|
"urlBase": "URL Base",
|
||||||
"urlBaseNotice": "Only needed if using a reverse proxy on a subfolder (e.g 'jellyf.in/accounts').",
|
"urlBaseNotice": "Only needed if using a reverse proxy on a subdomain (e.g 'jellyf.in/accounts').",
|
||||||
"lightTheme": "Light",
|
"lightTheme": "Light",
|
||||||
"darkTheme": "Dark",
|
"darkTheme": "Dark",
|
||||||
"useHTTPS": "Use HTTPS",
|
"useHTTPS": "Use HTTPS",
|
||||||
@ -94,13 +94,7 @@
|
|||||||
"ombi": {
|
"ombi": {
|
||||||
"title": "Ombi",
|
"title": "Ombi",
|
||||||
"description": "By connecting to Ombi, both a Jellyfin and Ombi account will be created when a user joins through jfa-go. After setup is finished, go to Settings to set a default profile for new ombi users.",
|
"description": "By connecting to Ombi, both a Jellyfin and Ombi account will be created when a user joins through jfa-go. After setup is finished, go to Settings to set a default profile for new ombi users.",
|
||||||
"apiKeyNotice": "Find this in the first tab of Ombi settings.",
|
"apiKeyNotice": "Find this in the first tab of Ombi settings."
|
||||||
"stabilityWarning": "Warning: Ombi integration is unstable, and can cause issues. Jellyseerr is recommended instead. See {n} for more info."
|
|
||||||
},
|
|
||||||
"jellyseerr": {
|
|
||||||
"title": "Jellyseerr",
|
|
||||||
"description": "Jellyseerr is an alternative to Ombi, and integrates with jfa-go slightly better. Again, after setup is finished, go to Settings to create a profile and add a template for new Jellyseerr accounts.",
|
|
||||||
"importExistingDescription": "If enabled, your existing users will have contact details and preferences from jfa-go synchronized."
|
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"title": "Messages",
|
"title": "Messages",
|
||||||
@ -140,7 +134,6 @@
|
|||||||
"passwordResets": {
|
"passwordResets": {
|
||||||
"title": "Password Resets",
|
"title": "Password Resets",
|
||||||
"description": "When a user tries to reset their password, Jellyfin creates a file named 'passwordreset-*.json' which contains a PIN. jfa-go reads the file and sends the PIN to the user. If you enabled the \"User Page\" feature, a reset can also be performed there, given a username, email, or contact method.",
|
"description": "When a user tries to reset their password, Jellyfin creates a file named 'passwordreset-*.json' which contains a PIN. jfa-go reads the file and sends the PIN to the user. If you enabled the \"User Page\" feature, a reset can also be performed there, given a username, email, or contact method.",
|
||||||
"moreInfo": "More information about the different ways of resetting passwords can be found on {n}.",
|
|
||||||
"pathToJellyfin": "Path to Jellyfin configuration directory",
|
"pathToJellyfin": "Path to Jellyfin configuration directory",
|
||||||
"pathToJellyfinNotice": "If you don't know where this is, try resetting your password in Jellyfin. A popup with '<path to jellyfin>/passwordreset-*.json' will appear. This is not necessary if you only want to use self-service password resets through the \"User Page\".",
|
"pathToJellyfinNotice": "If you don't know where this is, try resetting your password in Jellyfin. A popup with '<path to jellyfin>/passwordreset-*.json' will appear. This is not necessary if you only want to use self-service password resets through the \"User Page\".",
|
||||||
"resetLinks": "Send a link instead of a PIN",
|
"resetLinks": "Send a link instead of a PIN",
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
module github.com/hrfee/logmessages
|
|
||||||
|
|
||||||
go 1.22.4
|
|
@ -1,371 +0,0 @@
|
|||||||
package logmessages
|
|
||||||
|
|
||||||
/* Log strings for (almost) all the program.
|
|
||||||
* Helps avoid writing redundant, slightly different
|
|
||||||
* strings constantly.
|
|
||||||
* Also would help if I were to ever set up translation
|
|
||||||
* for logs. Mostly split by file, but obviously there's
|
|
||||||
* re-use, and occasionally related stuff is grouped.
|
|
||||||
*/
|
|
||||||
const (
|
|
||||||
Jellyseerr = "Jellyseerr"
|
|
||||||
Jellyfin = "Jellyfin"
|
|
||||||
Ombi = "Ombi"
|
|
||||||
Discord = "Discord"
|
|
||||||
Telegram = "Telegram"
|
|
||||||
Matrix = "Matrix"
|
|
||||||
Email = "Email"
|
|
||||||
|
|
||||||
// main.go
|
|
||||||
FailedLogging = "Failed to start log wrapper: %v\n"
|
|
||||||
|
|
||||||
NoConfig = "Couldn't find default config file"
|
|
||||||
Write = "Wrote to \"%s\""
|
|
||||||
FailedWriting = "Failed to write to \"%s\": %v"
|
|
||||||
FailedCreateDir = "Failed to create directory \"%s\": %v"
|
|
||||||
FailedReading = "Failed to read from \"%s\": %v"
|
|
||||||
FailedOpen = "Failed to open \"%s\": %v"
|
|
||||||
FailedStat = "Failed to stat \"%s\": %v"
|
|
||||||
PathNotFound = "Path \"%s\" not found"
|
|
||||||
|
|
||||||
CopyConfig = "Copied default configuration to \"%s\""
|
|
||||||
FailedCopyConfig = "Failed to copy default configuration to \"%s\": %v"
|
|
||||||
LoadConfig = "Loaded config file \"%s\""
|
|
||||||
FailedLoadConfig = "Failed to load config file \"%s\": %v"
|
|
||||||
ModifyConfig = "Config saved to \"%s\""
|
|
||||||
|
|
||||||
SocketPath = "Socket Path: \"%s\""
|
|
||||||
FailedSocketConnect = "Couldn't establish socket connection at \"%s\": %v"
|
|
||||||
SocketCheckRunning = "Make sure jfa-go is running."
|
|
||||||
FailedSocketRead = "Couldn't read message on socket \"%s\": %v"
|
|
||||||
SocketWrite = "Command sent."
|
|
||||||
FailedSocketWrite = "Coudln't write message on socket \"%s\": %v"
|
|
||||||
|
|
||||||
FailedLangLoad = "Failed to load language files: %v"
|
|
||||||
|
|
||||||
UsingTLS = "Using TLS/HTTP2"
|
|
||||||
|
|
||||||
UsingOmbi = "Starting " + " + Ombi + " + " client"
|
|
||||||
UsingJellyseerr = "Starting " + Jellyseerr + " client"
|
|
||||||
UsingEmby = "Using Emby server type (EXPERIMENTAL: PWRs are not available, and support is limited.)"
|
|
||||||
UsingJellyfin = "Using " + Jellyfin + " server type"
|
|
||||||
UsingJellyfinAuth = "Using " + Jellyfin + " for authentication"
|
|
||||||
UsingLocalAuth = "Using local username/pw authentication (NOT RECOMMENDED)"
|
|
||||||
|
|
||||||
AuthJellyfin = "Authenticated with " + Jellyfin + " @ \"%s\""
|
|
||||||
FailedAuthJellyfin = "Failed to authenticate with " + Jellyfin + " @ \"%s\" (code %d): %v"
|
|
||||||
|
|
||||||
InitDiscord = "Initialized Discord daemon"
|
|
||||||
FailedInitDiscord = "Failed to initialize Discord daemon: %v"
|
|
||||||
InitTelegram = "Initialized Telegram daemon"
|
|
||||||
FailedInitTelegram = "Failed to initialize Telegram daemon: %v"
|
|
||||||
InitMatrix = "Initialized Matrix daemon"
|
|
||||||
FailedInitMatrix = "Failed to initialize Matrix daemon: %v"
|
|
||||||
|
|
||||||
InitRouter = "Initializing router"
|
|
||||||
LoadRoutes = "Loading Routes"
|
|
||||||
|
|
||||||
LoadingSetup = "Loading setup @ \"%s\""
|
|
||||||
ServingSetup = "Loaded, visit \"%s\" to start."
|
|
||||||
|
|
||||||
InvalidSSLCert = "Failed loading SSL Certificate \"%s\": %v"
|
|
||||||
InvalidSSLKey = "Failed loading SSL Keyfile \"%s\": %v"
|
|
||||||
|
|
||||||
FailServeSSL = "Failure serving with SSL/TLS: %v"
|
|
||||||
FailServe = "Failure serving: %v"
|
|
||||||
|
|
||||||
Serving = "Loaded @ \"%s\""
|
|
||||||
|
|
||||||
QuitReceived = "Restart/Quit signal received, please be patient."
|
|
||||||
Quitting = "Shutting down..."
|
|
||||||
Restarting = "Restarting..."
|
|
||||||
FailedHardRestartWindows = "hard restarts not available on windows"
|
|
||||||
Quit = "Server shut down."
|
|
||||||
FailedQuit = "Server shutdown failed: %v"
|
|
||||||
|
|
||||||
// api-activities.go
|
|
||||||
FailedDBReadActivities = "Failed to read activities from DB: %v"
|
|
||||||
|
|
||||||
// api-backups.go
|
|
||||||
IgnoreInvalidFilename = "Invalid filename \"%s\", ignoring: %v"
|
|
||||||
GetUpload = "Retrieved uploaded file \"%s\""
|
|
||||||
FailedGetUpload = "Failed to retrieve file from form data: %v"
|
|
||||||
|
|
||||||
// api-invites.go
|
|
||||||
DeleteOldInvite = "Deleting old invite \"%s\""
|
|
||||||
DeleteInvite = "Deleting invite \"%s\""
|
|
||||||
FailedDeleteInvite = "Failed to delete invite \"%s\": %v"
|
|
||||||
GenerateInvite = "Generating new invite"
|
|
||||||
FailedGenerateInvite = "Failed to generate new invite: %v"
|
|
||||||
InvalidInviteCode = "Invalid invite code \"%s\""
|
|
||||||
|
|
||||||
FailedSendToTooltipNoUser = "Failed: \"%s\" not found"
|
|
||||||
FailedSendToTooltipMultiUser = "Failed: \"%s\" linked to multiple users"
|
|
||||||
|
|
||||||
FailedParseTime = "Failed to parse time value: %v"
|
|
||||||
|
|
||||||
FailedGetContactMethod = "Failed to get contact method for \"%s\", make sure one is set."
|
|
||||||
|
|
||||||
SetAdminNotify = "Set \"%s\" to %t for admin address \"%s\""
|
|
||||||
|
|
||||||
// *jellyseerr*.go
|
|
||||||
FailedGetUsers = "Failed to get user(s) from %s: %v"
|
|
||||||
// FIXME: Once done, look back at uses of FailedGetUsers for places where this would make more sense.
|
|
||||||
FailedGetUser = "Failed to get user \"%s\" from %s: %v"
|
|
||||||
FailedGetJellyseerrNotificationPrefs = "Failed to get user \"%s\"'s notification prefs from " + Jellyseerr + ": %v"
|
|
||||||
FailedSyncContactMethods = "Failed to sync contact methods with %s: %v"
|
|
||||||
ImportJellyseerrUser = "Triggered import for " + Jellyseerr + " user \"%s\" (New ID: %d)"
|
|
||||||
FailedImportUser = "Failed to get or trigger import for %s user \"%s\": %v"
|
|
||||||
|
|
||||||
// api-messages.go
|
|
||||||
FailedGetCustomMessage = "Failed to get custom message \"%s\""
|
|
||||||
SetContactPrefForService = "Set contact preference for %s (\"%s\"): %t"
|
|
||||||
|
|
||||||
// Matrix
|
|
||||||
InvalidPIN = "Invalid PIN \"%s\""
|
|
||||||
ExpiredPIN = "Expired PIN \"%s\""
|
|
||||||
InvalidPassword = "Invalid Password"
|
|
||||||
UnauthorizedPIN = "Unauthorized PIN \"%s\""
|
|
||||||
FailedCreateRoom = "Failed to create room: %v"
|
|
||||||
|
|
||||||
// api-profiles.go
|
|
||||||
SetDefaultProfile = "Setting default profile to \"%s\""
|
|
||||||
|
|
||||||
FailedApplyProfile = "Failed to apply profile for %s user \"%s\": %v"
|
|
||||||
ApplyProfile = "Applying settings from profile \"%s\""
|
|
||||||
FailedGetProfile = "Failed to find profile \"%s\""
|
|
||||||
FailedApplyTemplate = "Failed to apply %s template for %s user \"%s\": %v"
|
|
||||||
FallbackToDefault = ", using default"
|
|
||||||
CreateProfileFromUser = "Creating profile from user \"%s\""
|
|
||||||
FailedGetJellyfinDisplayPrefs = "Failed to get DisplayPreferences for user \"%s\" from " + Jellyfin + ": %v"
|
|
||||||
ProfileNoHomescreen = "No homescreen template in profile \"%s\""
|
|
||||||
Profile = "profile"
|
|
||||||
Lang = "language"
|
|
||||||
User = "user"
|
|
||||||
ApplyingTemplatesFrom = "Applying templates from %s: \"%s\" to %d users"
|
|
||||||
DelayingRequests = "Delay will be added between requests (count = %d)"
|
|
||||||
|
|
||||||
// api-userpage.go
|
|
||||||
EmailConfirmationRequired = "User \"%s\" requires email confirmation"
|
|
||||||
|
|
||||||
ChangePassword = "Changed password for %s user \"%s\""
|
|
||||||
FailedChangePassword = "Failed to change password for %s user \"%s\": %v"
|
|
||||||
|
|
||||||
GetReferralTemplate = "Found referral template \"%s\""
|
|
||||||
FailedGetReferralTemplate = "Failed to find referral template \"%s\": %v"
|
|
||||||
DeleteOldReferral = "Deleting old referral \"%s\""
|
|
||||||
RenewOldReferral = "Renewing old referral \"%s\""
|
|
||||||
|
|
||||||
// api-users.go
|
|
||||||
CreateUser = "Created %s user \"%s\""
|
|
||||||
FailedCreateUser = "Failed to create new %s user \"%s\": %v"
|
|
||||||
LinkUser = "Linked %s user \"%s\""
|
|
||||||
FailedLinkUser = "Failed to link %s user \"%s\" with \"%s\": %v"
|
|
||||||
DeleteUser = "Deleted %s user \"%s\""
|
|
||||||
FailedDeleteUser = "Failed to delete %s user \"%s\": %v"
|
|
||||||
FailedDeleteUsers = "Failed to delete %s user(s): %v"
|
|
||||||
UserExists = "user already exists"
|
|
||||||
AccountLinked = "account already linked and require_unique enabled"
|
|
||||||
AccountUnverified = "unverified"
|
|
||||||
FailedSetDiscordMemberRole = "Failed to apply/remove " + Discord + " member role: %v"
|
|
||||||
|
|
||||||
FailedSetEmailAddress = "Failed to set email address for %s user \"%s\": %v"
|
|
||||||
|
|
||||||
AdditionalErrors = "Additional errors from %s: %v"
|
|
||||||
|
|
||||||
IncorrectCaptcha = "captcha incorrect"
|
|
||||||
|
|
||||||
ExtendCreateExpiry = "Extended or created expiry for user \"%s\""
|
|
||||||
|
|
||||||
UserEmailAdjusted = "Email for user \"%s\" adjusted"
|
|
||||||
UserAdminAdjusted = "Admin state for user \"%s\" set to %t"
|
|
||||||
UserLabelAdjusted = "Label for user \"%s\" set to \"%s\""
|
|
||||||
|
|
||||||
// api.go
|
|
||||||
ApplyUpdate = "Applied update"
|
|
||||||
FailedApplyUpdate = "Failed to apply update: %v"
|
|
||||||
UpdateManual = "update is manual"
|
|
||||||
|
|
||||||
// backups.go
|
|
||||||
DeleteOldBackup = "Deleted old backup \"%s\""
|
|
||||||
FailedDeleteOldBackup = "Failed to delete old backup \"%s\": %v"
|
|
||||||
CreateBackup = "Created database backup \"%+v\""
|
|
||||||
FailedCreateBackup = "Faled to create database backup: %v"
|
|
||||||
MoveOldDB = "Moved existing database to \"%s\""
|
|
||||||
FailedMoveOldDB = "Failed to move existing database to \"%s\": %v"
|
|
||||||
RestoreDB = "Restored database from \"%s\""
|
|
||||||
FailedRestoreDB = "Failed to resotre database from \"%s\": %v"
|
|
||||||
|
|
||||||
// config.go
|
|
||||||
EnableAllPWRMethods = "No PWR method preferences set in [user_page], all will be enabled"
|
|
||||||
InitProxy = "Initialized proxy @ \"%s\""
|
|
||||||
FailedInitProxy = "Failed to initialize proxy @ \"%s\": %v\nStartup will pause for a bit to grab your attention."
|
|
||||||
|
|
||||||
// discord.go
|
|
||||||
StartDaemon = "Started %s daemon"
|
|
||||||
FailedStartDaemon = "Failed to start %s daemon: %v"
|
|
||||||
FailedGetDiscordGuildMembers = "Failed to get " + Discord + " guild members: %v"
|
|
||||||
FailedGetDiscordGuild = "Failed to get " + Discord + " guild: %v"
|
|
||||||
FailedGetDiscordRoles = "Failed to get " + Discord + " roles: %v"
|
|
||||||
FailedCreateDiscordInviteChannel = "Failed to create " + Discord + " invite channel: %v"
|
|
||||||
InviteChannelEmpty = "no invite channel set in settings"
|
|
||||||
FailedGetDiscordChannels = "Failed to get " + Discord + " channel(s): %v"
|
|
||||||
FailedGetDiscordChannel = "Failed to get " + Discord + " channel \"%s\": %v"
|
|
||||||
MonitorAllDiscordChannels = "Will monitor all " + Discord + " channels"
|
|
||||||
FailedCreateDiscordDMChannel = "Failed to create " + Discord + " private DM channel with \"%s\": %v"
|
|
||||||
NotFound = "not found"
|
|
||||||
RegisterDiscordChoice = "Registered " + Discord + " %s choice \"%s\""
|
|
||||||
FailedRegisterDiscordChoices = "Failed to register " + Discord + " %s choices: %v"
|
|
||||||
FailedDeregDiscordChoice = "Failed to deregister " + Discord + " %s choice \"%s\": %v"
|
|
||||||
RegisterDiscordCommand = "Registered " + Discord + " command \"%s\""
|
|
||||||
FailedRegisterDiscordCommand = "Failed to register " + Discord + " command \"%s\": %v"
|
|
||||||
FailedGetDiscordCommands = "Failed to get " + Discord + " commands: %v"
|
|
||||||
FailedDeregDiscordCommand = "Failed to deregister " + Discord + " command \"%s\": %v"
|
|
||||||
|
|
||||||
FailedReply = "Failed to reply to %s message from \"%s\": %v"
|
|
||||||
FailedMessage = "Failed to send %s message to \"%s\": %v"
|
|
||||||
|
|
||||||
IgnoreOutOfChannelMessage = "Ignoring out-of-channel %s message"
|
|
||||||
|
|
||||||
FailedGenerateDiscordInvite = "Failed to generate " + Discord + " invite: %v"
|
|
||||||
|
|
||||||
// email.go
|
|
||||||
FailedInitSMTP = "Failed to initialize SMTP mailer: %v"
|
|
||||||
FailedGeneratePWRLink = "Failed to generate PWR link: %v"
|
|
||||||
|
|
||||||
// housekeeping-d.go
|
|
||||||
hk = "Housekeeping: "
|
|
||||||
hkcu = hk + "cleaning up "
|
|
||||||
HousekeepingEmail = hkcu + Email + " addresses"
|
|
||||||
HousekeepingDiscord = hkcu + Discord + " IDs"
|
|
||||||
HousekeepingTelegram = hkcu + Telegram + " IDs"
|
|
||||||
HousekeepingMatrix = hkcu + Matrix + " IDs"
|
|
||||||
HousekeepingCaptcha = hkcu + "PWR Captchas"
|
|
||||||
HousekeepingActivity = hkcu + "Activity log"
|
|
||||||
HousekeepingInvites = hkcu + "Invites"
|
|
||||||
ActivityLogTxnTooBig = hk + "Activity log delete transaction was too big, going one-by-one"
|
|
||||||
|
|
||||||
// matrix*.go
|
|
||||||
FailedSyncMatrix = "Failed to sync " + Matrix + " daemon: %v"
|
|
||||||
FailedCreateMatrixRoom = "Failed to create " + Matrix + " room with user \"%s\": %v"
|
|
||||||
MatrixOLMLog = "Matrix/OLM: %v"
|
|
||||||
MatrixOLMTraceLog = "Matrix/OLM [TRACE]:"
|
|
||||||
FailedDecryptMatrixMessage = "Failed to decrypt " + Matrix + " E2EE'd message: %v"
|
|
||||||
FailedEnableMatrixEncryption = "Failed to enable encryption in " + Matrix + " room \"%s\": %v"
|
|
||||||
|
|
||||||
// NOTE: "migrations.go" is the one file where log messages are not part of logmessages/logmessages.go.
|
|
||||||
|
|
||||||
// pwreset.go
|
|
||||||
PWRExpired = "PWR for user \"%s\" already expired @ %s, check system time!"
|
|
||||||
|
|
||||||
// router.go
|
|
||||||
UseDefaultHTML = "Using default HTML \"%s\""
|
|
||||||
UseCustomHTML = "Using custom HTML \"%s\""
|
|
||||||
FailedLoadTemplates = "Failed to load %s templates: %v"
|
|
||||||
Internal = "internal"
|
|
||||||
External = "external"
|
|
||||||
RegisterPprof = "Registered pprof"
|
|
||||||
SwaggerWarning = "Warning: Swagger should not be used on a public instance."
|
|
||||||
|
|
||||||
// storage.go
|
|
||||||
ConnectDB = "Connected to DB \"%s\""
|
|
||||||
FailedConnectDB = "Failed to open/connect to database \"%s\": %v"
|
|
||||||
|
|
||||||
// updater.go
|
|
||||||
NoUpdate = "No new updates available"
|
|
||||||
FoundUpdate = "Found update"
|
|
||||||
FailedGetUpdateTag = "Failed to get latest tag: %v"
|
|
||||||
FailedGetUpdate = "Failed to get update: %v"
|
|
||||||
UpdateTagDetails = "Update/Tag details: %+v"
|
|
||||||
|
|
||||||
// user-auth.go
|
|
||||||
UserPage = "userpage"
|
|
||||||
UserPageRequiresJellyfinAuth = "Jellyfin login must be enabled for user page access."
|
|
||||||
|
|
||||||
// user-d.go
|
|
||||||
CheckUserExpiries = "Checking for user expiry"
|
|
||||||
DeleteExpiryForOldUser = "Deleting expiry for old user \"%s\""
|
|
||||||
DeleteExpiredUser = "Deleting expired user \"%s\""
|
|
||||||
DisableExpiredUser = "Disabling expired user \"%s\""
|
|
||||||
FailedDeleteOrDisableExpiredUser = "Failed to delete/disable expired user \"%s\": %v"
|
|
||||||
|
|
||||||
// views.go
|
|
||||||
FailedServerPush = "Failed to use HTTP/2 Server Push: %v"
|
|
||||||
IgnoreBotPWR = "Ignore PWR magic link visit from bot"
|
|
||||||
ReCAPTCHA = "ReCAPTCHA"
|
|
||||||
FailedGenerateCaptcha = "Failed to generate captcha: %v"
|
|
||||||
CaptchaNotFound = "Captcha \"%s\" not found in invite \"%s\""
|
|
||||||
FailedVerifyReCAPTCHA = "Failed to verify reCAPTCHA: %v"
|
|
||||||
InvalidHostname = "invalid hostname (wanted \"%s\", got \"%s\")"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FailedGetCookies = "Failed to get cookie(s) \"%s\": %v"
|
|
||||||
FailedParseJWT = "Failed to parse JWT: %v"
|
|
||||||
FailedCastJWT = "JWT claims unreadable"
|
|
||||||
InvalidJWT = "JWT was invalidated, of incorrect type or has expired"
|
|
||||||
LocallyInvalidatedJWT = "JWT is listed as invalidated"
|
|
||||||
FailedSignJWT = "Failed to sign JWT: %v"
|
|
||||||
|
|
||||||
RequestingToken = "Token requested (%s)"
|
|
||||||
TokenLoginAttempt = "login attempt"
|
|
||||||
TokenRefresh = "refresh token"
|
|
||||||
UserTokenLoginAttempt = UserPage + " " + TokenLoginAttempt
|
|
||||||
UserTokenRefresh = UserPage + " " + TokenRefresh
|
|
||||||
GenerateToken = "Token generated for user \"%s\""
|
|
||||||
FailedGenerateToken = "Failed to generate token: %v"
|
|
||||||
|
|
||||||
FailedAuthRequest = "Failed to authorize request: %v"
|
|
||||||
InvalidAuthHeader = "invalid auth header"
|
|
||||||
NonAdminToken = "token not for admin use"
|
|
||||||
NonAdminUser = "user \"%s\" not admin"
|
|
||||||
InvalidUserOrPass = "invalid user/pass"
|
|
||||||
EmptyUserOrPass = "invalid user/pass"
|
|
||||||
UserDisabled = "user is disabled"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FailedConstructExpiryAdmin = "Failed to construct expiry notification for \"%s\": %v"
|
|
||||||
FailedSendExpiryAdmin = "Failed to send expiry notification for \"%s\" to \"%s\": %v"
|
|
||||||
SentExpiryAdmin = "Sent expiry notification for \"%s\" to \"%s\""
|
|
||||||
|
|
||||||
FailedConstructCreationAdmin = "Failed to construct creation notification for \"%s\": %v"
|
|
||||||
FailedSendCreationAdmin = "Failed to send creation notification for \"%s\" to \"%s\": %v"
|
|
||||||
SentCreationAdmin = "Sent creation notification for \"%s\" to \"%s\""
|
|
||||||
|
|
||||||
FailedConstructInviteMessage = "Failed to construct invite message for \"%s\": %v"
|
|
||||||
FailedSendInviteMessage = "Failed to send invite message for \"%s\" to \"%s\": %v"
|
|
||||||
SentInviteMessage = "Sent invite message for \"%s\" to \"%s\""
|
|
||||||
|
|
||||||
FailedConstructConfirmationEmail = "Failed to construct confirmation email for \"%s\": %v"
|
|
||||||
FailedSendConfirmationEmail = "Failed to send confirmation email for \"%s\" to \"%s\": %v"
|
|
||||||
SentConfirmationEmail = "Sent confirmation email for \"%s\" to \"%s\""
|
|
||||||
|
|
||||||
FailedConstructPWRMessage = "Failed to construct PWR message for \"%s\": %v"
|
|
||||||
FailedSendPWRMessage = "Failed to send PWR message for \"%s\" to \"%s\": %v"
|
|
||||||
SentPWRMessage = "Sent PWR message for \"%s\" to \"%s\""
|
|
||||||
|
|
||||||
FailedConstructWelcomeMessage = "Failed to construct welcome message for \"%s\": %v"
|
|
||||||
FailedSendWelcomeMessage = "Failed to send welcome message for \"%s\" to \"%s\": %v"
|
|
||||||
SentWelcomeMessage = "Sent welcome message for \"%s\" to \"%s\""
|
|
||||||
|
|
||||||
FailedConstructEnableDisableMessage = "Failed to construct enable/disable message for \"%s\": %v"
|
|
||||||
FailedSendEnableDisableMessage = "Failed to send enable/disable message for \"%s\" to \"%s\": %v"
|
|
||||||
SentEnableDisableMessage = "Sent enable/disable message for \"%s\" to \"%s\""
|
|
||||||
|
|
||||||
FailedConstructDeletionMessage = "Failed to construct account deletion message for \"%s\": %v"
|
|
||||||
FailedSendDeletionMessage = "Failed to send account deletion message for \"%s\" to \"%s\": %v"
|
|
||||||
SentDeletionMessage = "Sent account deletion message for \"%s\" to \"%s\""
|
|
||||||
|
|
||||||
FailedConstructExpiryAdjustmentMessage = "Failed to construct expiry adjustment message for \"%s\": %v"
|
|
||||||
FailedSendExpiryAdjustmentMessage = "Failed to send expiry adjustment message for \"%s\" to \"%s\": %v"
|
|
||||||
SentExpiryAdjustmentMessage = "Sent expiry adjustment message for \"%s\" to \"%s\""
|
|
||||||
|
|
||||||
FailedConstructExpiryMessage = "Failed to construct expiry message for \"%s\": %v"
|
|
||||||
FailedSendExpiryMessage = "Failed to send expiry message for \"%s\" to \"%s\": %v"
|
|
||||||
SentExpiryMessage = "Sent expiry message for \"%s\" to \"%s\""
|
|
||||||
|
|
||||||
FailedConstructAnnouncementMessage = "Failed to construct announcement message for \"%s\": %v"
|
|
||||||
FailedSendAnnouncementMessage = "Failed to send announcement message for \"%s\" to \"%s\": %v"
|
|
||||||
SentAnnouncementMessage = "Sent announcement message for \"%s\" to \"%s\""
|
|
||||||
)
|
|
150
main.go
150
main.go
@ -27,7 +27,6 @@ import (
|
|||||||
"github.com/hrfee/jfa-go/easyproxy"
|
"github.com/hrfee/jfa-go/easyproxy"
|
||||||
"github.com/hrfee/jfa-go/jellyseerr"
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
"github.com/hrfee/jfa-go/logger"
|
"github.com/hrfee/jfa-go/logger"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/jfa-go/ombi"
|
"github.com/hrfee/jfa-go/ombi"
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
@ -102,9 +101,8 @@ type appContext struct {
|
|||||||
// Keeping jf name because I can't think of a better one
|
// Keeping jf name because I can't think of a better one
|
||||||
jf *mediabrowser.MediaBrowser
|
jf *mediabrowser.MediaBrowser
|
||||||
authJf *mediabrowser.MediaBrowser
|
authJf *mediabrowser.MediaBrowser
|
||||||
ombi *OmbiWrapper
|
ombi *ombi.Ombi
|
||||||
js *JellyseerrWrapper
|
js *jellyseerr.Jellyseerr
|
||||||
thirdPartyServices []ThirdPartyService
|
|
||||||
datePattern string
|
datePattern string
|
||||||
timePattern string
|
timePattern string
|
||||||
storage Storage
|
storage Storage
|
||||||
@ -113,7 +111,6 @@ type appContext struct {
|
|||||||
telegram *TelegramDaemon
|
telegram *TelegramDaemon
|
||||||
discord *DiscordDaemon
|
discord *DiscordDaemon
|
||||||
matrix *MatrixDaemon
|
matrix *MatrixDaemon
|
||||||
contactMethods []ContactMethodLinker
|
|
||||||
info, debug, err *logger.Logger
|
info, debug, err *logger.Logger
|
||||||
host string
|
host string
|
||||||
port int
|
port int
|
||||||
@ -216,21 +213,22 @@ func start(asDaemon, firstCall bool) {
|
|||||||
firstRun = true
|
firstRun = true
|
||||||
dConfig, err := fs.ReadFile(localFS, "config-default.ini")
|
dConfig, err := fs.ReadFile(localFS, "config-default.ini")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.NoConfig)
|
app.err.Fatalf("Couldn't find default config file")
|
||||||
}
|
}
|
||||||
nConfig, err := os.Create(app.configPath)
|
nConfig, err := os.Create(app.configPath)
|
||||||
if err != nil && os.IsNotExist(err) {
|
if err != nil && os.IsNotExist(err) {
|
||||||
err = os.MkdirAll(filepath.Dir(app.configPath), 0760)
|
err = os.MkdirAll(filepath.Dir(app.configPath), 0760)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedWriting, app.configPath, err)
|
app.err.Printf("Couldn't open config file for writing: \"%s\"", app.configPath)
|
||||||
|
app.err.Fatalf("Error: %s", err)
|
||||||
}
|
}
|
||||||
defer nConfig.Close()
|
defer nConfig.Close()
|
||||||
_, err = nConfig.Write(dConfig)
|
_, err = nConfig.Write(dConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedCopyConfig, app.configPath, err)
|
app.err.Fatalf("Couldn't copy default config.")
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.CopyConfig, app.configPath)
|
app.info.Printf("Copied default configuration to \"%s\"", app.configPath)
|
||||||
tempConfig, _ := ini.Load(app.configPath)
|
tempConfig, _ := ini.Load(app.configPath)
|
||||||
tempConfig.Section("").Key("first_run").SetValue("true")
|
tempConfig.Section("").Key("first_run").SetValue("true")
|
||||||
tempConfig.SaveTo(app.configPath)
|
tempConfig.SaveTo(app.configPath)
|
||||||
@ -239,9 +237,8 @@ func start(asDaemon, firstCall bool) {
|
|||||||
var debugMode bool
|
var debugMode bool
|
||||||
var address string
|
var address string
|
||||||
if err := app.loadConfig(); err != nil {
|
if err := app.loadConfig(); err != nil {
|
||||||
app.err.Fatalf(lm.FailedLoadConfig, app.configPath, err)
|
app.err.Fatalf("Failed to load config file \"%s\": %v", app.configPath, err)
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.LoadConfig, app.configPath)
|
|
||||||
|
|
||||||
if app.config.Section("").Key("first_run").MustBool(false) {
|
if app.config.Section("").Key("first_run").MustBool(false) {
|
||||||
firstRun = true
|
firstRun = true
|
||||||
@ -273,7 +270,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
os.Remove(SOCK)
|
os.Remove(SOCK)
|
||||||
listener, err := net.Listen("unix", SOCK)
|
listener, err := net.Listen("unix", SOCK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedSocketConnect, SOCK, err)
|
app.err.Fatalf("Couldn't establish socket connection at %s\n", SOCK)
|
||||||
}
|
}
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
@ -289,13 +286,13 @@ func start(asDaemon, firstCall bool) {
|
|||||||
for {
|
for {
|
||||||
con, err := listener.Accept()
|
con, err := listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedSocketRead, SOCK, err)
|
app.err.Printf("Couldn't read message on %s: %s", SOCK, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
buf := make([]byte, 512)
|
buf := make([]byte, 512)
|
||||||
nr, err := con.Read(buf)
|
nr, err := con.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedSocketRead, SOCK, err)
|
app.err.Printf("Couldn't read message on %s: %s", SOCK, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
command := string(buf[0:nr])
|
command := string(buf[0:nr])
|
||||||
@ -320,13 +317,13 @@ func start(asDaemon, firstCall bool) {
|
|||||||
err = app.storage.loadLang(langFS, os.DirFS(externalLang))
|
err = app.storage.loadLang(langFS, os.DirFS(externalLang))
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.info.Fatalf(lm.FailedLangLoad, err)
|
app.info.Fatalf("Failed to load language files: %+v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !firstRun {
|
if !firstRun {
|
||||||
app.host = app.config.Section("ui").Key("host").String()
|
app.host = app.config.Section("ui").Key("host").String()
|
||||||
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
||||||
app.info.Println(lm.UsingTLS)
|
app.info.Println("Using TLS/HTTP2")
|
||||||
app.port = app.config.Section("advanced").Key("tls_port").MustInt(8057)
|
app.port = app.config.Section("advanced").Key("tls_port").MustInt(8057)
|
||||||
} else {
|
} else {
|
||||||
app.port = app.config.Section("ui").Key("port").MustInt(8056)
|
app.port = app.config.Section("ui").Key("port").MustInt(8056)
|
||||||
@ -351,32 +348,29 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
||||||
|
|
||||||
// NOTE: As of writing this, the order in app.thirdPartServices doesn't matter,
|
app.debug.Printf("Loaded config file \"%s\"", app.configPath)
|
||||||
// but in future it might (like app.contactMethods does), so append to the end!
|
|
||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
app.ombi = &OmbiWrapper{}
|
app.debug.Printf("Connecting to Ombi")
|
||||||
app.debug.Printf(lm.UsingOmbi)
|
|
||||||
ombiServer := app.config.Section("ombi").Key("server").String()
|
ombiServer := app.config.Section("ombi").Key("server").String()
|
||||||
app.ombi.Ombi = ombi.NewOmbi(
|
app.ombi = ombi.NewOmbi(
|
||||||
ombiServer,
|
ombiServer,
|
||||||
app.config.Section("ombi").Key("api_key").String(),
|
app.config.Section("ombi").Key("api_key").String(),
|
||||||
common.NewTimeoutHandler("Ombi", ombiServer, true),
|
common.NewTimeoutHandler("Ombi", ombiServer, true),
|
||||||
)
|
)
|
||||||
app.thirdPartyServices = append(app.thirdPartyServices, app.ombi)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
||||||
app.js = &JellyseerrWrapper{}
|
app.debug.Printf("Connecting to Jellyseerr")
|
||||||
app.debug.Printf(lm.UsingJellyseerr)
|
|
||||||
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
|
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
|
||||||
app.js.Jellyseerr = jellyseerr.NewJellyseerr(
|
app.js = jellyseerr.NewJellyseerr(
|
||||||
jellyseerrServer,
|
jellyseerrServer,
|
||||||
app.config.Section("jellyseerr").Key("api_key").String(),
|
app.config.Section("jellyseerr").Key("api_key").String(),
|
||||||
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
|
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
|
||||||
)
|
)
|
||||||
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
|
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
|
||||||
// app.js.LogRequestBodies = true
|
// app.js.LogRequestBodies = true
|
||||||
app.thirdPartyServices = append(app.thirdPartyServices, app.js)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,9 +398,10 @@ func start(asDaemon, firstCall bool) {
|
|||||||
if stringServerType == "emby" {
|
if stringServerType == "emby" {
|
||||||
serverType = mediabrowser.EmbyServer
|
serverType = mediabrowser.EmbyServer
|
||||||
timeoutHandler = mediabrowser.NewNamedTimeoutHandler("Emby", "\""+server+"\"", true)
|
timeoutHandler = mediabrowser.NewNamedTimeoutHandler("Emby", "\""+server+"\"", true)
|
||||||
app.info.Println(lm.UsingEmby)
|
app.info.Println("Using Emby server type")
|
||||||
|
fmt.Println(warning("WARNING: Emby compatibility is experimental, and support is limited.\nPassword resets are not available."))
|
||||||
} else {
|
} else {
|
||||||
app.info.Println(lm.UsingJellyfin)
|
app.info.Println("Using Jellyfin server type")
|
||||||
}
|
}
|
||||||
|
|
||||||
app.jf, err = mediabrowser.NewServer(
|
app.jf, err = mediabrowser.NewServer(
|
||||||
@ -420,7 +415,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
cacheTimeout,
|
cacheTimeout,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedAuthJellyfin, server, -1, err)
|
app.err.Fatalf("Failed to authenticate with Jellyfin @ \"%s\": %v", server, err)
|
||||||
}
|
}
|
||||||
if debugMode {
|
if debugMode {
|
||||||
app.jf.Verbose = true
|
app.jf.Verbose = true
|
||||||
@ -438,9 +433,9 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
_, status, err = app.jf.MustAuthenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String(), retryOpts)
|
_, status, err = app.jf.MustAuthenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String(), retryOpts)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Fatalf(lm.FailedAuthJellyfin, server, status, err)
|
app.err.Fatalf("Failed to authenticate with Jellyfin @ \"%s\" (%d): %v", server, status, err)
|
||||||
}
|
}
|
||||||
app.info.Printf(lm.AuthJellyfin, server)
|
app.info.Printf("Authenticated with \"%s\"", server)
|
||||||
|
|
||||||
runMigrations(app)
|
runMigrations(app)
|
||||||
|
|
||||||
@ -453,9 +448,8 @@ func start(asDaemon, firstCall bool) {
|
|||||||
user.Username = app.config.Section("ui").Key("username").String()
|
user.Username = app.config.Section("ui").Key("username").String()
|
||||||
user.Password = app.config.Section("ui").Key("password").String()
|
user.Password = app.config.Section("ui").Key("password").String()
|
||||||
app.adminUsers = append(app.adminUsers, user)
|
app.adminUsers = append(app.adminUsers, user)
|
||||||
app.info.Println(lm.UsingLocalAuth)
|
|
||||||
} else {
|
} else {
|
||||||
app.debug.Println(lm.UsingJellyfinAuth)
|
app.debug.Println("Using Jellyfin for authentication")
|
||||||
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
|
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
|
||||||
if debugMode {
|
if debugMode {
|
||||||
app.authJf.Verbose = true
|
app.authJf.Verbose = true
|
||||||
@ -518,42 +512,34 @@ func start(asDaemon, firstCall bool) {
|
|||||||
defer backupDaemon.Shutdown()
|
defer backupDaemon.Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: The order in which these are placed in app.contactMethods matters.
|
|
||||||
// Add new ones to the end.
|
|
||||||
if discordEnabled {
|
|
||||||
app.discord, err = newDiscordDaemon(app)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedInitDiscord, err)
|
|
||||||
discordEnabled = false
|
|
||||||
} else {
|
|
||||||
app.debug.Println(lm.InitDiscord)
|
|
||||||
go app.discord.run()
|
|
||||||
defer app.discord.Shutdown()
|
|
||||||
app.contactMethods = append(app.contactMethods, app.discord)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if telegramEnabled {
|
if telegramEnabled {
|
||||||
app.telegram, err = newTelegramDaemon(app)
|
app.telegram, err = newTelegramDaemon(app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedInitTelegram, err)
|
app.err.Printf("Failed to authenticate with Telegram: %v", err)
|
||||||
telegramEnabled = false
|
telegramEnabled = false
|
||||||
} else {
|
} else {
|
||||||
app.debug.Println(lm.InitTelegram)
|
|
||||||
go app.telegram.run()
|
go app.telegram.run()
|
||||||
defer app.telegram.Shutdown()
|
defer app.telegram.Shutdown()
|
||||||
app.contactMethods = append(app.contactMethods, app.telegram)
|
}
|
||||||
|
}
|
||||||
|
if discordEnabled {
|
||||||
|
app.discord, err = newDiscordDaemon(app)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf("Failed to authenticate with Discord: %v", err)
|
||||||
|
discordEnabled = false
|
||||||
|
} else {
|
||||||
|
go app.discord.run()
|
||||||
|
defer app.discord.Shutdown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if matrixEnabled {
|
if matrixEnabled {
|
||||||
app.matrix, err = newMatrixDaemon(app)
|
app.matrix, err = newMatrixDaemon(app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedInitMatrix, err)
|
app.err.Printf("Failed to initialize Matrix daemon: %v", err)
|
||||||
matrixEnabled = false
|
matrixEnabled = false
|
||||||
} else {
|
} else {
|
||||||
app.debug.Println(lm.InitMatrix)
|
|
||||||
go app.matrix.run()
|
go app.matrix.run()
|
||||||
defer app.matrix.Shutdown()
|
defer app.matrix.Shutdown()
|
||||||
app.contactMethods = append(app.contactMethods, app.matrix)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -572,7 +558,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.storage.lang.SetupPath = "setup"
|
app.storage.lang.SetupPath = "setup"
|
||||||
err := app.storage.loadLangSetup(langFS)
|
err := app.storage.loadLangSetup(langFS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.info.Fatalf(lm.FailedLangLoad, err)
|
app.info.Fatalf("Failed to load language files: %+v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,14 +566,14 @@ func start(asDaemon, firstCall bool) {
|
|||||||
// workaround for potentially broken windows mime types
|
// workaround for potentially broken windows mime types
|
||||||
mime.AddExtensionType(".js", "application/javascript")
|
mime.AddExtensionType(".js", "application/javascript")
|
||||||
|
|
||||||
app.info.Println(lm.InitRouter)
|
app.info.Println("Initializing router")
|
||||||
router := app.loadRouter(address, debugMode)
|
router := app.loadRouter(address, debugMode)
|
||||||
app.info.Println(lm.LoadRoutes)
|
app.info.Println("Loading routes")
|
||||||
if !firstRun {
|
if !firstRun {
|
||||||
app.loadRoutes(router)
|
app.loadRoutes(router)
|
||||||
} else {
|
} else {
|
||||||
app.loadSetup(router)
|
app.loadSetup(router)
|
||||||
app.info.Printf(lm.LoadingSetup, address)
|
app.info.Printf("Loading setup @ %s", address)
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
||||||
@ -595,45 +581,45 @@ func start(asDaemon, firstCall bool) {
|
|||||||
key := app.config.Section("advanced").Key("tls_key").MustString("")
|
key := app.config.Section("advanced").Key("tls_key").MustString("")
|
||||||
if err := SRV.ListenAndServeTLS(cert, key); err != nil {
|
if err := SRV.ListenAndServeTLS(cert, key); err != nil {
|
||||||
filesToCheck := []string{cert, key}
|
filesToCheck := []string{cert, key}
|
||||||
fileNames := []string{lm.InvalidSSLCert, lm.InvalidSSLKey}
|
fileNames := []string{"Certificate", "Key"}
|
||||||
for i, v := range filesToCheck {
|
for i, v := range filesToCheck {
|
||||||
_, err := os.Stat(v)
|
_, err := os.Stat(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(fileNames[i], v, err)
|
app.err.Printf("SSL/TLS %s: %v\n", fileNames[i], err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == http.ErrServerClosed {
|
if err == http.ErrServerClosed {
|
||||||
app.err.Printf(lm.FailServeSSL, err)
|
app.err.Printf("Failure serving with SSL/TLS: %s", err)
|
||||||
} else {
|
} else {
|
||||||
app.err.Fatalf(lm.FailServeSSL, err)
|
app.err.Fatalf("Failure serving with SSL/TLS: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := SRV.ListenAndServe(); err != nil {
|
if err := SRV.ListenAndServe(); err != nil {
|
||||||
if err == http.ErrServerClosed {
|
if err == http.ErrServerClosed {
|
||||||
app.err.Printf(lm.FailServe, err)
|
app.err.Printf("Failure serving: %s", err)
|
||||||
} else {
|
} else {
|
||||||
app.err.Fatalf(lm.FailServe, err)
|
app.err.Fatalf("Failure serving: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if firstRun {
|
if firstRun {
|
||||||
app.info.Printf(lm.ServingSetup, address)
|
app.info.Printf("Loaded, visit %s to start.", address)
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf(lm.Serving, address)
|
app.info.Printf("Loaded @ %s", address)
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForRestart()
|
waitForRestart()
|
||||||
|
|
||||||
app.info.Printf(lm.QuitReceived)
|
app.info.Printf("Restart/Quit signal received, give me a second!")
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := SRV.Shutdown(ctx); err != nil {
|
if err := SRV.Shutdown(ctx); err != nil {
|
||||||
app.err.Fatalf(lm.FailedQuit, err)
|
app.err.Fatalf("Server shutdown error: %s", err)
|
||||||
}
|
}
|
||||||
app.info.Println(lm.Quit)
|
app.info.Println("Server shut down.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,7 +631,7 @@ func shutdown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) shutdown() {
|
func (app *appContext) shutdown() {
|
||||||
app.info.Println(lm.Quitting)
|
app.info.Println("Shutting down...")
|
||||||
shutdown()
|
shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -729,17 +715,15 @@ func printVersion() {
|
|||||||
fmt.Println(info("jfa-go version: %s (%s)%s\n", hiwhite(version), white(commit), tray))
|
fmt.Println(info("jfa-go version: %s (%s)%s\n", hiwhite(version), white(commit), tray))
|
||||||
}
|
}
|
||||||
|
|
||||||
const SYSTEMD_SERVICE = "jfa-go.service"
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
f, err := logOutput()
|
f, err := logOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf(lm.FailedLogging, err)
|
fmt.Printf("Failed to start logging: %v\n", err)
|
||||||
}
|
}
|
||||||
defer f()
|
defer f()
|
||||||
printVersion()
|
printVersion()
|
||||||
SOCK = filepath.Join(temp, SOCK)
|
SOCK = filepath.Join(temp, SOCK)
|
||||||
fmt.Printf(lm.SocketPath+"\n", SOCK)
|
fmt.Println("Socket:", SOCK)
|
||||||
if flagPassed("test") {
|
if flagPassed("test") {
|
||||||
TEST = true
|
TEST = true
|
||||||
}
|
}
|
||||||
@ -768,26 +752,24 @@ func main() {
|
|||||||
} else if flagPassed("stop") {
|
} else if flagPassed("stop") {
|
||||||
con, err := net.Dial("unix", SOCK)
|
con, err := net.Dial("unix", SOCK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf(lm.FailedSocketConnect+"\n", SOCK, err)
|
fmt.Printf("Couldn't dial socket %s, are you sure jfa-go is running?\n", SOCK)
|
||||||
fmt.Println(lm.SocketCheckRunning)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
_, err = con.Write([]byte("stop"))
|
_, err = con.Write([]byte("stop"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf(lm.FailedSocketWrite+"\n", SOCK, err)
|
fmt.Printf("Couldn't send command to socket %s, are you sure jfa-go is running?\n", SOCK)
|
||||||
fmt.Println(lm.SocketCheckRunning)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Println(lm.SocketWrite)
|
fmt.Println("Sent.")
|
||||||
} else if flagPassed("daemon") {
|
} else if flagPassed("daemon") {
|
||||||
start(true, true)
|
start(true, true)
|
||||||
} else if flagPassed("systemd") {
|
} else if flagPassed("systemd") {
|
||||||
service, err := fs.ReadFile(localFS, SYSTEMD_SERVICE)
|
service, err := fs.ReadFile(localFS, "jfa-go.service")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf(lm.FailedReading+"\n", SYSTEMD_SERVICE, err)
|
fmt.Printf("Couldn't read jfa-go.service: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
absPath, err := os.Executable()
|
absPath, err := filepath.Abs(os.Args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
absPath = os.Args[0]
|
absPath = os.Args[0]
|
||||||
}
|
}
|
||||||
@ -798,13 +780,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
service = []byte(strings.Replace(string(service), "{executable}", command, 1))
|
service = []byte(strings.Replace(string(service), "{executable}", command, 1))
|
||||||
err = os.WriteFile(SYSTEMD_SERVICE, service, 0666)
|
err = os.WriteFile("jfa-go.service", service, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf(lm.FailedWriting+"\n", SYSTEMD_SERVICE, err)
|
fmt.Printf("Couldn't write jfa-go.service: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Println(info(`If you want to execute jfa-go with special arguments, re-run this command with them.
|
fmt.Println(info(`If you want to execute jfa-go with special arguments, re-run this command with them.
|
||||||
Move the newly created SYSTEMD_SERVICE file to ~/.config/systemd/user (Creating it if necessary).
|
Move the newly created "jfa-go.service" file to ~/.config/systemd/user (Creating it if necessary).
|
||||||
Then run "systemctl --user daemon-reload".
|
Then run "systemctl --user daemon-reload".
|
||||||
You can then run:
|
You can then run:
|
||||||
|
|
||||||
|
71
matrix.go
71
matrix.go
@ -6,7 +6,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gomarkdown/markdown"
|
"github.com/gomarkdown/markdown"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
@ -32,6 +31,15 @@ type UnverifiedUser struct {
|
|||||||
User *MatrixUser
|
User *MatrixUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatrixUser struct {
|
||||||
|
RoomID string
|
||||||
|
Encrypted bool
|
||||||
|
UserID string
|
||||||
|
Lang string
|
||||||
|
Contact bool
|
||||||
|
JellyfinID string `badgerhold:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
var matrixFilter = mautrix.Filter{
|
var matrixFilter = mautrix.Filter{
|
||||||
Room: mautrix.RoomFilter{
|
Room: mautrix.RoomFilter{
|
||||||
Timeline: mautrix.FilterPart{
|
Timeline: mautrix.FilterPart{
|
||||||
@ -110,13 +118,13 @@ func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string
|
|||||||
|
|
||||||
func (d *MatrixDaemon) run() {
|
func (d *MatrixDaemon) run() {
|
||||||
startTime := d.start
|
startTime := d.start
|
||||||
d.app.info.Println(lm.StartDaemon, lm.Matrix)
|
d.app.info.Println("Starting Matrix bot daemon")
|
||||||
syncer := d.bot.Syncer.(*mautrix.DefaultSyncer)
|
syncer := d.bot.Syncer.(*mautrix.DefaultSyncer)
|
||||||
HandleSyncerCrypto(startTime, d, syncer)
|
HandleSyncerCrypto(startTime, d, syncer)
|
||||||
syncer.OnEventType(event.EventMessage, d.handleMessage)
|
syncer.OnEventType(event.EventMessage, d.handleMessage)
|
||||||
|
|
||||||
if err := d.bot.Sync(); err != nil {
|
if err := d.bot.Sync(); err != nil {
|
||||||
d.app.err.Printf(lm.FailedSyncMatrix, err)
|
d.app.err.Printf("Matrix sync failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +170,7 @@ func (d *MatrixDaemon) commandLang(evt *event.Event, code, lang string) {
|
|||||||
list,
|
list,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedReply, lm.Matrix, evt.Sender, err)
|
d.app.err.Printf("Matrix: Failed to send message to \"%s\": %v", evt.Sender, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -195,7 +203,7 @@ func (d *MatrixDaemon) CreateRoom(userID string) (roomID id.RoomID, encrypted bo
|
|||||||
func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
||||||
roomID, encrypted, err := d.CreateRoom(userID)
|
roomID, encrypted, err := d.CreateRoom(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedCreateMatrixRoom, userID, err)
|
d.app.err.Printf("Failed to create room for user \"%s\": %v", userID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lang := "en-us"
|
lang := "en-us"
|
||||||
@ -218,7 +226,7 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
|||||||
roomID,
|
roomID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedMessage, lm.Matrix, userID, err)
|
d.app.err.Printf("Matrix: Failed to send welcome message to \"%s\": %v", userID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ok = true
|
ok = true
|
||||||
@ -268,57 +276,6 @@ func (d *MatrixDaemon) UserExists(userID string) bool {
|
|||||||
return err != nil || c > 0
|
return err != nil || c > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exists returns whether or not the given user exists.
|
|
||||||
func (d *MatrixDaemon) Exists(user ContactMethodUser) bool {
|
|
||||||
return d.UserExists(user.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// User enters ID on sign-up, a PIN is sent to them. They enter it on sign-up.
|
// User enters ID on sign-up, a PIN is sent to them. They enter it on sign-up.
|
||||||
|
|
||||||
// Message the user first, to avoid E2EE by default
|
// Message the user first, to avoid E2EE by default
|
||||||
|
|
||||||
func (d *MatrixDaemon) PIN(req newUserDTO) string { return req.MatrixPIN }
|
|
||||||
|
|
||||||
func (d *MatrixDaemon) Name() string { return lm.Matrix }
|
|
||||||
|
|
||||||
func (d *MatrixDaemon) Required() bool {
|
|
||||||
return d.app.config.Section("telegram").Key("required").MustBool(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *MatrixDaemon) UniqueRequired() bool {
|
|
||||||
return d.app.config.Section("telegram").Key("require_unique").MustBool(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenVerified returns whether or not a token with the given PIN has been verified, and the token itself.
|
|
||||||
func (d *MatrixDaemon) TokenVerified(pin string) (token UnverifiedUser, ok bool) {
|
|
||||||
token, ok = d.tokens[pin]
|
|
||||||
// delete(t.verifiedTokens, pin)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteVerifiedToken removes the token with the given PIN.
|
|
||||||
func (d *MatrixDaemon) DeleteVerifiedToken(PIN string) {
|
|
||||||
delete(d.tokens, PIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *MatrixDaemon) UserVerified(PIN string) (ContactMethodUser, bool) {
|
|
||||||
token, ok := d.TokenVerified(PIN)
|
|
||||||
if !ok {
|
|
||||||
return &MatrixUser{}, false
|
|
||||||
}
|
|
||||||
return token.User, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *MatrixDaemon) PostVerificationTasks(string, ContactMethodUser) error { return nil }
|
|
||||||
|
|
||||||
func (m *MatrixUser) Name() string { return m.UserID }
|
|
||||||
func (m *MatrixUser) SetMethodID(id any) { m.UserID = id.(string) }
|
|
||||||
func (m *MatrixUser) MethodID() any { return m.UserID }
|
|
||||||
func (m *MatrixUser) SetJellyfin(id string) { m.JellyfinID = id }
|
|
||||||
func (m *MatrixUser) Jellyfin() string { return m.JellyfinID }
|
|
||||||
func (m *MatrixUser) SetAllowContactFromDTO(req newUserDTO) { m.Contact = req.MatrixContact }
|
|
||||||
func (m *MatrixUser) SetAllowContact(contact bool) { m.Contact = contact }
|
|
||||||
func (m *MatrixUser) AllowContact() bool { return m.Contact }
|
|
||||||
func (m *MatrixUser) Store(st *Storage) {
|
|
||||||
st.SetMatrixKey(m.Jellyfin(), *m)
|
|
||||||
}
|
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
//go:build e2ee
|
|
||||||
// +build e2ee
|
// +build e2ee
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
"maunium.net/go/mautrix/crypto"
|
"maunium.net/go/mautrix/crypto"
|
||||||
"maunium.net/go/mautrix/event"
|
"maunium.net/go/mautrix/event"
|
||||||
@ -68,22 +65,22 @@ type olmLogger struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o olmLogger) Error(message string, args ...interface{}) {
|
func (o olmLogger) Error(message string, args ...interface{}) {
|
||||||
o.app.err.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, args))
|
o.app.err.Printf("OLM: "+message+"\n", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o olmLogger) Warn(message string, args ...interface{}) {
|
func (o olmLogger) Warn(message string, args ...interface{}) {
|
||||||
o.app.info.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, args))
|
o.app.info.Printf("OLM: "+message+"\n", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o olmLogger) Debug(message string, args ...interface{}) {
|
func (o olmLogger) Debug(message string, args ...interface{}) {
|
||||||
o.app.debug.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, args))
|
o.app.debug.Printf("OLM: "+message+"\n", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o olmLogger) Trace(message string, args ...interface{}) {
|
func (o olmLogger) Trace(message string, args ...interface{}) {
|
||||||
if strings.HasPrefix(message, "Got membership state event") {
|
if strings.HasPrefix(message, "Got membership state event") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
o.app.debug.Printf(lm.MatrixOlmTracelog, fmt.Sprintf(message, args))
|
o.app.debug.Printf("OLM [TRACE]: "+message+"\n", args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitMatrixCrypto(d *MatrixDaemon) (err error) {
|
func InitMatrixCrypto(d *MatrixDaemon) (err error) {
|
||||||
@ -158,7 +155,7 @@ func HandleSyncerCrypto(startTime int64, d *MatrixDaemon, syncer *mautrix.Defaul
|
|||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf(lm.FailedDecryptMatrixMessage, err)
|
d.app.err.Printf("Failed to decrypt Matrix message: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.handleMessage(source, decrypted)
|
d.handleMessage(source, decrypted)
|
||||||
@ -183,7 +180,7 @@ func EncryptRoom(d *MatrixDaemon, room *mautrix.RespCreateRoom, userID id.UserID
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
encrypted = true
|
encrypted = true
|
||||||
} else {
|
} else {
|
||||||
d.app.debug.Printf(lm.FailedEnableMatrixEncryption, room.RoomID, err)
|
d.app.debug.Printf("Matrix: Failed to enable encryption in room: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.isEncrypted[room.RoomID] = encrypted
|
d.isEncrypted[room.RoomID] = encrypted
|
||||||
|
@ -9,8 +9,6 @@ import (
|
|||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NOTE: This is the one file where log messages are not part of logmessages/logmessages.go
|
|
||||||
|
|
||||||
func runMigrations(app *appContext) {
|
func runMigrations(app *appContext) {
|
||||||
migrateProfiles(app)
|
migrateProfiles(app)
|
||||||
migrateBootstrap(app)
|
migrateBootstrap(app)
|
||||||
|
@ -31,7 +31,7 @@ type newUserDTO struct {
|
|||||||
|
|
||||||
type newUserResponse struct {
|
type newUserResponse struct {
|
||||||
User bool `json:"user" binding:"required"` // Whether user was created successfully
|
User bool `json:"user" binding:"required"` // Whether user was created successfully
|
||||||
Email bool `json:"email"` // Whether welcome email was successfully sent (always true if feature is disabled)
|
Email bool `json:"email"` // Whether welcome email was successfully sent (always true if feature is disabled
|
||||||
Error string `json:"error"` // Optional error message.
|
Error string `json:"error"` // Optional error message.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
34
pwreset.go
34
pwreset.go
@ -8,7 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenInternalReset generates a local password reset PIN, for use with the PWR option on the Admin page.
|
// GenInternalReset generates a local password reset PIN, for use with the PWR option on the Admin page.
|
||||||
@ -40,16 +39,16 @@ func (app *appContext) GenResetLink(pin string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) StartPWR() {
|
func (app *appContext) StartPWR() {
|
||||||
app.info.Println(lm.StartDaemon, "PWR")
|
app.info.Println("Starting password reset daemon")
|
||||||
path := app.config.Section("password_resets").Key("watch_directory").String()
|
path := app.config.Section("password_resets").Key("watch_directory").String()
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
app.err.Printf(lm.FailedStartDaemon, "PWR", fmt.Sprintf(lm.PathNotFound, path))
|
app.err.Printf("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
watcher, err := fsnotify.NewWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
app.err.Printf("Couldn't initialise password reset daemon")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer watcher.Close()
|
defer watcher.Close()
|
||||||
@ -57,7 +56,7 @@ func (app *appContext) StartPWR() {
|
|||||||
go pwrMonitor(app, watcher)
|
go pwrMonitor(app, watcher)
|
||||||
err = watcher.Add(path)
|
err = watcher.Add(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
app.err.Printf("Failed to start password reset daemon: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForRestart()
|
waitForRestart()
|
||||||
@ -85,36 +84,43 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
|||||||
var pwr PasswordReset
|
var pwr PasswordReset
|
||||||
data, err := os.ReadFile(event.Name)
|
data, err := os.ReadFile(event.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.debug.Printf(lm.FailedReading, event.Name, err)
|
app.debug.Printf("PWR: Failed to read file: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(data, &pwr)
|
err = json.Unmarshal(data, &pwr)
|
||||||
if len(pwr.Pin) == 0 || err != nil {
|
if len(pwr.Pin) == 0 || err != nil {
|
||||||
app.debug.Printf(lm.FailedReading, event.Name, err)
|
app.debug.Printf("PWR: Failed to read PIN: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
app.info.Printf("New password reset for user \"%s\"", pwr.Username)
|
app.info.Printf("New password reset for user \"%s\"", pwr.Username)
|
||||||
if currentTime := time.Now(); pwr.Expiry.After(currentTime) {
|
if currentTime := time.Now(); pwr.Expiry.After(currentTime) {
|
||||||
user, status, err := app.jf.UserByName(pwr.Username, false)
|
user, status, err := app.jf.UserByName(pwr.Username, false)
|
||||||
if !(status == 200 || status == 204) || err != nil || user.ID == "" {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, pwr.Username, lm.Jellyfin, err)
|
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||||
|
app.debug.Printf("Error: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uid := user.ID
|
uid := user.ID
|
||||||
|
if uid == "" {
|
||||||
|
app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username)
|
||||||
|
return
|
||||||
|
}
|
||||||
name := app.getAddressOrName(uid)
|
name := app.getAddressOrName(uid)
|
||||||
if name != "" {
|
if name != "" {
|
||||||
msg, err := app.email.constructReset(pwr, app, false)
|
msg, err := app.email.constructReset(pwr, app, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
|
app.err.Printf("Failed to construct password reset message for \"%s\"", pwr.Username)
|
||||||
|
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||||
} else if err := app.sendByID(msg, uid); err != nil {
|
} else if err := app.sendByID(msg, uid); err != nil {
|
||||||
app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, name, err)
|
app.err.Printf("Failed to send password reset message to \"%s\"", name)
|
||||||
|
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||||
} else {
|
} else {
|
||||||
app.err.Printf(lm.SentPWRMessage, pwr.Username, name)
|
app.info.Printf("Sent password reset message to \"%s\"", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.err.Printf(lm.PWRExpired, pwr.Username, pwr.Expiry)
|
app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -122,7 +128,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
app.err.Printf("Password reset daemon: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "fmt"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (app *appContext) HardRestart() error {
|
func (app *appContext) HardRestart() error {
|
||||||
return fmt.Errorf(lm.FailedHardRestartWindows)
|
return fmt.Errorf("hard restarts not available on windows")
|
||||||
}
|
}
|
||||||
|
19
router.go
19
router.go
@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/gin-contrib/pprof"
|
"github.com/gin-contrib/pprof"
|
||||||
"github.com/gin-contrib/static"
|
"github.com/gin-contrib/static"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
swaggerFiles "github.com/swaggo/files"
|
swaggerFiles "github.com/swaggo/files"
|
||||||
ginSwagger "github.com/swaggo/gin-swagger"
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
)
|
)
|
||||||
@ -22,17 +21,17 @@ func (app *appContext) loadHTML(router *gin.Engine) {
|
|||||||
templatePath := "html"
|
templatePath := "html"
|
||||||
htmlFiles, err := fs.ReadDir(localFS, templatePath)
|
htmlFiles, err := fs.ReadDir(localFS, templatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedReading, templatePath, err)
|
app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadInternal := []string{}
|
loadInternal := []string{}
|
||||||
loadExternal := []string{}
|
loadExternal := []string{}
|
||||||
for _, f := range htmlFiles {
|
for _, f := range htmlFiles {
|
||||||
if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) {
|
||||||
app.debug.Printf(lm.UseDefaultHTML, f.Name())
|
app.debug.Printf("Using default \"%s\"", f.Name())
|
||||||
loadInternal = append(loadInternal, FSJoin(templatePath, f.Name()))
|
loadInternal = append(loadInternal, FSJoin(templatePath, f.Name()))
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf(lm.UseCustomHTML, f.Name())
|
app.info.Printf("Using custom \"%s\"", f.Name())
|
||||||
loadExternal = append(loadExternal, filepath.Join(filepath.Join(customPath, f.Name())))
|
loadExternal = append(loadExternal, filepath.Join(filepath.Join(customPath, f.Name())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,13 +39,13 @@ func (app *appContext) loadHTML(router *gin.Engine) {
|
|||||||
if len(loadInternal) != 0 {
|
if len(loadInternal) != 0 {
|
||||||
tmpl, err = template.ParseFS(localFS, loadInternal...)
|
tmpl, err = template.ParseFS(localFS, loadInternal...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedLoadTemplates, lm.Internal, err)
|
app.err.Fatalf("Failed to load templates: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(loadExternal) != 0 {
|
if len(loadExternal) != 0 {
|
||||||
tmpl, err = tmpl.ParseFiles(loadExternal...)
|
tmpl, err = tmpl.ParseFiles(loadExternal...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedLoadTemplates, lm.External, err)
|
app.err.Fatalf("Failed to load external templates: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
router.SetHTMLTemplate(tmpl)
|
router.SetHTMLTemplate(tmpl)
|
||||||
@ -97,7 +96,7 @@ func (app *appContext) loadRouter(address string, debug bool) *gin.Engine {
|
|||||||
router.Use(static.Serve("/", app.webFS))
|
router.Use(static.Serve("/", app.webFS))
|
||||||
router.NoRoute(app.NoRouteHandler)
|
router.NoRoute(app.NoRouteHandler)
|
||||||
if *PPROF {
|
if *PPROF {
|
||||||
app.debug.Println(lm.RegisterPprof)
|
app.debug.Println("Loading pprof")
|
||||||
pprof.Register(router)
|
pprof.Register(router)
|
||||||
}
|
}
|
||||||
SRV = &http.Server{
|
SRV = &http.Server{
|
||||||
@ -135,7 +134,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
router.GET(p+"/lang/:page/:file", app.ServeLang)
|
router.GET(p+"/lang/:page/:file", app.ServeLang)
|
||||||
router.GET(p+"/token/login", app.getTokenLogin)
|
router.GET(p+"/token/login", app.getTokenLogin)
|
||||||
router.GET(p+"/token/refresh", app.getTokenRefresh)
|
router.GET(p+"/token/refresh", app.getTokenRefresh)
|
||||||
router.POST(p+"/newUser", app.NewUserFromInvite)
|
router.POST(p+"/newUser", app.NewUser)
|
||||||
router.Use(static.Serve(p+"/invite/", app.webFS))
|
router.Use(static.Serve(p+"/invite/", app.webFS))
|
||||||
router.GET(p+"/invite/:invCode", app.InviteProxy)
|
router.GET(p+"/invite/:invCode", app.InviteProxy)
|
||||||
if app.config.Section("captcha").Key("enabled").MustBool(false) {
|
if app.config.Section("captcha").Key("enabled").MustBool(false) {
|
||||||
@ -166,7 +165,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if *SWAGGER {
|
if *SWAGGER {
|
||||||
app.info.Print(warning(lm.SwaggerWarning))
|
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
||||||
for _, p := range routePrefixes {
|
for _, p := range routePrefixes {
|
||||||
router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
}
|
}
|
||||||
@ -182,7 +181,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
router.POST(p+"/logout", app.Logout)
|
router.POST(p+"/logout", app.Logout)
|
||||||
api.DELETE(p+"/users", app.DeleteUsers)
|
api.DELETE(p+"/users", app.DeleteUsers)
|
||||||
api.GET(p+"/users", app.GetUsers)
|
api.GET(p+"/users", app.GetUsers)
|
||||||
api.POST(p+"/users", app.NewUserFromAdmin)
|
api.POST(p+"/users", app.NewUserAdmin)
|
||||||
api.POST(p+"/users/extend", app.ExtendExpiry)
|
api.POST(p+"/users/extend", app.ExtendExpiry)
|
||||||
api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
|
api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
|
||||||
api.POST(p+"/users/enable", app.EnableDisableUsers)
|
api.POST(p+"/users/enable", app.EnableDisableUsers)
|
||||||
|
15
setup.go
15
setup.go
@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/hrfee/jfa-go/easyproxy"
|
"github.com/hrfee/jfa-go/easyproxy"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -105,7 +104,7 @@ func (app *appContext) TestJF(gc *gin.Context) {
|
|||||||
case 404:
|
case 404:
|
||||||
msg = "error404"
|
msg = "error404"
|
||||||
}
|
}
|
||||||
app.err.Printf(lm.FailedAuthJellyfin, req.Server, status, err)
|
app.info.Printf("Auth failed with code %d (%s)", status, err)
|
||||||
if msg != "" {
|
if msg != "" {
|
||||||
respond(status, msg, gc)
|
respond(status, msg, gc)
|
||||||
} else {
|
} else {
|
||||||
@ -152,20 +151,16 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
|
|||||||
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
||||||
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
||||||
patchLang(&lang.StartPage, &fallback.StartPage, &english.StartPage)
|
patchLang(&lang.StartPage, &fallback.StartPage, &english.StartPage)
|
||||||
patchLang(&lang.EndPage, &fallback.EndPage, &english.EndPage)
|
|
||||||
patchLang(&lang.General, &fallback.General, &english.General)
|
|
||||||
patchLang(&lang.Updates, &fallback.Updates, &english.Updates)
|
patchLang(&lang.Updates, &fallback.Updates, &english.Updates)
|
||||||
patchLang(&lang.Proxy, &fallback.Proxy, &english.Proxy)
|
patchLang(&lang.Proxy, &fallback.Proxy, &english.Proxy)
|
||||||
|
patchLang(&lang.EndPage, &fallback.EndPage, &english.EndPage)
|
||||||
patchLang(&lang.Language, &fallback.Language, &english.Language)
|
patchLang(&lang.Language, &fallback.Language, &english.Language)
|
||||||
patchLang(&lang.Login, &fallback.Login, &english.Login)
|
patchLang(&lang.Login, &fallback.Login, &english.Login)
|
||||||
patchLang(&lang.JellyfinEmby, &fallback.JellyfinEmby, &english.JellyfinEmby)
|
patchLang(&lang.JellyfinEmby, &fallback.JellyfinEmby, &english.JellyfinEmby)
|
||||||
patchLang(&lang.Ombi, &fallback.Ombi, &english.Ombi)
|
|
||||||
patchLang(&lang.Jellyseerr, &fallback.Jellyseerr, &english.Jellyseerr)
|
|
||||||
patchLang(&lang.Email, &fallback.Email, &english.Email)
|
patchLang(&lang.Email, &fallback.Email, &english.Email)
|
||||||
patchLang(&lang.Messages, &fallback.Messages, &english.Messages)
|
patchLang(&lang.Messages, &fallback.Messages, &english.Messages)
|
||||||
patchLang(&lang.Notifications, &fallback.Notifications, &english.Notifications)
|
patchLang(&lang.Notifications, &fallback.Notifications, &english.Notifications)
|
||||||
patchLang(&lang.UserPage, &fallback.UserPage, &english.UserPage)
|
patchLang(&lang.UserPage, &fallback.UserPage, &english.UserPage)
|
||||||
patchLang(&lang.WelcomeEmails, &fallback.WelcomeEmails, &english.WelcomeEmails)
|
|
||||||
patchLang(&lang.PasswordResets, &fallback.PasswordResets, &english.PasswordResets)
|
patchLang(&lang.PasswordResets, &fallback.PasswordResets, &english.PasswordResets)
|
||||||
patchLang(&lang.InviteEmails, &fallback.InviteEmails, &english.InviteEmails)
|
patchLang(&lang.InviteEmails, &fallback.InviteEmails, &english.InviteEmails)
|
||||||
patchLang(&lang.PasswordValidation, &fallback.PasswordValidation, &english.PasswordValidation)
|
patchLang(&lang.PasswordValidation, &fallback.PasswordValidation, &english.PasswordValidation)
|
||||||
@ -175,20 +170,16 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
|
|||||||
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
||||||
patchLang(&lang.Strings, &english.Strings)
|
patchLang(&lang.Strings, &english.Strings)
|
||||||
patchLang(&lang.StartPage, &english.StartPage)
|
patchLang(&lang.StartPage, &english.StartPage)
|
||||||
patchLang(&lang.EndPage, &english.EndPage)
|
|
||||||
patchLang(&lang.General, &english.General)
|
|
||||||
patchLang(&lang.Updates, &english.Updates)
|
patchLang(&lang.Updates, &english.Updates)
|
||||||
patchLang(&lang.Proxy, &english.Proxy)
|
patchLang(&lang.Proxy, &english.Proxy)
|
||||||
|
patchLang(&lang.EndPage, &english.EndPage)
|
||||||
patchLang(&lang.Language, &english.Language)
|
patchLang(&lang.Language, &english.Language)
|
||||||
patchLang(&lang.Login, &english.Login)
|
patchLang(&lang.Login, &english.Login)
|
||||||
patchLang(&lang.JellyfinEmby, &english.JellyfinEmby)
|
patchLang(&lang.JellyfinEmby, &english.JellyfinEmby)
|
||||||
patchLang(&lang.Ombi, &english.Ombi)
|
|
||||||
patchLang(&lang.Jellyseerr, &english.Jellyseerr)
|
|
||||||
patchLang(&lang.Email, &english.Email)
|
patchLang(&lang.Email, &english.Email)
|
||||||
patchLang(&lang.Messages, &english.Messages)
|
patchLang(&lang.Messages, &english.Messages)
|
||||||
patchLang(&lang.Notifications, &english.Notifications)
|
patchLang(&lang.Notifications, &english.Notifications)
|
||||||
patchLang(&lang.UserPage, &english.UserPage)
|
patchLang(&lang.UserPage, &english.UserPage)
|
||||||
patchLang(&lang.WelcomeEmails, &english.WelcomeEmails)
|
|
||||||
patchLang(&lang.PasswordResets, &english.PasswordResets)
|
patchLang(&lang.PasswordResets, &english.PasswordResets)
|
||||||
patchLang(&lang.InviteEmails, &english.InviteEmails)
|
patchLang(&lang.InviteEmails, &english.InviteEmails)
|
||||||
patchLang(&lang.PasswordValidation, &english.PasswordValidation)
|
patchLang(&lang.PasswordValidation, &english.PasswordValidation)
|
||||||
|
283
static/banner.svg
Normal file
283
static/banner.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 58 KiB |
83
storage.go
83
storage.go
@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@ -14,7 +13,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/hrfee/jfa-go/jellyseerr"
|
"github.com/hrfee/jfa-go/jellyseerr"
|
||||||
"github.com/hrfee/jfa-go/logger"
|
"github.com/hrfee/jfa-go/logger"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
@ -177,10 +175,10 @@ func (app *appContext) ConnectDB() {
|
|||||||
opts.ValueDir = app.storage.db_path
|
opts.ValueDir = app.storage.db_path
|
||||||
db, err := badgerhold.Open(opts)
|
db, err := badgerhold.Open(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf(lm.FailedConnectDB, app.storage.db_path, err)
|
app.err.Fatalf("Failed to open db \"%s\": %v", app.storage.db_path, err)
|
||||||
}
|
}
|
||||||
app.storage.db = db
|
app.storage.db = db
|
||||||
app.info.Printf(lm.ConnectDB, app.storage.db_path)
|
app.info.Printf("Connected to DB \"%s\"", app.storage.db_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEmails returns a copy of the store.
|
// GetEmails returns a copy of the store.
|
||||||
@ -510,15 +508,6 @@ func (st *Storage) GetDefaultProfile() Profile {
|
|||||||
return defaultProfile
|
return defaultProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustGetProfileKey returns the profile at key k, or if missing, the default profile.
|
|
||||||
func (st *Storage) MustGetProfileKey(k string) Profile {
|
|
||||||
p, ok := st.GetProfileKey(k)
|
|
||||||
if !ok {
|
|
||||||
p = st.GetDefaultProfile()
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCustomContent returns a copy of the store.
|
// GetCustomContent returns a copy of the store.
|
||||||
func (st *Storage) GetCustomContent() []CustomContent {
|
func (st *Storage) GetCustomContent() []CustomContent {
|
||||||
result := []CustomContent{}
|
result := []CustomContent{}
|
||||||
@ -594,35 +583,12 @@ func (st *Storage) DeleteActivityKey(k string) {
|
|||||||
st.db.Delete(k, Activity{})
|
st.db.Delete(k, Activity{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThirdPartyService interface {
|
type TelegramUser struct {
|
||||||
// ok implies user imported, err can be any issue that occurs during
|
JellyfinID string `badgerhold:"key"`
|
||||||
ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool)
|
ChatID int64 `badgerhold:"index"`
|
||||||
AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error)
|
Username string `badgerhold:"index"`
|
||||||
Enabled(app *appContext, profile *Profile) bool
|
Lang string
|
||||||
Name() string
|
Contact bool // Whether to contact through telegram or not
|
||||||
}
|
|
||||||
|
|
||||||
type ContactMethodLinker interface {
|
|
||||||
PIN(req newUserDTO) string
|
|
||||||
Name() string
|
|
||||||
Required() bool
|
|
||||||
UniqueRequired() bool
|
|
||||||
UserVerified(PIN string) (ContactMethodUser, bool)
|
|
||||||
PostVerificationTasks(PIN string, u ContactMethodUser) error
|
|
||||||
DeleteVerifiedToken(PIN string)
|
|
||||||
Exists(ContactMethodUser) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContactMethodUser interface {
|
|
||||||
SetMethodID(id any)
|
|
||||||
MethodID() any
|
|
||||||
Name() string
|
|
||||||
SetJellyfin(id string)
|
|
||||||
Jellyfin() string
|
|
||||||
SetAllowContactFromDTO(req newUserDTO)
|
|
||||||
SetAllowContact(contact bool)
|
|
||||||
AllowContact() bool
|
|
||||||
Store(st *Storage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DiscordUser struct {
|
type DiscordUser struct {
|
||||||
@ -635,24 +601,6 @@ type DiscordUser struct {
|
|||||||
JellyfinID string `json:"-" badgerhold:"key"`
|
JellyfinID string `json:"-" badgerhold:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TelegramUser struct {
|
|
||||||
TelegramVerifiedToken
|
|
||||||
JellyfinID string `badgerhold:"key"`
|
|
||||||
ChatID int64 `badgerhold:"index"`
|
|
||||||
Username string `badgerhold:"index"`
|
|
||||||
Lang string
|
|
||||||
Contact bool // Whether to contact through telegram or not
|
|
||||||
}
|
|
||||||
|
|
||||||
type MatrixUser struct {
|
|
||||||
RoomID string
|
|
||||||
Encrypted bool
|
|
||||||
UserID string
|
|
||||||
Lang string
|
|
||||||
Contact bool
|
|
||||||
JellyfinID string `badgerhold:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EmailAddress struct {
|
type EmailAddress struct {
|
||||||
Addr string `badgerhold:"index"`
|
Addr string `badgerhold:"index"`
|
||||||
Label string // User Label.
|
Label string // User Label.
|
||||||
@ -737,16 +685,6 @@ type Invite struct {
|
|||||||
UseReferralExpiry bool `json:"use_referral_expiry"`
|
UseReferralExpiry bool `json:"use_referral_expiry"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (invite Invite) Source() (ActivitySource, string) {
|
|
||||||
sourceType := ActivityAnon
|
|
||||||
source := ""
|
|
||||||
if invite.ReferrerJellyfinID != "" {
|
|
||||||
sourceType = ActivityUser
|
|
||||||
source = invite.ReferrerJellyfinID
|
|
||||||
}
|
|
||||||
return sourceType, source
|
|
||||||
}
|
|
||||||
|
|
||||||
type Captcha struct {
|
type Captcha struct {
|
||||||
Answer string
|
Answer string
|
||||||
Image []byte // image/png
|
Image []byte // image/png
|
||||||
@ -780,27 +718,22 @@ type Lang struct {
|
|||||||
func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
||||||
err = st.loadLangCommon(filesystems...)
|
err = st.loadLangCommon(filesystems...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("common: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = st.loadLangAdmin(filesystems...)
|
err = st.loadLangAdmin(filesystems...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("admin: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = st.loadLangEmail(filesystems...)
|
err = st.loadLangEmail(filesystems...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("email: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = st.loadLangUser(filesystems...)
|
err = st.loadLangUser(filesystems...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("user: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = st.loadLangPWR(filesystems...)
|
err = st.loadLangPWR(filesystems...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("pwr: %v", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = st.loadLangTelegram(filesystems...)
|
err = st.loadLangTelegram(filesystems...)
|
||||||
|
73
telegram.go
73
telegram.go
@ -7,7 +7,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/timshannon/badgerhold/v4"
|
"github.com/timshannon/badgerhold/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,27 +15,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TelegramVerifiedToken struct {
|
type TelegramVerifiedToken struct {
|
||||||
JellyfinID string `badgerhold:"key"` // optional, for ensuring a user-requested change is only accessed by them.
|
ChatID int64
|
||||||
ChatID int64 `badgerhold:"index"`
|
Username string
|
||||||
Username string `badgerhold:"index"`
|
JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
|
||||||
}
|
|
||||||
|
|
||||||
func (tv TelegramVerifiedToken) ToUser() *TelegramUser {
|
|
||||||
return &TelegramUser{
|
|
||||||
TelegramVerifiedToken: tv,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TelegramVerifiedToken) Name() string { return t.Username }
|
|
||||||
func (t *TelegramVerifiedToken) SetMethodID(id any) { t.ChatID = id.(int64) }
|
|
||||||
func (t *TelegramVerifiedToken) MethodID() any { return t.ChatID }
|
|
||||||
func (t *TelegramVerifiedToken) SetJellyfin(id string) { t.JellyfinID = id }
|
|
||||||
func (t *TelegramVerifiedToken) Jellyfin() string { return t.JellyfinID }
|
|
||||||
func (t *TelegramUser) SetAllowContactFromDTO(req newUserDTO) { t.Contact = req.TelegramContact }
|
|
||||||
func (t *TelegramUser) SetAllowContact(contact bool) { t.Contact = contact }
|
|
||||||
func (t *TelegramUser) AllowContact() bool { return t.Contact }
|
|
||||||
func (t *TelegramUser) Store(st *Storage) {
|
|
||||||
st.SetTelegramKey(t.Jellyfin(), *t)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifToken stores details about a pending user verification token.
|
// VerifToken stores details about a pending user verification token.
|
||||||
@ -115,12 +96,12 @@ func (t *TelegramDaemon) NewAssignedAuthToken(id string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *TelegramDaemon) run() {
|
func (t *TelegramDaemon) run() {
|
||||||
t.app.info.Println(lm.StartDaemon, lm.Telegram)
|
t.app.info.Println("Starting Telegram bot daemon")
|
||||||
u := tg.NewUpdate(0)
|
u := tg.NewUpdate(0)
|
||||||
u.Timeout = 60
|
u.Timeout = 60
|
||||||
updates, err := t.bot.GetUpdatesChan(u)
|
updates, err := t.bot.GetUpdatesChan(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.app.err.Printf(lm.FailedStartDaemon, lm.Telegram, err)
|
t.app.err.Printf("Failed to start Telegram daemon: %v", err)
|
||||||
telegramEnabled = false
|
telegramEnabled = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -218,7 +199,7 @@ func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang strin
|
|||||||
content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"})
|
content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"})
|
||||||
err := t.Reply(upd, content)
|
err := t.Reply(upd, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err)
|
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,7 +211,7 @@ func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string
|
|||||||
}
|
}
|
||||||
err := t.Reply(upd, list)
|
err := t.Reply(upd, list)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err)
|
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -251,14 +232,14 @@ func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string)
|
|||||||
if !ok || time.Now().After(token.Expiry) {
|
if !ok || time.Now().After(token.Expiry) {
|
||||||
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
|
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err)
|
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||||
}
|
}
|
||||||
delete(t.tokens, upd.Message.Text)
|
delete(t.tokens, upd.Message.Text)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
|
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err)
|
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||||
}
|
}
|
||||||
t.verifiedTokens[upd.Message.Text] = TelegramVerifiedToken{
|
t.verifiedTokens[upd.Message.Text] = TelegramVerifiedToken{
|
||||||
ChatID: upd.Message.Chat.ID,
|
ChatID: upd.Message.Chat.ID,
|
||||||
@ -292,39 +273,7 @@ func (t *TelegramDaemon) UserExists(username string) bool {
|
|||||||
return err != nil || c > 0
|
return err != nil || c > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exists returns whether or not the given user exists.
|
|
||||||
func (t *TelegramDaemon) Exists(user ContactMethodUser) bool {
|
|
||||||
return t.UserExists(user.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteVerifiedToken removes the token with the given PIN.
|
// DeleteVerifiedToken removes the token with the given PIN.
|
||||||
func (t *TelegramDaemon) DeleteVerifiedToken(PIN string) {
|
func (t *TelegramDaemon) DeleteVerifiedToken(pin string) {
|
||||||
delete(t.verifiedTokens, PIN)
|
delete(t.verifiedTokens, pin)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TelegramDaemon) PIN(req newUserDTO) string { return req.TelegramPIN }
|
|
||||||
|
|
||||||
func (t *TelegramDaemon) Name() string { return lm.Telegram }
|
|
||||||
|
|
||||||
func (t *TelegramDaemon) Required() bool {
|
|
||||||
return t.app.config.Section("telegram").Key("required").MustBool(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TelegramDaemon) UniqueRequired() bool {
|
|
||||||
return t.app.config.Section("telegram").Key("require_unique").MustBool(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TelegramDaemon) UserVerified(PIN string) (ContactMethodUser, bool) {
|
|
||||||
token, ok := t.TokenVerified(PIN)
|
|
||||||
if !ok {
|
|
||||||
return &TelegramUser{}, false
|
|
||||||
}
|
|
||||||
tu := token.ToUser()
|
|
||||||
if lang, ok := t.languages[tu.ChatID]; ok {
|
|
||||||
tu.Lang = lang
|
|
||||||
}
|
|
||||||
|
|
||||||
return tu, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TelegramDaemon) PostVerificationTasks(string, ContactMethodUser) error { return nil }
|
|
||||||
|
20
ts/setup.ts
20
ts/setup.ts
@ -221,16 +221,12 @@ class LangSelect extends Select {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const replaceLink = (elName: string, sect: string, name: string, url: string, text: string) => html(elName, window.lang.var(sect, name, `<a class="underline" target="_blank" href="${url}">${text}</a>`));
|
|
||||||
|
|
||||||
window.lang = new lang(window.langFile as LangFile);
|
window.lang = new lang(window.langFile as LangFile);
|
||||||
replaceLink("language-description", "language", "description", "https://weblate.jfa-go.com", "Weblate");
|
html("language-description", window.lang.var("language", "description", `<a target="_blank" href="https://weblate.jfa-go.com">Weblate</a>`));
|
||||||
replaceLink("email-description", "email", "description", "https://mailgun.com", "Mailgun");
|
html("email-description", window.lang.var("email", "description", `<a target="_blank" href="https://mailgun.com">Mailgun</a>`));
|
||||||
replaceLink("email-dateformat-notice", "email", "dateFormatNotice", "https://strftime.timpetricola.com/", "strftime.timpetricola.com");
|
html("email-dateformat-notice", window.lang.var("email", "dateFormatNotice", `<a target="_blank" href="https://strftime.ninja/">strftime.ninja</a>`));
|
||||||
replaceLink("updates-description", "updates", "description", "https://builds.hrfee.dev/view/hrfee/jfa-go", "buildrone");
|
html("updates-description", window.lang.var("updates", "description", `<a target="_blank" href="https://builds.hrfee.dev/view/hrfee/jfa-go">buildrone</a>`));
|
||||||
replaceLink("messages-description", "messages", "description", "https://wiki.jfa-go.com", "Wiki");
|
html("messages-description", window.lang.var("messages", "description", `<a target="_blank" href="https://wiki.jfa-go.com">Wiki</a>`));
|
||||||
replaceLink("password_resets-more-info", "passwordResets", "moreInfo", "https://wiki.jfa-go.com/docs/pwr/", "wiki.jfa-go.com");
|
|
||||||
replaceLink("ombi-stability-warning", "ombi", "stabilityWarning", "https://wiki.jfa-go.com/docs/ombi/", "wiki.jfa-go.com");
|
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
@ -322,12 +318,6 @@ const settings = {
|
|||||||
"server": new Input(get("ombi-server"), "", "", "enabled", true, "ombi"),
|
"server": new Input(get("ombi-server"), "", "", "enabled", true, "ombi"),
|
||||||
"api_key": new Input(get("ombi-api_key"), "", "", "enabled", true, "ombi")
|
"api_key": new Input(get("ombi-api_key"), "", "", "enabled", true, "ombi")
|
||||||
},
|
},
|
||||||
"jellyseerr": {
|
|
||||||
"enabled": new Checkbox(get("jellyseerr-enabled"), "", false, "jellyseerr", "enabled"),
|
|
||||||
"server": new Input(get("jellyseerr-server"), "", "", "enabled", true, "jellyseerr"),
|
|
||||||
"api_key": new Input(get("jellyseerr-api_key"), "", "", "enabled", true, "jellyseerr"),
|
|
||||||
"import_existing": new Checkbox(get("jellyseerr-import_existing"), "enabled", true, "jellyseerr", "import_existing")
|
|
||||||
},
|
|
||||||
"advanced": {
|
"advanced": {
|
||||||
"tls": new Checkbox(get("advanced-tls"), "", false, "advanced", "tls"),
|
"tls": new Checkbox(get("advanced-tls"), "", false, "advanced", "tls"),
|
||||||
"tls_port": new Input(get("advanced-tls_port"), "", "", "tls", true, "advanced"),
|
"tls_port": new Input(get("advanced-tls_port"), "", "", "tls", true, "advanced"),
|
||||||
|
@ -4,8 +4,7 @@
|
|||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"lib": ["dom", "es2017"],
|
"lib": ["dom", "es2017"],
|
||||||
"typeRoots": ["./typings", "../node_modules/@types"],
|
"typeRoots": ["./typings", "../node_modules/@types"],
|
||||||
"module": "esnext",
|
"moduleResolution": "nodenext",
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
|
|
||||||
"github.com/hrfee/jfa-go/common"
|
"github.com/hrfee/jfa-go/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -562,16 +560,15 @@ func (app *appContext) checkForUpdates() {
|
|||||||
if err != nil && strings.Contains(err.Error(), "strconv.ParseInt") {
|
if err != nil && strings.Contains(err.Error(), "strconv.ParseInt") {
|
||||||
app.err.Println("No new updates available.")
|
app.err.Println("No new updates available.")
|
||||||
} else if status != -1 { // -1 means updates disabled, we don't need to log it.
|
} else if status != -1 { // -1 means updates disabled, we don't need to log it.
|
||||||
app.err.Printf(lm.FailedGetUpdateTag, err)
|
app.err.Printf("Failed to get latest tag (%d): %v", status, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if tag != app.tag && tag.IsNew() {
|
if tag != app.tag && tag.IsNew() {
|
||||||
app.info.Println(lm.FoundUpdate)
|
app.info.Println("Update found")
|
||||||
app.debug.Printf(lm.UpdateTagDetails, tag)
|
|
||||||
update, status, err := app.updater.GetUpdate(tag)
|
update, status, err := app.updater.GetUpdate(tag)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUpdate, err)
|
app.err.Printf("Failed to get update (%d): %v", status, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.tag = tag
|
app.tag = tag
|
||||||
|
18
user-auth.go
18
user-auth.go
@ -1,11 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (app *appContext) userAuth() gin.HandlerFunc {
|
func (app *appContext) userAuth() gin.HandlerFunc {
|
||||||
@ -15,7 +13,7 @@ func (app *appContext) userAuth() gin.HandlerFunc {
|
|||||||
func (app *appContext) userAuthenticate(gc *gin.Context) {
|
func (app *appContext) userAuthenticate(gc *gin.Context) {
|
||||||
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true)
|
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true)
|
||||||
if !jellyfinLogin {
|
if !jellyfinLogin {
|
||||||
app.err.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth)
|
app.err.Println("Enable Jellyfin Login to use the User Page feature.")
|
||||||
respond(500, "Contact Admin", gc)
|
respond(500, "Contact Admin", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -29,6 +27,7 @@ func (app *appContext) userAuthenticate(gc *gin.Context) {
|
|||||||
|
|
||||||
gc.Set("jfId", jfID)
|
gc.Set("jfId", jfID)
|
||||||
gc.Set("userMode", true)
|
gc.Set("userMode", true)
|
||||||
|
app.debug.Println("Auth succeeded")
|
||||||
gc.Next()
|
gc.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,11 +41,11 @@ func (app *appContext) userAuthenticate(gc *gin.Context) {
|
|||||||
// @Security getUserTokenAuth
|
// @Security getUserTokenAuth
|
||||||
func (app *appContext) getUserTokenLogin(gc *gin.Context) {
|
func (app *appContext) getUserTokenLogin(gc *gin.Context) {
|
||||||
if !app.config.Section("ui").Key("jellyfin_login").MustBool(true) {
|
if !app.config.Section("ui").Key("jellyfin_login").MustBool(true) {
|
||||||
app.err.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth)
|
app.err.Println("Enable Jellyfin Login to use the User Page feature.")
|
||||||
respond(500, "Contact Admin", gc)
|
respond(500, "Contact Admin", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.logIpInfo(gc, true, fmt.Sprintf(lm.RequestingToken, lm.UserTokenLoginAttempt))
|
app.logIpInfo(gc, true, "UserToken requested (login attempt)")
|
||||||
username, password, ok := app.decodeValidateLoginHeader(gc, true)
|
username, password, ok := app.decodeValidateLoginHeader(gc, true)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@ -59,11 +58,12 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
|
|||||||
|
|
||||||
token, refresh, err := CreateToken(user.ID, user.ID, false)
|
token, refresh, err := CreateToken(user.ID, user.ID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGenerateToken, err)
|
app.err.Printf("getUserToken failed: Couldn't generate user token (%s)", err)
|
||||||
respond(500, "Couldn't generate user token", gc)
|
respond(500, "Couldn't generate user token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.debug.Printf("Token generated for non-admin user \"%s\"", username)
|
||||||
uri := "/my"
|
uri := "/my"
|
||||||
if strings.HasPrefix(gc.Request.RequestURI, app.URLBase) {
|
if strings.HasPrefix(gc.Request.RequestURI, app.URLBase) {
|
||||||
uri = "/accounts/my"
|
uri = "/accounts/my"
|
||||||
@ -81,12 +81,12 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
|
|||||||
func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
|
func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
|
||||||
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true)
|
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true)
|
||||||
if !jellyfinLogin {
|
if !jellyfinLogin {
|
||||||
app.err.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth)
|
app.err.Println("Enable Jellyfin Login to use the User Page feature.")
|
||||||
respond(500, "Contact Admin", gc)
|
respond(500, "Contact Admin", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.logIpInfo(gc, true, fmt.Sprintf(lm.RequestingToken, lm.UserTokenRefresh))
|
app.logIpInfo(gc, true, "UserToken request (refresh token)")
|
||||||
claims, ok := app.decodeValidateRefreshCookie(gc, "user-refresh")
|
claims, ok := app.decodeValidateRefreshCookie(gc, "user-refresh")
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@ -96,7 +96,7 @@ func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
|
|||||||
|
|
||||||
jwt, refresh, err := CreateToken(jfID, jfID, false)
|
jwt, refresh, err := CreateToken(jfID, jfID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGenerateToken, err)
|
app.err.Printf("getUserToken failed: Couldn't generate user token (%s)", err)
|
||||||
respond(500, "Couldn't generate user token", gc)
|
respond(500, "Couldn't generate user token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
)
|
)
|
||||||
@ -22,17 +21,17 @@ func (app *appContext) checkUsers() {
|
|||||||
if len(app.storage.GetUserExpiries()) == 0 {
|
if len(app.storage.GetUserExpiries()) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.info.Println(lm.CheckUserExpiries)
|
app.info.Println("Daemon: Checking for user expiry")
|
||||||
users, status, err := app.jf.GetUsers(false)
|
users, status, err := app.jf.GetUsers(false)
|
||||||
if err != nil || status != 200 {
|
if err != nil || status != 200 {
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
app.err.Printf("Failed to get users (%d): %s", status, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mode := "disable"
|
mode := "disable"
|
||||||
phrase := lm.DisableExpiredUser
|
term := "Disabling"
|
||||||
if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" {
|
if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" {
|
||||||
mode = "delete"
|
mode = "delete"
|
||||||
phrase = lm.DeleteExpiredUser
|
term = "Deleting"
|
||||||
}
|
}
|
||||||
contact := false
|
contact := false
|
||||||
if messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) {
|
if messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) {
|
||||||
@ -46,7 +45,7 @@ func (app *appContext) checkUsers() {
|
|||||||
for _, expiry := range app.storage.GetUserExpiries() {
|
for _, expiry := range app.storage.GetUserExpiries() {
|
||||||
id := expiry.JellyfinID
|
id := expiry.JellyfinID
|
||||||
if _, ok := userExists[id]; !ok {
|
if _, ok := userExists[id]; !ok {
|
||||||
app.info.Printf(lm.DeleteExpiryForOldUser, id)
|
app.info.Printf("Deleting expiry for non-existent user \"%s\"", id)
|
||||||
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
|
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
|
||||||
} else if time.Now().After(expiry.Expiry) {
|
} else if time.Now().After(expiry.Expiry) {
|
||||||
found := false
|
found := false
|
||||||
@ -59,10 +58,11 @@ func (app *appContext) checkUsers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
|
app.info.Printf("Expired user already deleted, ignoring.")
|
||||||
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
|
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
app.info.Printf(phrase, user.Name)
|
app.info.Printf("%s expired user \"%s\"", term, user.Name)
|
||||||
|
|
||||||
// Record activity
|
// Record activity
|
||||||
activity := Activity{
|
activity := Activity{
|
||||||
@ -72,24 +72,18 @@ func (app *appContext) checkUsers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if mode == "delete" {
|
if mode == "delete" {
|
||||||
deleted := false
|
status, err = app.jf.DeleteUser(id)
|
||||||
err, deleted = app.DeleteUser(user)
|
|
||||||
// Silence unimportant errors
|
|
||||||
if deleted {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
activity.Type = ActivityDeletion
|
activity.Type = ActivityDeletion
|
||||||
// Store the user name, since there's no longer a user ID to reference back to
|
|
||||||
activity.Value = user.Name
|
activity.Value = user.Name
|
||||||
} else if mode == "disable" {
|
} else if mode == "disable" {
|
||||||
|
user.Policy.IsDisabled = true
|
||||||
// Admins can't be disabled
|
// Admins can't be disabled
|
||||||
// so they're not an admin anymore, sorry
|
|
||||||
user.Policy.IsAdministrator = false
|
user.Policy.IsAdministrator = false
|
||||||
err, _, _ = app.SetUserDisabled(user, true)
|
status, err = app.jf.SetPolicy(id, user.Policy)
|
||||||
activity.Type = ActivityDisabled
|
activity.Type = ActivityDisabled
|
||||||
}
|
}
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedDeleteOrDisableExpiredUser, user.ID, err)
|
app.err.Printf("Failed to %s \"%s\" (%d): %s", mode, user.Name, status, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,11 +98,11 @@ func (app *appContext) checkUsers() {
|
|||||||
name := app.getAddressOrName(user.ID)
|
name := app.getAddressOrName(user.ID)
|
||||||
msg, err := app.email.constructUserExpired(app, false)
|
msg, err := app.email.constructUserExpired(app, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err)
|
app.err.Printf("Failed to construct expiry message for \"%s\": %s", user.Name, err)
|
||||||
} else if err := app.sendByID(msg, user.ID); err != nil {
|
} else if err := app.sendByID(msg, user.ID); err != nil {
|
||||||
app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, name, err)
|
app.err.Printf("Failed to send expiry message to \"%s\": %s", name, err)
|
||||||
} else {
|
} else {
|
||||||
app.err.Printf(lm.SentExpiryMessage, user.ID, name)
|
app.info.Printf("Sent expiry notification to \"%s\"", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
223
users.go
223
users.go
@ -1,223 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/mediabrowser"
|
|
||||||
"github.com/lithammer/shortuuid/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReponseFunc responds to the user, generally by HTTP response
|
|
||||||
// The cases when more than this occurs are given below.
|
|
||||||
type ResponseFunc func(gc *gin.Context)
|
|
||||||
|
|
||||||
// LogFunc prints a log line once called.
|
|
||||||
type LogFunc func()
|
|
||||||
|
|
||||||
type ContactMethodConf struct {
|
|
||||||
Email, Discord, Telegram, Matrix bool
|
|
||||||
}
|
|
||||||
type ContactMethodUsers struct {
|
|
||||||
Email emailStore
|
|
||||||
Discord DiscordUser
|
|
||||||
Telegram TelegramVerifiedToken
|
|
||||||
Matrix MatrixUser
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContactMethodValidation struct {
|
|
||||||
Verified ContactMethodConf
|
|
||||||
Users ContactMethodUsers
|
|
||||||
}
|
|
||||||
|
|
||||||
type NewUserParams struct {
|
|
||||||
Req newUserDTO
|
|
||||||
SourceType ActivitySource
|
|
||||||
Source string
|
|
||||||
ContextForIPLogging *gin.Context
|
|
||||||
Profile *Profile
|
|
||||||
}
|
|
||||||
|
|
||||||
type NewUserData struct {
|
|
||||||
Created bool
|
|
||||||
Success bool
|
|
||||||
User mediabrowser.User
|
|
||||||
Message string
|
|
||||||
Status int
|
|
||||||
Log func()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called after a new-user-creating route has done pre-steps (veryfing contact methods for example).
|
|
||||||
func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData) {
|
|
||||||
// Some helper functions which will behave as our app.info/error/debug
|
|
||||||
deferLogInfo := func(s string, args ...any) {
|
|
||||||
out.Log = func() {
|
|
||||||
app.info.Printf(s, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* deferLogDebug := func(s string, args ...any) {
|
|
||||||
out.Log = func() {
|
|
||||||
app.debug.Printf(s, args)
|
|
||||||
}
|
|
||||||
} */
|
|
||||||
deferLogError := func(s string, args ...any) {
|
|
||||||
out.Log = func() {
|
|
||||||
app.err.Printf(s, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
existingUser, _, _ := app.jf.UserByName(p.Req.Username, false)
|
|
||||||
if existingUser.Name != "" {
|
|
||||||
out.Message = lm.UserExists
|
|
||||||
deferLogInfo(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
|
||||||
out.Status = 401
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var status int
|
|
||||||
var err error
|
|
||||||
out.User, status, err = app.jf.NewUser(p.Req.Username, p.Req.Password)
|
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
|
||||||
out.Message = err.Error()
|
|
||||||
deferLogError(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
|
|
||||||
out.Status = 401
|
|
||||||
return
|
|
||||||
}
|
|
||||||
out.Created = true
|
|
||||||
// Invalidate Cache to be safe
|
|
||||||
app.jf.CacheExpiry = time.Now()
|
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
|
||||||
Type: ActivityCreation,
|
|
||||||
UserID: out.User.ID,
|
|
||||||
SourceType: p.SourceType,
|
|
||||||
Source: p.Source,
|
|
||||||
InviteCode: p.Req.Code, // Left blank when an admin does this
|
|
||||||
Value: out.User.Name,
|
|
||||||
Time: time.Now(),
|
|
||||||
}, p.ContextForIPLogging, (p.SourceType != ActivityAdmin))
|
|
||||||
|
|
||||||
if p.Profile != nil {
|
|
||||||
status, err = app.jf.SetPolicy(out.User.ID, p.Profile.Policy)
|
|
||||||
if !((status == 200 || status == 204) && err == nil) {
|
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, out.User.ID, err)
|
|
||||||
}
|
|
||||||
status, err = app.jf.SetConfiguration(out.User.ID, p.Profile.Configuration)
|
|
||||||
if (status == 200 || status == 204) && err == nil {
|
|
||||||
status, err = app.jf.SetDisplayPreferences(out.User.ID, p.Profile.Displayprefs)
|
|
||||||
}
|
|
||||||
if !((status == 200 || status == 204) && err == nil) {
|
|
||||||
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, out.User.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tps := range app.thirdPartyServices {
|
|
||||||
if !tps.Enabled(app, p.Profile) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// When ok and err != nil, its a non-fatal failure that we lot without the "FailedImportUser".
|
|
||||||
err, ok := tps.ImportUser(out.User.ID, p.Req, *p.Profile)
|
|
||||||
if !ok {
|
|
||||||
app.err.Printf(lm.FailedImportUser, tps.Name(), p.Req.Username, err)
|
|
||||||
} else if err != nil {
|
|
||||||
app.info.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Welcome email is sent by each user of this method separately..
|
|
||||||
|
|
||||||
out.Status = 200
|
|
||||||
out.Success = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) WelcomeNewUser(user mediabrowser.User, expiry time.Time) (failed bool) {
|
|
||||||
if !app.config.Section("welcome_email").Key("enabled").MustBool(false) {
|
|
||||||
// we didn't "fail", we "politely declined"
|
|
||||||
// failed = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
failed = true
|
|
||||||
name := app.getAddressOrName(user.ID)
|
|
||||||
if name == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg, err := app.email.constructWelcome(user.Name, expiry, app, false)
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedConstructWelcomeMessage, user.ID, err)
|
|
||||||
} else if err := app.sendByID(msg, user.ID); err != nil {
|
|
||||||
app.err.Printf(lm.FailedSendWelcomeMessage, user.ID, name, err)
|
|
||||||
} else {
|
|
||||||
app.info.Printf(lm.SentWelcomeMessage, user.ID, name)
|
|
||||||
failed = false
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) SetUserDisabled(user mediabrowser.User, disabled bool) (err error, change bool, activityType ActivityType) {
|
|
||||||
activityType = ActivityEnabled
|
|
||||||
if disabled {
|
|
||||||
activityType = ActivityDisabled
|
|
||||||
}
|
|
||||||
change = user.Policy.IsDisabled != disabled
|
|
||||||
user.Policy.IsDisabled = disabled
|
|
||||||
|
|
||||||
var status int
|
|
||||||
status, err = app.jf.SetPolicy(user.ID, user.Policy)
|
|
||||||
if !(status == 200 || status == 204) && err == nil {
|
|
||||||
err = fmt.Errorf("failed (code %d)", status)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.discord != nil && app.config.Section("discord").Key("disable_enable_role").MustBool(false) {
|
|
||||||
cmUser, ok := app.storage.GetDiscordKey(user.ID)
|
|
||||||
if ok {
|
|
||||||
if err := app.discord.SetRoleDisabled(cmUser.MethodID().(string), disabled); err != nil {
|
|
||||||
app.err.Printf(lm.FailedSetDiscordMemberRole, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) DeleteUser(user mediabrowser.User) (err error, deleted bool) {
|
|
||||||
var status int
|
|
||||||
if app.ombi != nil {
|
|
||||||
var tpUser map[string]any
|
|
||||||
tpUser, status, err = app.getOmbiUser(user.ID)
|
|
||||||
if status == 200 && err == nil {
|
|
||||||
if id, ok := tpUser["id"]; ok {
|
|
||||||
status, err = app.ombi.DeleteUser(id.(string))
|
|
||||||
if status != 200 && err == nil {
|
|
||||||
err = fmt.Errorf("failed (code %d)", status)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedDeleteUser, lm.Ombi, user.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.discord != nil && app.config.Section("discord").Key("disable_enable_role").MustBool(false) {
|
|
||||||
cmUser, ok := app.storage.GetDiscordKey(user.ID)
|
|
||||||
if ok {
|
|
||||||
if err := app.discord.RemoveRole(cmUser.MethodID().(string)); err != nil {
|
|
||||||
app.err.Printf(lm.FailedSetDiscordMemberRole, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
status, err = app.jf.DeleteUser(user.ID)
|
|
||||||
if status != 200 && status != 204 && err == nil {
|
|
||||||
err = fmt.Errorf("failed (code %d)", status)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
deleted = true
|
|
||||||
return
|
|
||||||
}
|
|
243
views.go
243
views.go
@ -4,7 +4,6 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@ -17,7 +16,6 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/gomarkdown/markdown"
|
"github.com/gomarkdown/markdown"
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
|
||||||
"github.com/hrfee/mediabrowser"
|
"github.com/hrfee/mediabrowser"
|
||||||
"github.com/lithammer/shortuuid/v3"
|
"github.com/lithammer/shortuuid/v3"
|
||||||
"github.com/steambap/captcha"
|
"github.com/steambap/captcha"
|
||||||
@ -68,9 +66,10 @@ func (app *appContext) pushResources(gc *gin.Context, page Page) {
|
|||||||
toPush = []string{}
|
toPush = []string{}
|
||||||
}
|
}
|
||||||
if pusher := gc.Writer.Pusher(); pusher != nil {
|
if pusher := gc.Writer.Pusher(); pusher != nil {
|
||||||
|
app.debug.Println("Using HTTP2 Server push")
|
||||||
for _, f := range toPush {
|
for _, f := range toPush {
|
||||||
if err := pusher.Push(app.URLBase+f, nil); err != nil {
|
if err := pusher.Push(app.URLBase+f, nil); err != nil {
|
||||||
app.debug.Printf(lm.FailedServerPush, err)
|
app.debug.Printf("Failed HTTP2 ServerPush of \"%s\": %+v", f, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,13 +139,13 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
|||||||
var license string
|
var license string
|
||||||
l, err := fs.ReadFile(localFS, "LICENSE")
|
l, err := fs.ReadFile(localFS, "LICENSE")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.debug.Printf(lm.FailedReading, "LICENSE", err)
|
app.debug.Printf("Failed to load LICENSE: %s", err)
|
||||||
license = ""
|
license = ""
|
||||||
}
|
}
|
||||||
license = string(l)
|
license = string(l)
|
||||||
fontLicense, err := fs.ReadFile(localFS, filepath.Join("web", "fonts", "OFL.txt"))
|
fontLicense, err := fs.ReadFile(localFS, filepath.Join("web", "fonts", "OFL.txt"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.debug.Printf(lm.FailedReading, "fontLicense", err)
|
app.debug.Printf("Failed to load OFL.txt: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
license += "---Hanken Grotesk---\n\n"
|
license += "---Hanken Grotesk---\n\n"
|
||||||
@ -237,7 +236,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
|
|||||||
"server_channel": app.discord.serverChannelName,
|
"server_channel": app.discord.serverChannelName,
|
||||||
}))
|
}))
|
||||||
data["discordServerName"] = app.discord.serverName
|
data["discordServerName"] = app.discord.serverName
|
||||||
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
|
data["discordInviteLink"] = app.discord.inviteChannelName != ""
|
||||||
}
|
}
|
||||||
if data["linkResetEnabled"].(bool) {
|
if data["linkResetEnabled"].(bool) {
|
||||||
data["resetPasswordUsername"] = app.config.Section("user_page").Key("allow_pwr_username").MustBool(true)
|
data["resetPasswordUsername"] = app.config.Section("user_page").Key("allow_pwr_username").MustBool(true)
|
||||||
@ -313,7 +312,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
defer gcHTML(gc, http.StatusOK, "password-reset.html", data)
|
defer gcHTML(gc, http.StatusOK, "password-reset.html", data)
|
||||||
// If it's a bot, pretend to be a success so the preview is nice.
|
// If it's a bot, pretend to be a success so the preview is nice.
|
||||||
if isBot {
|
if isBot {
|
||||||
app.debug.Println(lm.IgnoreBotPWR)
|
app.debug.Println("PWR: Ignoring magic link visit from bot")
|
||||||
data["success"] = true
|
data["success"] = true
|
||||||
data["pin"] = "NO-BO-TS"
|
data["pin"] = "NO-BO-TS"
|
||||||
return
|
return
|
||||||
@ -339,13 +338,13 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
if !isInternal && !setPassword {
|
if !isInternal && !setPassword {
|
||||||
resp, status, err = app.jf.ResetPassword(pin)
|
resp, status, err = app.jf.ResetPassword(pin)
|
||||||
} else if time.Now().After(pwr.Expiry) {
|
} else if time.Now().After(pwr.Expiry) {
|
||||||
app.debug.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, pin))
|
app.debug.Printf("Ignoring PWR request due to expired internal PIN: %s", pin)
|
||||||
app.NoRouteHandler(gc)
|
app.NoRouteHandler(gc)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
status, err = app.jf.ResetPasswordAdmin(pwr.ID)
|
status, err = app.jf.ResetPasswordAdmin(pwr.ID)
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", err)
|
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
||||||
} else {
|
} else {
|
||||||
status, err = app.jf.SetPassword(pwr.ID, "", pin)
|
status, err = app.jf.SetPassword(pwr.ID, "", pin)
|
||||||
}
|
}
|
||||||
@ -359,7 +358,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
username = resp.UsersReset[0]
|
username = resp.UsersReset[0]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", err)
|
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only log PWRs we know the user for.
|
// Only log PWRs we know the user for.
|
||||||
@ -379,21 +378,21 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||||
jfUser, status, err := app.jf.UserByName(username, false)
|
jfUser, status, err := app.jf.UserByName(username, false)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, username, lm.Jellyfin, err)
|
app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ombiUser, status, err := app.getOmbiUser(jfUser.ID)
|
ombiUser, status, err := app.getOmbiUser(jfUser.ID)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedGetUser, username, lm.Ombi, err)
|
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", username, status, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ombiUser["password"] = pin
|
ombiUser["password"] = pin
|
||||||
status, err = app.ombi.ModifyUser(ombiUser)
|
status, err = app.ombi.ModifyUser(ombiUser)
|
||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
|
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.debug.Printf(lm.ChangePassword, lm.Ombi, ombiUser["userName"])
|
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,7 +460,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
capt, err := captcha.New(300, 100)
|
capt, err := captcha.New(300, 100)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedGenerateCaptcha, err)
|
app.err.Printf("Failed to generate captcha: %v", err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -471,7 +470,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
|
|||||||
captchaID := genAuthToken()
|
captchaID := genAuthToken()
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := capt.WriteImage(bufio.NewWriter(&buf)); err != nil {
|
if err := capt.WriteImage(bufio.NewWriter(&buf)); err != nil {
|
||||||
app.err.Printf(lm.FailedGenerateCaptcha, err)
|
app.err.Printf("Failed to render captcha: %v", err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -504,12 +503,8 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
|
|||||||
ok := true
|
ok := true
|
||||||
if !isPWR {
|
if !isPWR {
|
||||||
inv, ok := app.storage.GetInvitesKey(code)
|
inv, ok := app.storage.GetInvitesKey(code)
|
||||||
if !ok {
|
if !ok || (!isPWR && inv.Captchas == nil) {
|
||||||
app.debug.Printf(lm.InvalidInviteCode, code)
|
app.debug.Printf("Couldn't find invite \"%s\"", code)
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !isPWR && inv.Captchas == nil {
|
|
||||||
app.debug.Printf(lm.CaptchaNotFound, id, code)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
c, ok = inv.Captchas[id]
|
c, ok = inv.Captchas[id]
|
||||||
@ -517,7 +512,7 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
|
|||||||
c, ok = app.pwrCaptchas[code]
|
c, ok = app.pwrCaptchas[code]
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
app.debug.Printf(lm.CaptchaNotFound, id, code)
|
app.debug.Printf("Couldn't find Captcha \"%s\"", id)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return strings.ToLower(c.Answer) == strings.ToLower(text)
|
return strings.ToLower(c.Answer) == strings.ToLower(text)
|
||||||
@ -539,11 +534,8 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
|
|||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err == nil && resp.StatusCode != 200 {
|
if err != nil || resp.StatusCode != 200 {
|
||||||
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
|
app.err.Printf("Failed to read reCAPTCHA status (%d): %+v\n", resp.Status, err)
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@ -551,19 +543,18 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
|
|||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
err = json.Unmarshal(body, &data)
|
err = json.Unmarshal(body, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
|
app.err.Printf("Failed to unmarshal reCAPTCHA response: %+v\n", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
hostname := app.config.Section("captcha").Key("recaptcha_hostname").MustString("")
|
hostname := app.config.Section("captcha").Key("recaptcha_hostname").MustString("")
|
||||||
if strings.ToLower(data.Hostname) != strings.ToLower(hostname) && data.Hostname != "" {
|
if strings.ToLower(data.Hostname) != strings.ToLower(hostname) && data.Hostname != "" {
|
||||||
err = fmt.Errorf(lm.InvalidHostname, hostname, data.Hostname)
|
app.debug.Printf("Invalidating reCAPTCHA request: Hostnames didn't match (Wanted \"%s\", got \"%s\"\n", hostname, data.Hostname)
|
||||||
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data.ErrorCodes) > 0 {
|
if len(data.ErrorCodes) > 0 {
|
||||||
app.err.Printf(lm.AdditionalErrors, lm.ReCAPTCHA, data.ErrorCodes)
|
app.err.Printf("reCAPTCHA returned errors: %+v\n", data.ErrorCodes)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,108 +607,13 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) NewUserFromConfirmationKey(invite Invite, key string, lang string, gc *gin.Context) {
|
|
||||||
fail := func() {
|
|
||||||
gcHTML(gc, 404, "404.html", gin.H{
|
|
||||||
"urlBase": app.getURLBase(gc),
|
|
||||||
"cssClass": app.cssClass,
|
|
||||||
"cssVersion": cssVersion,
|
|
||||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req newUserDTO
|
|
||||||
if app.ConfirmationKeys == nil {
|
|
||||||
fail()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
invKeys, ok := app.ConfirmationKeys[invite.Code]
|
|
||||||
if !ok {
|
|
||||||
fail()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req, ok = invKeys[key]
|
|
||||||
if !ok {
|
|
||||||
fail()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
token, err := jwt.Parse(key, checkToken)
|
|
||||||
if err != nil {
|
|
||||||
fail()
|
|
||||||
app.debug.Printf(lm.FailedParseJWT, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
|
||||||
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
|
|
||||||
if !(ok && token.Valid && claims["invite"].(string) == invite.Code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
|
||||||
fail()
|
|
||||||
app.debug.Printf(lm.InvalidJWT)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceType, source := invite.Source()
|
|
||||||
|
|
||||||
var profile *Profile = nil
|
|
||||||
if invite.Profile != "" {
|
|
||||||
p, ok := app.storage.GetProfileKey(invite.Profile)
|
|
||||||
if !ok {
|
|
||||||
app.debug.Printf(lm.FailedGetProfile+lm.FallbackToDefault, invite.Profile)
|
|
||||||
p = app.storage.GetDefaultProfile()
|
|
||||||
}
|
|
||||||
profile = &p
|
|
||||||
}
|
|
||||||
|
|
||||||
nu := app.NewUserPostVerification(NewUserParams{
|
|
||||||
Req: req,
|
|
||||||
SourceType: sourceType,
|
|
||||||
Source: source,
|
|
||||||
ContextForIPLogging: gc,
|
|
||||||
Profile: profile,
|
|
||||||
})
|
|
||||||
if !nu.Success {
|
|
||||||
nu.Log()
|
|
||||||
}
|
|
||||||
if !nu.Created {
|
|
||||||
respond(nu.Status, nu.Message, gc)
|
|
||||||
fail()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.checkInvite(req.Code, true, req.Username)
|
|
||||||
|
|
||||||
jfLink := app.config.Section("ui").Key("redirect_url").String()
|
|
||||||
if app.config.Section("ui").Key("auto_redirect").MustBool(false) {
|
|
||||||
gc.Redirect(301, jfLink)
|
|
||||||
} else {
|
|
||||||
gcHTML(gc, http.StatusOK, "create-success.html", gin.H{
|
|
||||||
"urlBase": app.getURLBase(gc),
|
|
||||||
"cssClass": app.cssClass,
|
|
||||||
"cssVersion": cssVersion,
|
|
||||||
"strings": app.storage.lang.User[lang].Strings,
|
|
||||||
"successMessage": app.config.Section("ui").Key("success_message").String(),
|
|
||||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
|
||||||
"jfLink": jfLink,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
app.confirmationKeysLock.Lock()
|
|
||||||
// Re-fetch invKeys just incase an update occurred
|
|
||||||
invKeys, ok = app.ConfirmationKeys[invite.Code]
|
|
||||||
if !ok {
|
|
||||||
fail()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
delete(invKeys, key)
|
|
||||||
app.ConfirmationKeys[invite.Code] = invKeys
|
|
||||||
app.confirmationKeysLock.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *appContext) InviteProxy(gc *gin.Context) {
|
func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||||
app.pushResources(gc, FormPage)
|
app.pushResources(gc, FormPage)
|
||||||
|
code := gc.Param("invCode")
|
||||||
lang := app.getLang(gc, FormPage, app.storage.lang.chosenUserLang)
|
lang := app.getLang(gc, FormPage, app.storage.lang.chosenUserLang)
|
||||||
|
|
||||||
/* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */
|
/* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */
|
||||||
// if app.checkInvite(code, false, "") {
|
// if app.checkInvite(code, false, "") {
|
||||||
invite, ok := app.storage.GetInvitesKey(gc.Param("invCode"))
|
inv, ok := app.storage.GetInvitesKey(code)
|
||||||
if !ok {
|
if !ok {
|
||||||
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
||||||
"urlBase": app.getURLBase(gc),
|
"urlBase": app.getURLBase(gc),
|
||||||
@ -727,13 +623,76 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
||||||
app.NewUserFromConfirmationKey(invite, key, lang, gc)
|
fail := func() {
|
||||||
|
gcHTML(gc, 404, "404.html", gin.H{
|
||||||
|
"urlBase": app.getURLBase(gc),
|
||||||
|
"cssClass": app.cssClass,
|
||||||
|
"cssVersion": cssVersion,
|
||||||
|
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var req newUserDTO
|
||||||
|
if app.ConfirmationKeys == nil {
|
||||||
|
fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
invKeys, ok := app.ConfirmationKeys[code]
|
||||||
|
if !ok {
|
||||||
|
fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req, ok = invKeys[key]
|
||||||
|
if !ok {
|
||||||
|
fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token, err := jwt.Parse(key, checkToken)
|
||||||
|
if err != nil {
|
||||||
|
fail()
|
||||||
|
app.err.Printf("Failed to parse key: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
|
||||||
|
if !(ok && token.Valid && claims["invite"].(string) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
||||||
|
fail()
|
||||||
|
app.debug.Printf("Invalid key")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, success := app.newUser(req, true, gc)
|
||||||
|
if !success {
|
||||||
|
app.err.Printf("Failed to create new user")
|
||||||
|
// Not meant for us. Calling this will be a mess, but at least it might give us some information.
|
||||||
|
f(gc)
|
||||||
|
fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jfLink := app.config.Section("ui").Key("redirect_url").String()
|
||||||
|
if app.config.Section("ui").Key("auto_redirect").MustBool(false) {
|
||||||
|
gc.Redirect(301, jfLink)
|
||||||
|
} else {
|
||||||
|
gcHTML(gc, http.StatusOK, "create-success.html", gin.H{
|
||||||
|
"urlBase": app.getURLBase(gc),
|
||||||
|
"cssClass": app.cssClass,
|
||||||
|
"cssVersion": cssVersion,
|
||||||
|
"strings": app.storage.lang.User[lang].Strings,
|
||||||
|
"successMessage": app.config.Section("ui").Key("success_message").String(),
|
||||||
|
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||||
|
"jfLink": jfLink,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
delete(invKeys, key)
|
||||||
|
app.confirmationKeysLock.Lock()
|
||||||
|
app.ConfirmationKeys[code] = invKeys
|
||||||
|
app.confirmationKeysLock.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
email := ""
|
||||||
email := invite.SendTo
|
if invite, ok := app.storage.GetInvitesKey(code); ok {
|
||||||
|
email = invite.SendTo
|
||||||
|
}
|
||||||
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
|
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
|
||||||
email = ""
|
email = ""
|
||||||
}
|
}
|
||||||
@ -748,8 +707,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
userPageAddress += "/my/account"
|
userPageAddress += "/my/account"
|
||||||
|
|
||||||
fromUser := ""
|
fromUser := ""
|
||||||
if invite.ReferrerJellyfinID != "" {
|
if inv.ReferrerJellyfinID != "" {
|
||||||
sender, status, err := app.jf.UserByID(invite.ReferrerJellyfinID, false)
|
sender, status, err := app.jf.UserByID(inv.ReferrerJellyfinID, false)
|
||||||
if status == 200 && err == nil {
|
if status == 200 && err == nil {
|
||||||
fromUser = sender.Name
|
fromUser = sender.Name
|
||||||
}
|
}
|
||||||
@ -771,13 +730,13 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
"strings": app.storage.lang.User[lang].Strings,
|
"strings": app.storage.lang.User[lang].Strings,
|
||||||
"validationStrings": app.storage.lang.User[lang].validationStringsJSON,
|
"validationStrings": app.storage.lang.User[lang].validationStringsJSON,
|
||||||
"notifications": app.storage.lang.User[lang].notificationsJSON,
|
"notifications": app.storage.lang.User[lang].notificationsJSON,
|
||||||
"code": invite.Code,
|
"code": code,
|
||||||
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
|
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
|
||||||
"userExpiry": invite.UserExpiry,
|
"userExpiry": inv.UserExpiry,
|
||||||
"userExpiryMonths": invite.UserMonths,
|
"userExpiryMonths": inv.UserMonths,
|
||||||
"userExpiryDays": invite.UserDays,
|
"userExpiryDays": inv.UserDays,
|
||||||
"userExpiryHours": invite.UserHours,
|
"userExpiryHours": inv.UserHours,
|
||||||
"userExpiryMinutes": invite.UserMinutes,
|
"userExpiryMinutes": inv.UserMinutes,
|
||||||
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
|
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
|
||||||
"langName": lang,
|
"langName": lang,
|
||||||
"passwordReset": false,
|
"passwordReset": false,
|
||||||
@ -812,7 +771,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
"server_channel": app.discord.serverChannelName,
|
"server_channel": app.discord.serverChannelName,
|
||||||
}))
|
}))
|
||||||
data["discordServerName"] = app.discord.serverName
|
data["discordServerName"] = app.discord.serverName
|
||||||
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
|
data["discordInviteLink"] = app.discord.inviteChannelName != ""
|
||||||
}
|
}
|
||||||
if msg, ok := app.storage.GetCustomContentKey("PostSignupCard"); ok && msg.Enabled {
|
if msg, ok := app.storage.GetCustomContentKey("PostSignupCard"); ok && msg.Enabled {
|
||||||
data["customSuccessCard"] = true
|
data["customSuccessCard"] = true
|
||||||
|
Loading…
Reference in New Issue
Block a user