mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-28 03:50:10 +00:00
Compare commits
22 Commits
2d066ea7cd
...
62e27c394d
Author | SHA1 | Date | |
---|---|---|---|
62e27c394d | |||
8f3c723b07 | |||
3c28537498 | |||
b0e94a4ef6 | |||
baeb89b694 | |||
016263894f | |||
|
5fe532fb78 | ||
|
598859ae31 | ||
|
d8dcb84870 | ||
28ca02272c | |||
448955c915 | |||
ffd46ff190 | |||
44311162a6 | |||
f289680d98 | |||
280c6e4f16 | |||
54e4a51a7f | |||
711394232b | |||
15a317f84f | |||
f348262f88 | |||
e9b8d970d1 | |||
fb5d3c4165 | |||
c442ff5f98 |
172
.drone.yml.old
172
.drone.yml.old
@ -1,172 +0,0 @@
|
|||||||
---
|
|
||||||
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,3 +25,4 @@ scripts/langmover/lang
|
|||||||
scripts/langmover/lang2
|
scripts/langmover/lang2
|
||||||
scripts/langmover/out
|
scripts/langmover/out
|
||||||
tinyproxy.conf
|
tinyproxy.conf
|
||||||
|
static/banner.svg
|
||||||
|
@ -11,6 +11,7 @@ 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,6 +167,7 @@ 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,6 +2,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -120,7 +121,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("Failed to read activities from DB: %v\n", err)
|
app.err.Printf(lm.FailedDBReadActivities, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := GetActivitiesRespDTO{
|
resp := GetActivitiesRespDTO{
|
||||||
|
@ -8,6 +8,7 @@ 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.
|
||||||
@ -35,7 +36,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("Ignoring backup DL request due to fname: %v\n", err)
|
app.debug.Printf(lm.IgnoreInvalidFilename, fname, err)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -83,7 +84,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("Ignoring backup DL request due to fname: %v\n", err)
|
app.debug.Printf(lm.IgnoreInvalidFilename, fname, err)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -103,15 +104,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("Failed to get file from form data: %v\n", err)
|
app.err.Printf(lm.FailedGetUpload, err)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.debug.Printf("Got uploaded file \"%s\"\n", file.Filename)
|
app.debug.Printf(lm.GetUpload, 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("Saved to \"%s\"\n", fullpath)
|
app.debug.Printf(lm.Write, fullpath)
|
||||||
LOADBAK = fullpath
|
LOADBAK = fullpath
|
||||||
app.restart(gc)
|
app.restart(gc)
|
||||||
}
|
}
|
||||||
|
221
api-invites.go
221
api-invites.go
@ -8,6 +8,7 @@ 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"
|
||||||
@ -29,6 +30,7 @@ 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() {
|
||||||
@ -52,60 +54,11 @@ 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)
|
||||||
@ -114,54 +67,8 @@ 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.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
app.deleteExpiredInvite(inv)
|
||||||
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
|
||||||
@ -187,6 +94,67 @@ 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"
|
||||||
@ -196,7 +164,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
|||||||
// @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("Generating new invite")
|
app.debug.Println(lm.GenerateInvite)
|
||||||
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)
|
||||||
@ -230,13 +198,12 @@ 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("Failed: User not found: \"%s\"", req.SendTo)
|
invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipNoUser, req.SendTo)
|
||||||
} else if len(users) > 1 {
|
} else if len(users) > 1 {
|
||||||
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
|
invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipMultiUser, req.SendTo)
|
||||||
} else {
|
} else {
|
||||||
invite.SendTo = req.SendTo
|
invite.SendTo = req.SendTo
|
||||||
addressValid = true
|
addressValid = true
|
||||||
@ -249,8 +216,10 @@ 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 {
|
||||||
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
// Slight misuse of the template
|
||||||
app.err.Printf("%s: Failed to construct invite message: %v", invite.Code, err)
|
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, req.SendTo, err)
|
||||||
|
|
||||||
|
app.err.Printf(lm.FailedConstructInviteMessage, invite.Code, err)
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
if discord != "" {
|
if discord != "" {
|
||||||
@ -259,10 +228,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("Failed to send to %s", req.SendTo)
|
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, req.SendTo, err)
|
||||||
app.err.Printf("%s: %s: %v", invite.Code, invite.SendTo, err)
|
app.err.Println(invite.SendTo)
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf("%s: Sent invite email to \"%s\"", invite.Code, req.SendTo)
|
app.info.Printf(lm.SentInviteMessage, invite.Code, req.SendTo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -297,7 +266,6 @@ 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
|
||||||
@ -332,7 +300,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("Failed to parse usedBy time: %v", err)
|
app.err.Printf(lm.FailedParseTime, err)
|
||||||
}
|
}
|
||||||
unix = date.Unix()
|
unix = date.Unix()
|
||||||
}
|
}
|
||||||
@ -347,7 +315,6 @@ 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")
|
||||||
@ -397,10 +364,9 @@ 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("%s: Profile \"%s\" not found", req.Invite, req.Profile)
|
app.err.Printf(lm.FailedGetProfile, req.Profile)
|
||||||
respond(500, "Profile not found", gc)
|
respond(500, "Profile not found", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -424,11 +390,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 {
|
||||||
app.err.Printf("%s Notification setting change failed: Invalid code", code)
|
msg := fmt.Sprintf(lm.InvalidInviteCode, code)
|
||||||
respond(400, "Invalid invite code", gc)
|
app.err.Println(msg)
|
||||||
|
respond(400, msg, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var address string
|
var address string
|
||||||
@ -436,9 +402,8 @@ 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("%s: Couldn't find contact method for admin. Make sure one is set.", code)
|
app.err.Printf(lm.FailedGetContactMethod, gc.GetString("jfId"))
|
||||||
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
respond(500, fmt.Sprintf(lm.FailedGetContactMethod, "admin"), gc)
|
||||||
respond(500, "Missing user contact method", gc)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
address = gc.GetString("jfId")
|
address = gc.GetString("jfId")
|
||||||
@ -453,15 +418,12 @@ 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 {
|
||||||
*/
|
*/
|
||||||
if _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] {
|
for _, notifyType := range []string{"notify-expiry", "notify-creation"} {
|
||||||
invite.Notify[address]["notify-expiry"] = settings["notify-expiry"]
|
if _, ok := settings[notifyType]; ok && invite.Notify[address][notifyType] != settings[notifyType] {
|
||||||
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address)
|
invite.Notify[address][notifyType] = settings[notifyType]
|
||||||
changed = true
|
app.debug.Printf(lm.SetAdminNotify, notifyType, settings[notifyType], address)
|
||||||
}
|
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)
|
||||||
@ -480,7 +442,6 @@ 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)
|
||||||
@ -495,10 +456,10 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
|||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
}, gc, false)
|
}, gc, false)
|
||||||
|
|
||||||
app.info.Printf("%s: Invite deleted", req.Code)
|
app.info.Printf(lm.DeleteInvite, req.Code)
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.err.Printf("%s: Deletion failed: Invalid code", req.Code)
|
app.err.Printf(lm.FailedDeleteInvite, req.Code, "invalid code")
|
||||||
respond(400, "Code doesn't exist", gc)
|
respond(400, "Code doesn't exist", gc)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
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.
|
||||||
@ -15,14 +18,12 @@ 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("Failed to get users from Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyseerr, 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 {
|
||||||
@ -60,14 +61,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("Couldn't get user from Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyseerr, 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("Couldn't get user's notification prefs from Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedGetJellyseerrNotificationPrefs, gc.Param("id"), err)
|
||||||
respond(500, "Couldn't get user notification prefs", gc)
|
respond(500, "Couldn't get user notification prefs", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -98,3 +99,67 @@ 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,6 +6,7 @@ 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"
|
||||||
)
|
)
|
||||||
@ -134,7 +135,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("Failed to get custom message with ID \"%s\"", id)
|
app.err.Printf(lm.FailedGetCustomMessage, id)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -328,7 +329,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("Failed to sync contact methods with Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
linkExistingOmbiDiscordTelegram(app)
|
linkExistingOmbiDiscordTelegram(app)
|
||||||
@ -361,11 +362,7 @@ 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 {
|
||||||
msg := ""
|
app.debug.Printf(lm.SetContactPrefForService, lm.Telegram, tgUser.Username, req.Telegram)
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -374,11 +371,7 @@ 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 {
|
||||||
msg := ""
|
app.debug.Printf(lm.SetContactPrefForService, lm.Discord, dcUser.Username, req.Discord)
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -387,11 +380,7 @@ 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 {
|
||||||
msg := ""
|
app.debug.Printf(lm.SetContactPrefForService, lm.Matrix, mxUser.UserID, req.Matrix)
|
||||||
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 {
|
||||||
@ -399,18 +388,14 @@ 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 {
|
||||||
msg := ""
|
app.debug.Printf(lm.SetContactPrefForService, lm.Email, email.Addr, req.Email)
|
||||||
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("Failed to sync contact prefs with Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
@ -446,7 +431,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.DeleteVerifiedUser(pin)
|
app.discord.DeleteVerifiedToken(pin)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -469,7 +454,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.ID) {
|
if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.MethodID().(string)) {
|
||||||
delete(app.discord.verifiedTokens, pin)
|
delete(app.discord.verifiedTokens, pin)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
@ -487,7 +472,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.inviteChannelName == "" {
|
if app.discord.InviteChannel.Name == "" {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -555,7 +540,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.Println("Matrix: Invite code was invalid")
|
app.debug.Printf(lm.InvalidInviteCode, code)
|
||||||
respondBool(401, false, gc)
|
respondBool(401, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -563,12 +548,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.Println("Matrix: PIN not found")
|
app.debug.Printf(lm.InvalidPIN, pin)
|
||||||
respondBool(200, false, gc)
|
respondBool(200, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if user.User.UserID != userID {
|
if user.User.UserID != userID {
|
||||||
app.debug.Println("Matrix: User ID of PIN didn't match")
|
app.debug.Printf(lm.UnauthorizedPIN, pin)
|
||||||
respondBool(200, false, gc)
|
respondBool(200, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -596,7 +581,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("Matrix: Failed to generate token: %v", err)
|
app.err.Printf(lm.FailedGenerateToken, err)
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -607,7 +592,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("Failed to save config to \"%s\": %v", app.configPath, err)
|
app.err.Printf(lm.FailedWriting, app.configPath, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -631,7 +616,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("Matrix: Failed to create room: %v", err)
|
app.err.Printf(lm.FailedCreateRoom, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -701,7 +686,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("Failed to sync contact methods with Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -739,7 +724,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("Failed to sync contact methods with Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -775,7 +760,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("Failed to sync contact methods with Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, 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,6 +22,14 @@ 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 {
|
||||||
@ -31,13 +39,13 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
|
|||||||
return ombiUser, code, err
|
return ombiUser, code, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, 400, fmt.Errorf("couldn't find user")
|
return nil, 400, errors.New(lm.NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (app *appContext) getOmbiImportedUser(name string) (map[string]interface{}, int, error) {
|
func (ombi *OmbiWrapper) getImportedUser(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 := app.ombi.GetUsers()
|
ombiUsers, code, err := ombi.GetUsers()
|
||||||
if err != nil || code != 200 {
|
if err != nil || code != 200 {
|
||||||
return nil, code, err
|
return nil, code, err
|
||||||
}
|
}
|
||||||
@ -66,10 +74,9 @@ func (app *appContext) getOmbiImportedUser(name string) (map[string]interface{},
|
|||||||
// @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("Failed to get users from Ombi (%d): %v", status, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
|
||||||
respond(500, "Couldn't get users", gc)
|
respond(500, "Couldn't get users", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -105,7 +112,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("Couldn't get user from Ombi (%d): %v", code, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
|
||||||
respond(500, "Couldn't get user", gc)
|
respond(500, "Couldn't get user", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -136,7 +143,11 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
|
|||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) applyOmbiProfile(user map[string]interface{}, profile map[string]interface{}) (status int, err error) {
|
type OmbiWrapper struct {
|
||||||
|
*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{}:
|
||||||
@ -147,6 +158,66 @@ func (app *appContext) applyOmbiProfile(user map[string]interface{}, profile map
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
status, err = app.ombi.ModifyUser(user)
|
status, err = 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,9 +1,11 @@
|
|||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -14,7 +16,6 @@ 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{},
|
||||||
@ -52,10 +53,11 @@ 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("Setting default profile to \"%s\"", req.Name)
|
app.info.Printf(lm.SetDefaultProfile, req.Name)
|
||||||
if _, ok := app.storage.GetProfileKey(req.Name); !ok {
|
if _, ok := app.storage.GetProfileKey(req.Name); !ok {
|
||||||
app.err.Printf("Profile not found: \"%s\"", req.Name)
|
msg := fmt.Sprintf(lm.FailedGetProfile, req.Name)
|
||||||
respond(500, "Profile not found", gc)
|
app.err.Println(msg)
|
||||||
|
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 {
|
||||||
@ -79,13 +81,12 @@ 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("Failed to get user from Jellyfin (%d): %v", status, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
respond(500, "Couldn't get user", gc)
|
respond(500, "Couldn't get user", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -94,12 +95,12 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
|
|||||||
Policy: user.Policy,
|
Policy: user.Policy,
|
||||||
Homescreen: req.Homescreen,
|
Homescreen: req.Homescreen,
|
||||||
}
|
}
|
||||||
app.debug.Printf("Creating profile from user \"%s\"", user.Name)
|
app.debug.Printf(lm.CreateProfileFromUser, 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("Failed to get DisplayPrefs (%d): %v", status, err)
|
app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err)
|
||||||
respond(500, "Couldn't get displayprefs", gc)
|
respond(500, "Couldn't get displayprefs", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -145,13 +146,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("\"%s\": Failed to enable referrals: invite not found", profileName)
|
app.err.Printf(lm.InvalidInviteCode, invCode)
|
||||||
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("\"%s\": Failed to enable referrals: profile not found", profileName)
|
app.err.Printf(lm.FailedGetProfile, profileName)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@ -9,6 +10,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/hrfee/jfa-go/jellyseerr"
|
"github.com/hrfee/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"
|
||||||
)
|
)
|
||||||
@ -29,7 +31,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("Failed to get Jellyfin user (%d): %+v\n", status, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
respond(500, "Failed to get user", gc)
|
respond(500, "Failed to get user", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -133,8 +135,9 @@ 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 {
|
||||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
msg := fmt.Sprintf(lm.FailedGetCookies, "user-refresh", err)
|
||||||
respond(500, "Couldn't fetch cookies", gc)
|
app.debug.Println(msg)
|
||||||
|
respond(500, msg, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||||
@ -174,21 +177,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("Failed to parse key: %s", err)
|
app.err.Printf(lm.FailedParseJWT, 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.Printf("Failed to parse key: %s", err)
|
app.err.Println(lm.FailedCastJWT)
|
||||||
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.Printf("Invalid key")
|
app.err.Println(lm.InvalidJWT)
|
||||||
fail()
|
fail()
|
||||||
// respond(400, "invalidKey", gc)
|
// respond(400, "invalidKey", gc)
|
||||||
return
|
return
|
||||||
@ -212,7 +215,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
|||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
}, gc, true)
|
}, gc, true)
|
||||||
|
|
||||||
app.info.Println("Email list modified")
|
app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId"))
|
||||||
gc.Redirect(http.StatusSeeOther, "/my/account")
|
gc.Redirect(http.StatusSeeOther, "/my/account")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -231,7 +234,6 @@ 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
|
||||||
@ -251,7 +253,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("Failed to generate confirmation token: %v", err)
|
app.err.Printf(lm.FailedSignJWT, err)
|
||||||
respond(500, "errorUnknown", gc)
|
respond(500, "errorUnknown", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -262,15 +264,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("%s: Email confirmation required", id)
|
app.debug.Printf(lm.EmailConfirmationRequired, 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("%s: Failed to construct confirmation email: %v", name, err)
|
app.err.Printf(lm.FailedConstructConfirmationEmail, id, 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("%s: Failed to send user confirmation email: %v", name, err)
|
app.err.Printf(lm.FailedSendConfirmationEmail, id, req.Email, err)
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf("%s: Sent user confirmation email to \"%s\"", name, req.Email)
|
app.err.Printf(lm.SentConfirmationEmail, id, req.Email)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -290,7 +292,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.inviteChannelName == "" {
|
if app.discord.InviteChannel.Name == "" {
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -338,7 +340,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.DeleteVerifiedUser(pin)
|
app.discord.DeleteVerifiedToken(pin)
|
||||||
if !ok {
|
if !ok {
|
||||||
respondBool(200, false, gc)
|
respondBool(200, false, gc)
|
||||||
return
|
return
|
||||||
@ -358,7 +360,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("Failed to sync contact methods with Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -413,7 +415,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("Failed to sync contact methods with Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -477,12 +479,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.Println("Matrix: PIN not found")
|
app.debug.Printf(lm.InvalidPIN, pin)
|
||||||
respondBool(200, false, gc)
|
respondBool(200, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if user.User.UserID != userID {
|
if user.User.UserID != userID {
|
||||||
app.debug.Println("Matrix: User ID of PIN didn't match")
|
app.debug.Printf(lm.UnauthorizedPIN, pin)
|
||||||
respondBool(200, false, gc)
|
respondBool(200, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -523,7 +525,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("Failed to sync contact methods with Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -551,7 +553,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("Failed to sync contact methods with Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||||
@ -606,7 +608,6 @@ 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
|
||||||
@ -616,7 +617,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("Ignoring PWR request: User not found")
|
app.debug.Printf(lm.FailedGetUsers, lm.Jellyfin, "no results")
|
||||||
|
|
||||||
for range timerWait {
|
for range timerWait {
|
||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
@ -626,7 +627,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("Failed to get user from Jellyfin: %v", err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
for range timerWait {
|
for range timerWait {
|
||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
return
|
return
|
||||||
@ -647,16 +648,16 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
|||||||
}, app, false,
|
}, app, false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf("Failed to construct password reset message for \"%s\": %v", pwr.Username, err)
|
app.err.Printf(lm.FailedConstructPWRMessage, 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("Failed to send password reset message to \"%s\": %v", address, err)
|
app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, "?", err)
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf("Sent password reset message to \"%s\"", address)
|
app.info.Printf(lm.SentPWRMessage, pwr.Username, "?")
|
||||||
}
|
}
|
||||||
for range timerWait {
|
for range timerWait {
|
||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
@ -683,14 +684,13 @@ 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("Failed to change password: couldn't find user (%d): %+v", status, err)
|
app.err.Printf(lm.FailedGetUser, gc.GetString("jfId"), lm.Jellyfin, 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("Failed to get user \"%s\" from ombi (%d): %v", user.Name, status, err)
|
app.err.Printf(lm.FailedGetUser, user.Name, lm.Ombi, 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("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
app.debug.Printf(lm.ChangePassword, lm.Ombi, 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("Couldn't get cookies: %s", err)
|
app.debug.Printf(lm.FailedGetCookies, "user-refresh", 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("Ignoring referral request, couldn't find template.")
|
app.debug.Printf(lm.FailedGetReferralTemplate, user.ReferralTemplateKey, err)
|
||||||
respondBool(400, false, gc)
|
respondBool(400, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -782,6 +782,7 @@ 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 = ""
|
||||||
@ -791,6 +792,7 @@ 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)
|
||||||
|
959
api-users.go
959
api-users.go
File diff suppressed because it is too large
Load Diff
63
api.go
63
api.go
@ -1,10 +1,12 @@
|
|||||||
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"
|
||||||
@ -122,14 +124,14 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !valid || req.PIN == "" {
|
if !valid || req.PIN == "" {
|
||||||
app.info.Printf("%s: Password reset failed: Invalid password", req.PIN)
|
app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", lm.InvalidPassword)
|
||||||
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("%s: PWR Failed: Captcha Incorrect", req.PIN)
|
app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", lm.IncorrectCaptcha)
|
||||||
respond(400, "errorCaptcha", gc)
|
respond(400, "errorCaptcha", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -138,7 +140,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("Password reset failed: PIN \"%s\" has expired", reset.PIN)
|
app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, reset.PIN))
|
||||||
respondBool(401, false, gc)
|
respondBool(401, false, gc)
|
||||||
delete(app.internalPWRs, req.PIN)
|
delete(app.internalPWRs, req.PIN)
|
||||||
return
|
return
|
||||||
@ -148,7 +150,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("Password Reset failed (%d): %v", status, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
|
||||||
respondBool(status, false, gc)
|
respondBool(status, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -156,7 +158,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("Password Reset failed (%d): %v", status, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
|
||||||
respondBool(status, false, gc)
|
respondBool(status, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -176,7 +178,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("Failed to get user \"%s\" (%d): %v", username, status, err)
|
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -195,31 +197,33 @@ 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("Failed to change password for \"%s\" (%d): %v", username, status, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, user.ID, 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) {
|
||||||
// Silently fail for changing ombi passwords
|
// This makes no sense so has been commented out.
|
||||||
|
// 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("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err)
|
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, 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("Failed to get user \"%s\" from ombi (%d): %v", username, status, err)
|
app.err.Printf(lm.FailedGetUser, user.ID, lm.Ombi, 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("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Ombi, user.ID, err)
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
app.debug.Printf(lm.ChangePassword, lm.Ombi, user.ID)
|
||||||
}
|
}
|
||||||
respondBool(200, true, gc)
|
respondBool(200, true, gc)
|
||||||
}
|
}
|
||||||
@ -231,7 +235,6 @@ 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()
|
||||||
@ -341,7 +344,6 @@ 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.
|
||||||
@ -366,26 +368,18 @@ 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("Failed to save config to \"%s\": %v", app.configPath, err)
|
app.err.Printf(lm.FailedWriting, app.configPath, err)
|
||||||
respond(500, err.Error(), gc)
|
respond(500, err.Error(), gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.debug.Println("Config saved")
|
app.info.Printf(lm.ModifyConfig, app.configPath)
|
||||||
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.info.Println("Restarting...")
|
app.Restart()
|
||||||
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),
|
||||||
@ -425,12 +419,13 @@ 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 {
|
||||||
respond(400, "Update is manual", gc)
|
app.info.Printf(lm.FailedApplyUpdate, lm.UpdateManual)
|
||||||
|
respond(400, lm.UpdateManual, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := app.update.update()
|
err := app.update.update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf("Failed to apply update: %v", err)
|
app.err.Printf(lm.FailedApplyUpdate, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -452,8 +447,9 @@ 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 {
|
||||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
msg := fmt.Sprintf(lm.FailedGetCookies, "refresh", err)
|
||||||
respond(500, "Couldn't fetch cookies", gc)
|
app.debug.Println(msg)
|
||||||
|
respond(500, msg, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||||
@ -526,11 +522,7 @@ 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.info.Println("Restarting...")
|
app.Restart()
|
||||||
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.
|
||||||
@ -544,6 +536,7 @@ 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,6 +9,7 @@ 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"
|
||||||
)
|
)
|
||||||
@ -41,6 +42,8 @@ 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
|
||||||
@ -72,32 +75,26 @@ 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.debug.Println("Invalid authorization header")
|
app.authLog(lm.InvalidAuthHeader)
|
||||||
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.debug.Printf("Auth denied: %s", err)
|
app.authLog(fmt.Sprintf(lm.FailedParseJWT, 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.debug.Println("Invalid JWT")
|
app.authLog(lm.FailedCastJWT)
|
||||||
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.debug.Printf("Auth denied: Invalid token")
|
app.authLog(lm.InvalidJWT)
|
||||||
// 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
|
||||||
@ -115,7 +112,7 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
isAdminToken := claims["admin"].(bool)
|
isAdminToken := claims["admin"].(bool)
|
||||||
if !isAdminToken {
|
if !isAdminToken {
|
||||||
app.debug.Printf("Auth denied: Token was not for admin access")
|
app.authLog(lm.NonAdminToken)
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -130,14 +127,13 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !match {
|
if !match {
|
||||||
app.debug.Printf("Couldn't find user ID \"%s\"", userID)
|
app.authLog(fmt.Sprintf(lm.NonAdminUser, 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +156,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, "Auth denied: blank username/password")
|
app.logIpDebug(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.EmptyUserOrPass))
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -173,16 +169,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, "Auth denied: Invalid username/password (Jellyfin)")
|
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if status == 403 {
|
if status == 403 {
|
||||||
app.logIpInfo(gc, userpage, "Auth denied: Jellyfin account disabled")
|
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.UserDisabled))
|
||||||
respond(403, "yourAccountWasDisabled", gc)
|
respond(403, "yourAccountWasDisabled", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
|
app.authLog(fmt.Sprintf(lm.FailedAuthJellyfin, app.jf.Server, status, err))
|
||||||
respond(500, "Jellyfin error", gc)
|
respond(500, "Jellyfin error", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -199,7 +195,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, "Token requested (login attempt)")
|
app.logIpInfo(gc, false, fmt.Sprintf(lm.RequestingToken, lm.TokenLoginAttempt))
|
||||||
username, password, ok := app.decodeValidateLoginHeader(gc, false)
|
username, password, ok := app.decodeValidateLoginHeader(gc, false)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@ -209,13 +205,12 @@ 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, "Auth denied: Invalid username/password")
|
app.logIpInfo(gc, false, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -233,7 +228,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.debug.Printf("Auth denied: Users \"%s\" isn't admin", username)
|
app.authLog(fmt.Sprintf(lm.NonAdminUser, username))
|
||||||
respond(401, "Unauthorized", gc)
|
respond(401, "Unauthorized", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -243,12 +238,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|||||||
newUser := User{
|
newUser := User{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
app.debug.Printf("Token generated for user \"%s\"", username)
|
app.debug.Printf(lm.GenerateToken, 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("getToken failed: Couldn't generate token (%s)", err)
|
app.err.Printf(lm.FailedGenerateToken, err)
|
||||||
respond(500, "Couldn't generate token", gc)
|
respond(500, "Couldn't generate token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -261,35 +256,29 @@ 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.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err)
|
app.authLog(fmt.Sprintf(lm.FailedGetCookies, cookieName, 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.debug.Println("getTokenRefresh: Invalid token")
|
app.authLog(lm.LocallyInvalidatedJWT)
|
||||||
respond(401, "Invalid token", gc)
|
respond(401, lm.InvalidJWT, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
token, err := jwt.Parse(cookie, checkToken)
|
token, err := jwt.Parse(cookie, checkToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.debug.Println("getTokenRefresh: Invalid token")
|
app.authLog(lm.FailedParseJWT)
|
||||||
respond(400, "Invalid token", gc)
|
respond(400, lm.InvalidJWT, 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.debug.Printf("getTokenRefresh: Invalid token: %+v", err)
|
app.authLog(lm.InvalidJWT)
|
||||||
respond(401, "Invalid token", gc)
|
respond(401, lm.InvalidJWT, gc)
|
||||||
ok = false
|
ok = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -304,7 +293,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, "Token requested (refresh token)")
|
app.logIpInfo(gc, false, fmt.Sprintf(lm.RequestingToken, lm.TokenRefresh))
|
||||||
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
|
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@ -313,7 +302,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("getTokenRefresh failed: Couldn't generate token (%s)", err)
|
app.err.Printf(lm.FailedGenerateToken, err)
|
||||||
respond(500, "Couldn't generate token", gc)
|
respond(500, "Couldn't generate token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
31
backups.go
31
backups.go
@ -7,6 +7,8 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -60,12 +62,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("Failed to create backup directory \"%s\": %v\n", path, err)
|
app.err.Printf(lm.FailedCreateDir, 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("Failed to read backup directory \"%s\": %v\n", path, err)
|
app.err.Printf(lm.FailedReading, path, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
backups := &BackupList{}
|
backups := &BackupList{}
|
||||||
@ -78,7 +80,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("Failed to parse backup filename \"%s\": %v\n", item.Name(), err)
|
app.debug.Printf(lm.FailedParseTime, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
backups.dates[i] = t
|
backups.dates[i] = t
|
||||||
@ -101,36 +103,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("Failed to delete old backup \"%s\": %v\n", fullpath, err)
|
app.err.Printf(lm.FailedDeleteOldBackup, 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("Failed to open backup file \"%s\": %v\n", fullpath, err)
|
app.err.Printf(lm.FailedOpen, 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("Failed to create backup: %v\n", err)
|
app.err.Printf(lm.FailedCreateBackup, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fstat, err := f.Stat()
|
fstat, err := f.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf("Failed to get info on new backup: %v\n", err)
|
app.err.Printf(lm.FailedStat, fullpath, 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
|
||||||
// fmt.Printf("Created backup %+v\n", fileDetails)
|
app.debug.Printf(lm.CreateBackup, fileDetails)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,25 +141,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("Failed to move existing database: %v\n", err)
|
app.err.Fatalf(lm.FailedMoveOldDB, oldPath, 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("Failed to open backup file \"%s\": %v\n", LOADBAK, err)
|
app.err.Fatalf(lm.FailedOpen, 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("Failed to restore backup file \"%s\": %v\n", LOADBAK, err)
|
app.err.Fatalf(lm.FailedRestoreDB, LOADBAK, err)
|
||||||
}
|
}
|
||||||
app.info.Printf("Restored backup \"%s\".", LOADBAK)
|
app.info.Printf(lm.RestoreDB, LOADBAK)
|
||||||
LOADBAK = ""
|
LOADBAK = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +167,6 @@ 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,8 +7,10 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -140,7 +142,7 @@ func (app *appContext) loadConfig() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if allDisabled {
|
if allDisabled {
|
||||||
fmt.Println("SETALLTRUE")
|
app.info.Println(lm.EnableAllPWRMethods)
|
||||||
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")
|
||||||
}
|
}
|
||||||
@ -175,9 +177,15 @@ 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("Failed to initialize Proxy: %v\n", err)
|
app.err.Printf(lm.FailedInitProxy, app.proxyConfig.Addr, 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,6 +1120,15 @@
|
|||||||
"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,
|
||||||
|
347
discord.go
347
discord.go
@ -6,25 +6,26 @@ 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.
|
||||||
channelID, channelName, inviteChannelID, inviteChannelName string
|
Channel, InviteChannel struct{ ID, Name 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) {
|
||||||
@ -92,13 +93,11 @@ 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("Discord: Failed to start daemon: %v", err)
|
d.app.err.Printf(lm.FailedStartDaemon, lm.Discord, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Wait for everything to populate, it's slow sometimes.
|
// Wait for everything to populate, it's slow sometimes.
|
||||||
@ -116,17 +115,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("Discord: Failed to get guild: %v", err)
|
d.app.err.Printf(lm.FailedGetDiscordGuild, 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.channelName = channel
|
d.Channel.Name = 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.inviteChannelName = invChannel
|
d.InviteChannel.Name = 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"))
|
||||||
@ -145,7 +144,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("Discord: Failed to get roles: %v", err)
|
d.app.err.Printf(lm.FailedGetDiscordRoles, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, role := range r {
|
for _, role := range r {
|
||||||
@ -168,44 +167,62 @@ 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.inviteChannelName == "" {
|
if d.InviteChannel.Name == "" {
|
||||||
d.app.err.Println("Discord: Cannot create invite without channel specified in settings.")
|
d.app.err.Println(lm.FailedCreateDiscordInviteChannel, lm.InviteChannelEmpty)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if d.inviteChannelID == "" {
|
if d.InviteChannel.ID == "" {
|
||||||
channels, err := d.bot.GuildChannels(d.guildID)
|
channels, err := d.bot.GuildChannels(d.guildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Couldn't get channel list: %v", err)
|
d.app.err.Printf(lm.FailedGetDiscordChannels, 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("Discord: Couldn't get channel: %v", err)
|
// d.app.err.Printf(lm.FailedGetDiscordChannel, ch.ID, err)
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
if channel.Name == d.inviteChannelName {
|
if channel.Name == d.InviteChannel.Name {
|
||||||
d.inviteChannelID = channel.ID
|
d.InviteChannel.ID = channel.ID
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
d.app.err.Printf("Discord: Couldn't find invite channel \"%s\"", d.inviteChannelName)
|
d.app.err.Printf(lm.FailedGetDiscordChannel, d.InviteChannel.Name, lm.NotFound)
|
||||||
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("Discord: Couldn't get invite channel: %v", err)
|
// d.app.err.Printf(lm.FailedGetDiscordChannel, d.inviteChannelID, err)
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
inv, err = d.bot.ChannelInviteCreate(d.inviteChannelID, dg.Invite{
|
inv, err = d.bot.ChannelInviteCreate(d.InviteChannel.ID, 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,
|
||||||
@ -214,13 +231,13 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
|
|||||||
Temporary: false,
|
Temporary: false,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to create invite: %v", err)
|
d.app.err.Printf(lm.FailedGenerateDiscordInvite, 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("Discord: Failed to get guild: %v", err)
|
d.app.err.Printf(lm.FailedGetDiscordGuild, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
iconURL = guild.IconURL("256")
|
iconURL = guild.IconURL("256")
|
||||||
@ -255,7 +272,7 @@ func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
|
|||||||
1000,
|
1000,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to get members: %v", err)
|
d.app.err.Printf(lm.FailedGetDiscordGuildMembers, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
hasDiscriminator := strings.Contains(username, "#")
|
hasDiscriminator := strings.Contains(username, "#")
|
||||||
@ -285,7 +302,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("Discord: Failed to get user: %v", err)
|
d.app.err.Printf(lm.FailedGetUser, ID, lm.Discord, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.ID = ID
|
user.ID = ID
|
||||||
@ -294,7 +311,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("Discord: Failed to create DM channel: %v", err)
|
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, ID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.ChannelID = channel.ID
|
user.ChannelID = channel.ID
|
||||||
@ -381,7 +398,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("Discord: registering lang choice \"%s\":\"%s\"\n", d.app.storage.lang.Telegram[code].Meta.Name, code)
|
d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Lang, 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,
|
||||||
@ -392,7 +409,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("Discord: registering profile choice \"%s\"", profile.Name)
|
d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Profile, 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,
|
||||||
@ -409,9 +426,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("Discord: Cannot create command \"%s\": %v", cmd.Name, err)
|
d.app.err.Printf(lm.FailedRegisterDiscordCommand, cmd.Name, err)
|
||||||
} else {
|
} else {
|
||||||
d.app.debug.Printf("Discord: registered command \"%s\"", cmd.Name)
|
d.app.debug.Printf(lm.RegisterDiscordCommand, cmd.Name)
|
||||||
d.commandIDs[i] = command.ID
|
d.commandIDs[i] = command.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -420,12 +437,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("Discord: Failed to get commands: %v", err)
|
d.app.err.Printf(lm.FailedGetDiscordCommands, 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("Discord: Failed to deregister command: %v", err)
|
d.app.err.Printf(lm.FailedDeregDiscordCommand, cmd.Name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -436,7 +453,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("Discord: registering profile choice \"%s\"", profile.Name)
|
d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Profile, 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,
|
||||||
@ -444,7 +461,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("Discord: Failed to update profile list: %v\n", err)
|
d.app.err.Printf(lm.FailedRegisterDiscordChoices, lm.Profile, err)
|
||||||
} else {
|
} else {
|
||||||
d.commandIDs[3] = cmd.ID
|
d.commandIDs[3] = cmd.ID
|
||||||
}
|
}
|
||||||
@ -452,19 +469,20 @@ 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.channelName != "" {
|
if i.GuildID != "" && d.Channel.Name != "" {
|
||||||
if d.channelID == "" {
|
if d.Channel.ID == "" {
|
||||||
channel, err := s.Channel(i.ChannelID)
|
channel, err := s.Channel(i.ChannelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
|
d.app.err.Printf(lm.FailedGetDiscordChannel, i.ChannelID, err)
|
||||||
d.channelName = ""
|
d.app.err.Println(lm.MonitorAllDiscordChannels)
|
||||||
|
d.Channel.Name = ""
|
||||||
}
|
}
|
||||||
if channel.Name == d.channelName {
|
if channel.Name == d.Channel.Name {
|
||||||
d.channelID = channel.ID
|
d.Channel.ID = channel.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if d.channelID != i.ChannelID {
|
if d.Channel.ID != i.ChannelID {
|
||||||
d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
|
d.app.debug.Printf(lm.IgnoreOutOfChannelMessage, lm.Discord)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -486,7 +504,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("Discord: Failed to create private channel with \"%s\": %v", i.Interaction.Member.User.Username, err)
|
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, i.Interaction.Member.User.ID, 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)
|
||||||
@ -503,7 +521,7 @@ func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang st
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send reply: %v", err)
|
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -521,7 +539,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
|
||||||
}
|
}
|
||||||
delete(d.tokens, pin)
|
delete(d.tokens, pin)
|
||||||
return
|
return
|
||||||
@ -535,7 +553,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, 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
|
||||||
@ -566,7 +584,7 @@ func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang str
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send reply: %v", err)
|
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -575,7 +593,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("Discord: Failed to create private channel with \"%s\": %v", i.Interaction.Member.User.Username, err)
|
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, i.Interaction.Member.User.ID, 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)
|
||||||
@ -590,12 +608,9 @@ 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 {
|
if !(ok && requesterEmail.Admin) {
|
||||||
d.app.err.Printf("Failed to verify admin")
|
d.app.err.Printf(lm.FailedGenerateInvite, fmt.Sprintf(lm.NonAdminUser, requester.JellyfinID))
|
||||||
}
|
// FIXME: add response message
|
||||||
if !requesterEmail.Admin {
|
|
||||||
d.app.err.Printf("User is not admin")
|
|
||||||
//add response message
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -629,7 +644,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("Discord: %s", RenderDiscordUsername(recipient)),
|
Label: fmt.Sprintf("%s: %s", lm.Discord, RenderDiscordUsername(recipient)),
|
||||||
}
|
}
|
||||||
if profileName != "" {
|
if profileName != "" {
|
||||||
if _, ok := d.app.storage.GetProfileKey(profileName); ok {
|
if _, ok := d.app.storage.GetProfileKey(profileName); ok {
|
||||||
@ -638,13 +653,12 @@ 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("Failed to send to %s", RenderDiscordUsername(recipient))
|
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, invite.Code, err)
|
||||||
d.app.err.Printf("%s: Failed to construct invite message: %v", invite.Code, err)
|
d.app.err.Println(invite.SendTo)
|
||||||
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{
|
||||||
@ -653,14 +667,14 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
|
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, 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("Failed to send to %s", RenderDiscordUsername(recipient))
|
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, RenderDiscordUsername(recipient), err)
|
||||||
d.app.err.Printf("%s: %s: %v", invite.Code, invite.SendTo, err)
|
d.app.err.Println(invite.SendTo)
|
||||||
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{
|
||||||
@ -669,10 +683,10 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
|
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
d.app.info.Printf("%s: Sent invite email to \"%s\"", invite.Code, RenderDiscordUsername(recipient))
|
d.app.info.Printf(lm.SentInviteMessage, 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{
|
||||||
@ -681,7 +695,7 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
|
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -690,140 +704,6 @@ 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 {
|
||||||
@ -877,10 +757,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) (user DiscordUser, ok bool) {
|
func (d *DiscordDaemon) UserVerified(pin string) (ContactMethodUser, bool) {
|
||||||
user, ok = d.verifiedTokens[pin]
|
u, ok := d.verifiedTokens[pin]
|
||||||
// delete(d.verifiedTokens, pin)
|
// delete(d.verifiedTokens, pin)
|
||||||
return
|
return &u, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@ -900,7 +780,44 @@ func (d *DiscordDaemon) UserExists(id string) bool {
|
|||||||
return err != nil || c > 0
|
return err != nil || c > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteVerifiedUser removes the token with the given PIN.
|
// Exists returns whether or not the given user exists.
|
||||||
func (d *DiscordDaemon) DeleteVerifiedUser(pin string) {
|
func (d *DiscordDaemon) Exists(user ContactMethodUser) bool {
|
||||||
delete(d.verifiedTokens, pin)
|
return d.UserExists(user.MethodID().(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,6 +20,7 @@ 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"
|
||||||
@ -95,7 +96,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("Error while initiating SMTP mailer: %v", err)
|
app.err.Printf(lm.FailedInitSMTP, 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())
|
||||||
@ -580,7 +581,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("Couldn't generate PWR link: %v", err)
|
app.info.Println(lm.FailedGeneratePWRLink, err)
|
||||||
template["pin"] = pwr.Pin
|
template["pin"] = pwr.Pin
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"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
|
||||||
|
|
||||||
@ -36,7 +40,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("%s started", d.name)
|
d.app.info.Printf(lm.StartDaemon, 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.20
|
go 1.22.4
|
||||||
|
|
||||||
replace github.com/hrfee/jfa-go/docs => ./docs
|
replace github.com/hrfee/jfa-go/docs => ./docs
|
||||||
|
|
||||||
@ -10,6 +10,8 @@ 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
|
||||||
@ -35,7 +37,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-20230626224816-f72960635dc3
|
github.com/hrfee/jfa-go/logger v0.0.0-20240731152135-2d066ea7cd32
|
||||||
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
|
||||||
@ -91,6 +93,7 @@ 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
|
||||||
|
@ -4,6 +4,7 @@ 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"
|
||||||
)
|
)
|
||||||
@ -12,7 +13,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("Housekeeping: removing unused email addresses")
|
app.debug.Println(lm.HousekeepingEmail)
|
||||||
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)
|
||||||
@ -28,15 +29,20 @@ 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("Housekeeping: removing unused Discord IDs")
|
app.debug.Println(lm.HousekeepingDiscord)
|
||||||
discordUsers := app.storage.GetDiscord()
|
discordUsers := app.storage.GetDiscord()
|
||||||
for _, discordUser := range discordUsers {
|
for _, discordUser := range discordUsers {
|
||||||
_, _, err := app.jf.UserByID(discordUser.JellyfinID, false)
|
user, _, 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,7 +50,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("Housekeeping: removing unused Matrix IDs")
|
app.debug.Println(lm.HousekeepingMatrix)
|
||||||
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)
|
||||||
@ -60,7 +66,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("Housekeeping: removing unused Telegram IDs")
|
app.debug.Println(lm.HousekeepingTelegram)
|
||||||
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)
|
||||||
@ -75,7 +81,7 @@ func (app *appContext) clearTelegram() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) clearPWRCaptchas() {
|
func (app *appContext) clearPWRCaptchas() {
|
||||||
app.debug.Println("Housekeeping: Clearing old PWR Captchas")
|
app.debug.Println(lm.HousekeepingCaptcha)
|
||||||
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()) {
|
||||||
@ -86,7 +92,7 @@ func (app *appContext) clearPWRCaptchas() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) clearActivities() {
|
func (app *appContext) clearActivities() {
|
||||||
app.debug.Println("Housekeeping: Cleaning up Activity log...")
|
app.debug.Println(lm.HousekeepingActivity)
|
||||||
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)
|
||||||
@ -103,7 +109,7 @@ func (app *appContext) clearActivities() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == badger.ErrTxnTooBig {
|
if err == badger.ErrTxnTooBig {
|
||||||
app.debug.Printf("Activities: Delete txn was too big, doing it manually.")
|
app.debug.Printf(lm.ActivityLogTxnTooBig)
|
||||||
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))
|
||||||
@ -119,7 +125,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("Housekeeping: Checking for expired invites")
|
app.debug.Println(lm.HousekeepingInvites)
|
||||||
app.checkInvites()
|
app.checkInvites()
|
||||||
},
|
},
|
||||||
func(app *appContext) { app.clearActivities() },
|
func(app *appContext) { app.clearActivities() },
|
||||||
@ -128,7 +134,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)
|
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false) || app.config.Section("discord").Key("disable_enable_role").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)
|
@ -246,6 +246,7 @@
|
|||||||
<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>
|
||||||
@ -258,6 +259,23 @@
|
|||||||
<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>
|
||||||
@ -423,6 +441,7 @@
|
|||||||
<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>
|
||||||
@ -541,4 +560,3 @@
|
|||||||
<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
Executable file → Normal file
0
images/jfa-go-icon.png
Executable file → Normal file
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
0
images/jfa-go-icon.svg
Executable file → Normal file
0
images/jfa-go-icon.svg
Executable file → Normal file
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
@ -5,20 +5,21 @@ 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("Failed to get or trigger import for Jellyseerr (user \"%s\"): %v", jfID, err)
|
app.debug.Printf(lm.FailedImportUser, lm.Jellyseerr, jfID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if imported {
|
if imported {
|
||||||
app.debug.Printf("Jellyseerr: Triggered import for Jellyfin user \"%s\" (ID %d)", jfID, user.ID)
|
app.debug.Printf(lm.ImportJellyseerrUser, 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("Failed to get notification prefs for Jellyseerr (user \"%s\"): %v", jfID, err)
|
app.debug.Printf(lm.FailedGetJellyseerrNotificationPrefs, jfID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +28,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("Failed to set Jellyseerr email address: %v\n", err)
|
app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err)
|
||||||
} else {
|
} else {
|
||||||
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
|
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
|
||||||
}
|
}
|
||||||
@ -51,7 +52,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("Failed to sync contact methods with Jellyseerr: %v", err)
|
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,7 +60,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("Failed to get users (%d): %s", status, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// I'm sure Jellyseerr can handle it,
|
// I'm sure Jellyseerr can handle it,
|
1
lang.go
1
lang.go
@ -122,6 +122,7 @@ 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"`
|
||||||
|
88
lang/form/ckb-iq.json
Normal file
88
lang/form/ckb-iq.json
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"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 untenstehende PIN an den Bot und komm dann hierher zurück, um dein Konto zu verbinden.",
|
"sendPIN": "Sende die PIN unten an den Bot und komm dann hierher zurück, um dein Konto zu verknüpfen.",
|
||||||
"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,7 +30,20 @@
|
|||||||
"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": {
|
||||||
@ -67,6 +80,10 @@
|
|||||||
"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 subdomain (e.g 'jellyf.in/accounts').",
|
"urlBaseNotice": "Only needed if using a reverse proxy on a subfolder (e.g 'jellyf.in/accounts').",
|
||||||
"lightTheme": "Light",
|
"lightTheme": "Light",
|
||||||
"darkTheme": "Dark",
|
"darkTheme": "Dark",
|
||||||
"useHTTPS": "Use HTTPS",
|
"useHTTPS": "Use HTTPS",
|
||||||
@ -94,7 +94,13 @@
|
|||||||
"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",
|
||||||
@ -134,6 +140,7 @@
|
|||||||
"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",
|
||||||
|
3
logmessages/go.mod
Normal file
3
logmessages/go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module github.com/hrfee/logmessages
|
||||||
|
|
||||||
|
go 1.22.4
|
371
logmessages/logmessages.go
Normal file
371
logmessages/logmessages.go
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
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,6 +27,7 @@ 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"
|
||||||
@ -101,8 +102,9 @@ 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 *ombi.Ombi
|
ombi *OmbiWrapper
|
||||||
js *jellyseerr.Jellyseerr
|
js *JellyseerrWrapper
|
||||||
|
thirdPartyServices []ThirdPartyService
|
||||||
datePattern string
|
datePattern string
|
||||||
timePattern string
|
timePattern string
|
||||||
storage Storage
|
storage Storage
|
||||||
@ -111,6 +113,7 @@ 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
|
||||||
@ -213,22 +216,21 @@ 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("Couldn't find default config file")
|
app.err.Fatalf(lm.NoConfig)
|
||||||
}
|
}
|
||||||
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.Printf("Couldn't open config file for writing: \"%s\"", app.configPath)
|
app.err.Fatalf(lm.FailedWriting, app.configPath, err)
|
||||||
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("Couldn't copy default config.")
|
app.err.Fatalf(lm.FailedCopyConfig, app.configPath, err)
|
||||||
}
|
}
|
||||||
app.info.Printf("Copied default configuration to \"%s\"", app.configPath)
|
app.info.Printf(lm.CopyConfig, 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)
|
||||||
@ -237,8 +239,9 @@ 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("Failed to load config file \"%s\": %v", app.configPath, err)
|
app.err.Fatalf(lm.FailedLoadConfig, 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
|
||||||
@ -270,7 +273,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("Couldn't establish socket connection at %s\n", SOCK)
|
app.err.Fatalf(lm.FailedSocketConnect, SOCK, err)
|
||||||
}
|
}
|
||||||
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)
|
||||||
@ -286,13 +289,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("Couldn't read message on %s: %s", SOCK, err)
|
app.err.Printf(lm.FailedSocketRead, 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("Couldn't read message on %s: %s", SOCK, err)
|
app.err.Printf(lm.FailedSocketRead, SOCK, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
command := string(buf[0:nr])
|
command := string(buf[0:nr])
|
||||||
@ -317,13 +320,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("Failed to load language files: %+v\n", err)
|
app.info.Fatalf(lm.FailedLangLoad, 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("Using TLS/HTTP2")
|
app.info.Println(lm.UsingTLS)
|
||||||
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)
|
||||||
@ -348,29 +351,32 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
||||||
|
|
||||||
app.debug.Printf("Loaded config file \"%s\"", app.configPath)
|
// NOTE: As of writing this, the order in app.thirdPartServices doesn't matter,
|
||||||
|
// 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.debug.Printf("Connecting to Ombi")
|
app.ombi = &OmbiWrapper{}
|
||||||
|
app.debug.Printf(lm.UsingOmbi)
|
||||||
ombiServer := app.config.Section("ombi").Key("server").String()
|
ombiServer := app.config.Section("ombi").Key("server").String()
|
||||||
app.ombi = ombi.NewOmbi(
|
app.ombi.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.debug.Printf("Connecting to Jellyseerr")
|
app.js = &JellyseerrWrapper{}
|
||||||
|
app.debug.Printf(lm.UsingJellyseerr)
|
||||||
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
|
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
|
||||||
app.js = jellyseerr.NewJellyseerr(
|
app.js.Jellyseerr = 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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,10 +404,9 @@ 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("Using Emby server type")
|
app.info.Println(lm.UsingEmby)
|
||||||
fmt.Println(warning("WARNING: Emby compatibility is experimental, and support is limited.\nPassword resets are not available."))
|
|
||||||
} else {
|
} else {
|
||||||
app.info.Println("Using Jellyfin server type")
|
app.info.Println(lm.UsingJellyfin)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.jf, err = mediabrowser.NewServer(
|
app.jf, err = mediabrowser.NewServer(
|
||||||
@ -415,7 +420,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
cacheTimeout,
|
cacheTimeout,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ \"%s\": %v", server, err)
|
app.err.Fatalf(lm.FailedAuthJellyfin, server, -1, err)
|
||||||
}
|
}
|
||||||
if debugMode {
|
if debugMode {
|
||||||
app.jf.Verbose = true
|
app.jf.Verbose = true
|
||||||
@ -433,9 +438,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("Failed to authenticate with Jellyfin @ \"%s\" (%d): %v", server, status, err)
|
app.err.Fatalf(lm.FailedAuthJellyfin, server, status, err)
|
||||||
}
|
}
|
||||||
app.info.Printf("Authenticated with \"%s\"", server)
|
app.info.Printf(lm.AuthJellyfin, server)
|
||||||
|
|
||||||
runMigrations(app)
|
runMigrations(app)
|
||||||
|
|
||||||
@ -448,8 +453,9 @@ 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("Using Jellyfin for authentication")
|
app.debug.Println(lm.UsingJellyfinAuth)
|
||||||
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
|
||||||
@ -512,34 +518,42 @@ func start(asDaemon, firstCall bool) {
|
|||||||
defer backupDaemon.Shutdown()
|
defer backupDaemon.Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
if telegramEnabled {
|
// NOTE: The order in which these are placed in app.contactMethods matters.
|
||||||
app.telegram, err = newTelegramDaemon(app)
|
// Add new ones to the end.
|
||||||
if err != nil {
|
|
||||||
app.err.Printf("Failed to authenticate with Telegram: %v", err)
|
|
||||||
telegramEnabled = false
|
|
||||||
} else {
|
|
||||||
go app.telegram.run()
|
|
||||||
defer app.telegram.Shutdown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if discordEnabled {
|
if discordEnabled {
|
||||||
app.discord, err = newDiscordDaemon(app)
|
app.discord, err = newDiscordDaemon(app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf("Failed to authenticate with Discord: %v", err)
|
app.err.Printf(lm.FailedInitDiscord, err)
|
||||||
discordEnabled = false
|
discordEnabled = false
|
||||||
} else {
|
} else {
|
||||||
|
app.debug.Println(lm.InitDiscord)
|
||||||
go app.discord.run()
|
go app.discord.run()
|
||||||
defer app.discord.Shutdown()
|
defer app.discord.Shutdown()
|
||||||
|
app.contactMethods = append(app.contactMethods, app.discord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if telegramEnabled {
|
||||||
|
app.telegram, err = newTelegramDaemon(app)
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf(lm.FailedInitTelegram, err)
|
||||||
|
telegramEnabled = false
|
||||||
|
} else {
|
||||||
|
app.debug.Println(lm.InitTelegram)
|
||||||
|
go app.telegram.run()
|
||||||
|
defer app.telegram.Shutdown()
|
||||||
|
app.contactMethods = append(app.contactMethods, app.telegram)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if matrixEnabled {
|
if matrixEnabled {
|
||||||
app.matrix, err = newMatrixDaemon(app)
|
app.matrix, err = newMatrixDaemon(app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf("Failed to initialize Matrix daemon: %v", err)
|
app.err.Printf(lm.FailedInitMatrix, 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 {
|
||||||
@ -558,7 +572,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("Failed to load language files: %+v\n", err)
|
app.info.Fatalf(lm.FailedLangLoad, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,14 +580,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("Initializing router")
|
app.info.Println(lm.InitRouter)
|
||||||
router := app.loadRouter(address, debugMode)
|
router := app.loadRouter(address, debugMode)
|
||||||
app.info.Println("Loading routes")
|
app.info.Println(lm.LoadRoutes)
|
||||||
if !firstRun {
|
if !firstRun {
|
||||||
app.loadRoutes(router)
|
app.loadRoutes(router)
|
||||||
} else {
|
} else {
|
||||||
app.loadSetup(router)
|
app.loadSetup(router)
|
||||||
app.info.Printf("Loading setup @ %s", address)
|
app.info.Printf(lm.LoadingSetup, address)
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
||||||
@ -581,45 +595,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{"Certificate", "Key"}
|
fileNames := []string{lm.InvalidSSLCert, lm.InvalidSSLKey}
|
||||||
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("SSL/TLS %s: %v\n", fileNames[i], err)
|
app.err.Printf(fileNames[i], v, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == http.ErrServerClosed {
|
if err == http.ErrServerClosed {
|
||||||
app.err.Printf("Failure serving with SSL/TLS: %s", err)
|
app.err.Printf(lm.FailServeSSL, err)
|
||||||
} else {
|
} else {
|
||||||
app.err.Fatalf("Failure serving with SSL/TLS: %s", err)
|
app.err.Fatalf(lm.FailServeSSL, 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("Failure serving: %s", err)
|
app.err.Printf(lm.FailServe, err)
|
||||||
} else {
|
} else {
|
||||||
app.err.Fatalf("Failure serving: %s", err)
|
app.err.Fatalf(lm.FailServe, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if firstRun {
|
if firstRun {
|
||||||
app.info.Printf("Loaded, visit %s to start.", address)
|
app.info.Printf(lm.ServingSetup, address)
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf("Loaded @ %s", address)
|
app.info.Printf(lm.Serving, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForRestart()
|
waitForRestart()
|
||||||
|
|
||||||
app.info.Printf("Restart/Quit signal received, give me a second!")
|
app.info.Printf(lm.QuitReceived)
|
||||||
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("Server shutdown error: %s", err)
|
app.err.Fatalf(lm.FailedQuit, err)
|
||||||
}
|
}
|
||||||
app.info.Println("Server shut down.")
|
app.info.Println(lm.Quit)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -631,7 +645,7 @@ func shutdown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) shutdown() {
|
func (app *appContext) shutdown() {
|
||||||
app.info.Println("Shutting down...")
|
app.info.Println(lm.Quitting)
|
||||||
shutdown()
|
shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -715,15 +729,17 @@ 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("Failed to start logging: %v\n", err)
|
fmt.Printf(lm.FailedLogging, err)
|
||||||
}
|
}
|
||||||
defer f()
|
defer f()
|
||||||
printVersion()
|
printVersion()
|
||||||
SOCK = filepath.Join(temp, SOCK)
|
SOCK = filepath.Join(temp, SOCK)
|
||||||
fmt.Println("Socket:", SOCK)
|
fmt.Printf(lm.SocketPath+"\n", SOCK)
|
||||||
if flagPassed("test") {
|
if flagPassed("test") {
|
||||||
TEST = true
|
TEST = true
|
||||||
}
|
}
|
||||||
@ -752,24 +768,26 @@ 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("Couldn't dial socket %s, are you sure jfa-go is running?\n", SOCK)
|
fmt.Printf(lm.FailedSocketConnect+"\n", SOCK, err)
|
||||||
|
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("Couldn't send command to socket %s, are you sure jfa-go is running?\n", SOCK)
|
fmt.Printf(lm.FailedSocketWrite+"\n", SOCK, err)
|
||||||
|
fmt.Println(lm.SocketCheckRunning)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Println("Sent.")
|
fmt.Println(lm.SocketWrite)
|
||||||
} 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, "jfa-go.service")
|
service, err := fs.ReadFile(localFS, SYSTEMD_SERVICE)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Couldn't read jfa-go.service: %v\n", err)
|
fmt.Printf(lm.FailedReading+"\n", SYSTEMD_SERVICE, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
absPath, err := filepath.Abs(os.Args[0])
|
absPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
absPath = os.Args[0]
|
absPath = os.Args[0]
|
||||||
}
|
}
|
||||||
@ -780,13 +798,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
service = []byte(strings.Replace(string(service), "{executable}", command, 1))
|
service = []byte(strings.Replace(string(service), "{executable}", command, 1))
|
||||||
err = os.WriteFile("jfa-go.service", service, 0666)
|
err = os.WriteFile(SYSTEMD_SERVICE, service, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Couldn't write jfa-go.service: %v\n", err)
|
fmt.Printf(lm.FailedWriting+"\n", SYSTEMD_SERVICE, 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 "jfa-go.service" file to ~/.config/systemd/user (Creating it if necessary).
|
Move the newly created SYSTEMD_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,6 +6,7 @@ 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"
|
||||||
@ -31,15 +32,6 @@ 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{
|
||||||
@ -118,13 +110,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("Starting Matrix bot daemon")
|
d.app.info.Println(lm.StartDaemon, lm.Matrix)
|
||||||
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("Matrix sync failed: %v", err)
|
d.app.err.Printf(lm.FailedSyncMatrix, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +162,7 @@ func (d *MatrixDaemon) commandLang(evt *event.Event, code, lang string) {
|
|||||||
list,
|
list,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Matrix: Failed to send message to \"%s\": %v", evt.Sender, err)
|
d.app.err.Printf(lm.FailedReply, lm.Matrix, evt.Sender, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -203,7 +195,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("Failed to create room for user \"%s\": %v", userID, err)
|
d.app.err.Printf(lm.FailedCreateMatrixRoom, userID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
lang := "en-us"
|
lang := "en-us"
|
||||||
@ -226,7 +218,7 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
|||||||
roomID,
|
roomID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Matrix: Failed to send welcome message to \"%s\": %v", userID, err)
|
d.app.err.Printf(lm.FailedMessage, lm.Matrix, userID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ok = true
|
ok = true
|
||||||
@ -276,6 +268,57 @@ 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,10 +1,13 @@
|
|||||||
|
//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"
|
||||||
@ -65,22 +68,22 @@ type olmLogger struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o olmLogger) Error(message string, args ...interface{}) {
|
func (o olmLogger) Error(message string, args ...interface{}) {
|
||||||
o.app.err.Printf("OLM: "+message+"\n", args)
|
o.app.err.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, args))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o olmLogger) Warn(message string, args ...interface{}) {
|
func (o olmLogger) Warn(message string, args ...interface{}) {
|
||||||
o.app.info.Printf("OLM: "+message+"\n", args)
|
o.app.info.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, args))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o olmLogger) Debug(message string, args ...interface{}) {
|
func (o olmLogger) Debug(message string, args ...interface{}) {
|
||||||
o.app.debug.Printf("OLM: "+message+"\n", args)
|
o.app.debug.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, 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("OLM [TRACE]: "+message+"\n", args)
|
o.app.debug.Printf(lm.MatrixOlmTracelog, fmt.Sprintf(message, args))
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitMatrixCrypto(d *MatrixDaemon) (err error) {
|
func InitMatrixCrypto(d *MatrixDaemon) (err error) {
|
||||||
@ -155,7 +158,7 @@ func HandleSyncerCrypto(startTime int64, d *MatrixDaemon, syncer *mautrix.Defaul
|
|||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.err.Printf("Failed to decrypt Matrix message: %v", err)
|
d.app.err.Printf(lm.FailedDecryptMatrixMessage, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.handleMessage(source, decrypted)
|
d.handleMessage(source, decrypted)
|
||||||
@ -180,7 +183,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("Matrix: Failed to enable encryption in room: %v", err)
|
d.app.debug.Printf(lm.FailedEnableMatrixEncryption, room.RoomID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.isEncrypted[room.RoomID] = encrypted
|
d.isEncrypted[room.RoomID] = encrypted
|
||||||
|
@ -9,6 +9,8 @@ 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,6 +8,7 @@ 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.
|
||||||
@ -39,16 +40,16 @@ func (app *appContext) GenResetLink(pin string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) StartPWR() {
|
func (app *appContext) StartPWR() {
|
||||||
app.info.Println("Starting password reset daemon")
|
app.info.Println(lm.StartDaemon, "PWR")
|
||||||
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("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path)
|
app.err.Printf(lm.FailedStartDaemon, "PWR", fmt.Sprintf(lm.PathNotFound, path))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
watcher, err := fsnotify.NewWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.err.Printf("Couldn't initialise password reset daemon")
|
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer watcher.Close()
|
defer watcher.Close()
|
||||||
@ -56,7 +57,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("Failed to start password reset daemon: %s", err)
|
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForRestart()
|
waitForRestart()
|
||||||
@ -84,43 +85,36 @@ 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("PWR: Failed to read file: %v", err)
|
app.debug.Printf(lm.FailedReading, event.Name, 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("PWR: Failed to read PIN: %v", err)
|
app.debug.Printf(lm.FailedReading, event.Name, 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 {
|
if !(status == 200 || status == 204) || err != nil || user.ID == "" {
|
||||||
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
app.err.Printf(lm.FailedGetUser, pwr.Username, lm.Jellyfin, err)
|
||||||
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("Failed to construct password reset message for \"%s\"", pwr.Username)
|
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
|
||||||
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("Failed to send password reset message to \"%s\"", name)
|
app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, name, err)
|
||||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf("Sent password reset message to \"%s\"", name)
|
app.err.Printf(lm.SentPWRMessage, pwr.Username, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry)
|
app.err.Printf(lm.PWRExpired, pwr.Username, pwr.Expiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -128,7 +122,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.err.Printf("Password reset daemon: %s", err)
|
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
|
)
|
||||||
|
|
||||||
func (app *appContext) HardRestart() error {
|
func (app *appContext) HardRestart() error {
|
||||||
return fmt.Errorf("hard restarts not available on windows")
|
return fmt.Errorf(lm.FailedHardRestartWindows)
|
||||||
}
|
}
|
||||||
|
19
router.go
19
router.go
@ -11,6 +11,7 @@ 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"
|
||||||
)
|
)
|
||||||
@ -21,17 +22,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("Couldn't access template directory: \"%s\"", templatePath)
|
app.err.Fatalf(lm.FailedReading, templatePath, err)
|
||||||
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("Using default \"%s\"", f.Name())
|
app.debug.Printf(lm.UseDefaultHTML, f.Name())
|
||||||
loadInternal = append(loadInternal, FSJoin(templatePath, f.Name()))
|
loadInternal = append(loadInternal, FSJoin(templatePath, f.Name()))
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf("Using custom \"%s\"", f.Name())
|
app.info.Printf(lm.UseCustomHTML, f.Name())
|
||||||
loadExternal = append(loadExternal, filepath.Join(filepath.Join(customPath, f.Name())))
|
loadExternal = append(loadExternal, filepath.Join(filepath.Join(customPath, f.Name())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,13 +40,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("Failed to load templates: %v", err)
|
app.err.Fatalf(lm.FailedLoadTemplates, lm.Internal, 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("Failed to load external templates: %v", err)
|
app.err.Fatalf(lm.FailedLoadTemplates, lm.External, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
router.SetHTMLTemplate(tmpl)
|
router.SetHTMLTemplate(tmpl)
|
||||||
@ -96,7 +97,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("Loading pprof")
|
app.debug.Println(lm.RegisterPprof)
|
||||||
pprof.Register(router)
|
pprof.Register(router)
|
||||||
}
|
}
|
||||||
SRV = &http.Server{
|
SRV = &http.Server{
|
||||||
@ -134,7 +135,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.NewUser)
|
router.POST(p+"/newUser", app.NewUserFromInvite)
|
||||||
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) {
|
||||||
@ -165,7 +166,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if *SWAGGER {
|
if *SWAGGER {
|
||||||
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
app.info.Print(warning(lm.SwaggerWarning))
|
||||||
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))
|
||||||
}
|
}
|
||||||
@ -181,7 +182,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.NewUserAdmin)
|
api.POST(p+"/users", app.NewUserFromAdmin)
|
||||||
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,6 +8,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -104,7 +105,7 @@ func (app *appContext) TestJF(gc *gin.Context) {
|
|||||||
case 404:
|
case 404:
|
||||||
msg = "error404"
|
msg = "error404"
|
||||||
}
|
}
|
||||||
app.info.Printf("Auth failed with code %d (%s)", status, err)
|
app.err.Printf(lm.FailedAuthJellyfin, req.Server, status, err)
|
||||||
if msg != "" {
|
if msg != "" {
|
||||||
respond(status, msg, gc)
|
respond(status, msg, gc)
|
||||||
} else {
|
} else {
|
||||||
@ -151,16 +152,20 @@ 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)
|
||||||
@ -170,16 +175,20 @@ 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)
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 58 KiB |
83
storage.go
83
storage.go
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@ -13,6 +14,7 @@ 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"
|
||||||
@ -175,10 +177,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("Failed to open db \"%s\": %v", app.storage.db_path, err)
|
app.err.Fatalf(lm.FailedConnectDB, app.storage.db_path, err)
|
||||||
}
|
}
|
||||||
app.storage.db = db
|
app.storage.db = db
|
||||||
app.info.Printf("Connected to DB \"%s\"", app.storage.db_path)
|
app.info.Printf(lm.ConnectDB, app.storage.db_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEmails returns a copy of the store.
|
// GetEmails returns a copy of the store.
|
||||||
@ -508,6 +510,15 @@ 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{}
|
||||||
@ -583,12 +594,35 @@ func (st *Storage) DeleteActivityKey(k string) {
|
|||||||
st.db.Delete(k, Activity{})
|
st.db.Delete(k, Activity{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type TelegramUser struct {
|
type ThirdPartyService interface {
|
||||||
JellyfinID string `badgerhold:"key"`
|
// ok implies user imported, err can be any issue that occurs during
|
||||||
ChatID int64 `badgerhold:"index"`
|
ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool)
|
||||||
Username string `badgerhold:"index"`
|
AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error)
|
||||||
Lang string
|
Enabled(app *appContext, profile *Profile) bool
|
||||||
Contact bool // Whether to contact through telegram or not
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
@ -601,6 +635,24 @@ 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.
|
||||||
@ -685,6 +737,16 @@ 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
|
||||||
@ -718,22 +780,27 @@ 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...)
|
||||||
|
75
telegram.go
75
telegram.go
@ -7,6 +7,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,9 +16,27 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TelegramVerifiedToken struct {
|
type TelegramVerifiedToken struct {
|
||||||
ChatID int64
|
JellyfinID string `badgerhold:"key"` // optional, for ensuring a user-requested change is only accessed by them.
|
||||||
Username string
|
ChatID int64 `badgerhold:"index"`
|
||||||
JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
|
Username string `badgerhold:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
@ -96,12 +115,12 @@ func (t *TelegramDaemon) NewAssignedAuthToken(id string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *TelegramDaemon) run() {
|
func (t *TelegramDaemon) run() {
|
||||||
t.app.info.Println("Starting Telegram bot daemon")
|
t.app.info.Println(lm.StartDaemon, lm.Telegram)
|
||||||
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("Failed to start Telegram daemon: %v", err)
|
t.app.err.Printf(lm.FailedStartDaemon, lm.Telegram, err)
|
||||||
telegramEnabled = false
|
telegramEnabled = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -199,7 +218,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("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +230,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("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -232,14 +251,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("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
t.app.err.Printf(lm.FailedReply, lm.Telegram, 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("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
t.app.err.Printf(lm.FailedReply, lm.Telegram, 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,
|
||||||
@ -273,7 +292,39 @@ func (t *TelegramDaemon) UserExists(username string) bool {
|
|||||||
return err != nil || c > 0
|
return err != nil || c > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteVerifiedToken removes the token with the given PIN.
|
// Exists returns whether or not the given user exists.
|
||||||
func (t *TelegramDaemon) DeleteVerifiedToken(pin string) {
|
func (t *TelegramDaemon) Exists(user ContactMethodUser) bool {
|
||||||
delete(t.verifiedTokens, pin)
|
return t.UserExists(user.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteVerifiedToken removes the token with the given PIN.
|
||||||
|
func (t *TelegramDaemon) DeleteVerifiedToken(PIN string) {
|
||||||
|
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,12 +221,16 @@ 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);
|
||||||
html("language-description", window.lang.var("language", "description", `<a target="_blank" href="https://weblate.jfa-go.com">Weblate</a>`));
|
replaceLink("language-description", "language", "description", "https://weblate.jfa-go.com", "Weblate");
|
||||||
html("email-description", window.lang.var("email", "description", `<a target="_blank" href="https://mailgun.com">Mailgun</a>`));
|
replaceLink("email-description", "email", "description", "https://mailgun.com", "Mailgun");
|
||||||
html("email-dateformat-notice", window.lang.var("email", "dateFormatNotice", `<a target="_blank" href="https://strftime.ninja/">strftime.ninja</a>`));
|
replaceLink("email-dateformat-notice", "email", "dateFormatNotice", "https://strftime.timpetricola.com/", "strftime.timpetricola.com");
|
||||||
html("updates-description", window.lang.var("updates", "description", `<a target="_blank" href="https://builds.hrfee.dev/view/hrfee/jfa-go">buildrone</a>`));
|
replaceLink("updates-description", "updates", "description", "https://builds.hrfee.dev/view/hrfee/jfa-go", "buildrone");
|
||||||
html("messages-description", window.lang.var("messages", "description", `<a target="_blank" href="https://wiki.jfa-go.com">Wiki</a>`));
|
replaceLink("messages-description", "messages", "description", "https://wiki.jfa-go.com", "Wiki");
|
||||||
|
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": {
|
||||||
@ -318,6 +322,12 @@ 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,7 +4,8 @@
|
|||||||
"target": "es2017",
|
"target": "es2017",
|
||||||
"lib": ["dom", "es2017"],
|
"lib": ["dom", "es2017"],
|
||||||
"typeRoots": ["./typings", "../node_modules/@types"],
|
"typeRoots": ["./typings", "../node_modules/@types"],
|
||||||
"moduleResolution": "nodenext",
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
|
|
||||||
"github.com/hrfee/jfa-go/common"
|
"github.com/hrfee/jfa-go/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -560,15 +562,16 @@ 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("Failed to get latest tag (%d): %v", status, err)
|
app.err.Printf(lm.FailedGetUpdateTag, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if tag != app.tag && tag.IsNew() {
|
if tag != app.tag && tag.IsNew() {
|
||||||
app.info.Println("Update found")
|
app.info.Println(lm.FoundUpdate)
|
||||||
|
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("Failed to get update (%d): %v", status, err)
|
app.err.Printf(lm.FailedGetUpdate, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.tag = tag
|
app.tag = tag
|
||||||
|
18
user-auth.go
18
user-auth.go
@ -1,9 +1,11 @@
|
|||||||
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 {
|
||||||
@ -13,7 +15,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.Println("Enable Jellyfin Login to use the User Page feature.")
|
app.err.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth)
|
||||||
respond(500, "Contact Admin", gc)
|
respond(500, "Contact Admin", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -27,7 +29,6 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,11 +42,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.Println("Enable Jellyfin Login to use the User Page feature.")
|
app.err.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth)
|
||||||
respond(500, "Contact Admin", gc)
|
respond(500, "Contact Admin", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.logIpInfo(gc, true, "UserToken requested (login attempt)")
|
app.logIpInfo(gc, true, fmt.Sprintf(lm.RequestingToken, lm.UserTokenLoginAttempt))
|
||||||
username, password, ok := app.decodeValidateLoginHeader(gc, true)
|
username, password, ok := app.decodeValidateLoginHeader(gc, true)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@ -58,12 +59,11 @@ 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("getUserToken failed: Couldn't generate user token (%s)", err)
|
app.err.Printf(lm.FailedGenerateToken, 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.Println("Enable Jellyfin Login to use the User Page feature.")
|
app.err.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth)
|
||||||
respond(500, "Contact Admin", gc)
|
respond(500, "Contact Admin", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.logIpInfo(gc, true, "UserToken request (refresh token)")
|
app.logIpInfo(gc, true, fmt.Sprintf(lm.RequestingToken, lm.UserTokenRefresh))
|
||||||
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("getUserToken failed: Couldn't generate user token (%s)", err)
|
app.err.Printf(lm.FailedGenerateToken, err)
|
||||||
respond(500, "Couldn't generate user token", gc)
|
respond(500, "Couldn't generate user token", gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ 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"
|
||||||
)
|
)
|
||||||
@ -21,17 +22,17 @@ func (app *appContext) checkUsers() {
|
|||||||
if len(app.storage.GetUserExpiries()) == 0 {
|
if len(app.storage.GetUserExpiries()) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.info.Println("Daemon: Checking for user expiry")
|
app.info.Println(lm.CheckUserExpiries)
|
||||||
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("Failed to get users (%d): %s", status, err)
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mode := "disable"
|
mode := "disable"
|
||||||
term := "Disabling"
|
phrase := lm.DisableExpiredUser
|
||||||
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"
|
||||||
term = "Deleting"
|
phrase = lm.DeleteExpiredUser
|
||||||
}
|
}
|
||||||
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) {
|
||||||
@ -45,7 +46,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("Deleting expiry for non-existent user \"%s\"", id)
|
app.info.Printf(lm.DeleteExpiryForOldUser, 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
|
||||||
@ -58,11 +59,10 @@ 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("%s expired user \"%s\"", term, user.Name)
|
app.info.Printf(phrase, user.Name)
|
||||||
|
|
||||||
// Record activity
|
// Record activity
|
||||||
activity := Activity{
|
activity := Activity{
|
||||||
@ -72,18 +72,24 @@ func (app *appContext) checkUsers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if mode == "delete" {
|
if mode == "delete" {
|
||||||
status, err = app.jf.DeleteUser(id)
|
deleted := false
|
||||||
|
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
|
||||||
status, err = app.jf.SetPolicy(id, user.Policy)
|
err, _, _ = app.SetUserDisabled(user, true)
|
||||||
activity.Type = ActivityDisabled
|
activity.Type = ActivityDisabled
|
||||||
}
|
}
|
||||||
if !(status == 200 || status == 204) || err != nil {
|
if !(status == 200 || status == 204) || err != nil {
|
||||||
app.err.Printf("Failed to %s \"%s\" (%d): %s", mode, user.Name, status, err)
|
app.err.Printf(lm.FailedDeleteOrDisableExpiredUser, user.ID, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,11 +104,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("Failed to construct expiry message for \"%s\": %s", user.Name, err)
|
app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err)
|
||||||
} else if err := app.sendByID(msg, user.ID); err != nil {
|
} else if err := app.sendByID(msg, user.ID); err != nil {
|
||||||
app.err.Printf("Failed to send expiry message to \"%s\": %s", name, err)
|
app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, name, err)
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf("Sent expiry notification to \"%s\"", name)
|
app.err.Printf(lm.SentExpiryMessage, user.ID, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
223
users.go
Normal file
223
users.go
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
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,6 +4,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@ -16,6 +17,7 @@ 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"
|
||||||
@ -66,10 +68,9 @@ 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("Failed HTTP2 ServerPush of \"%s\": %+v", f, err)
|
app.debug.Printf(lm.FailedServerPush, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,13 +140,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("Failed to load LICENSE: %s", err)
|
app.debug.Printf(lm.FailedReading, "LICENSE", 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("Failed to load OFL.txt: %s", err)
|
app.debug.Printf(lm.FailedReading, "fontLicense", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
license += "---Hanken Grotesk---\n\n"
|
license += "---Hanken Grotesk---\n\n"
|
||||||
@ -236,7 +237,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.inviteChannelName != ""
|
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
|
||||||
}
|
}
|
||||||
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)
|
||||||
@ -312,7 +313,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("PWR: Ignoring magic link visit from bot")
|
app.debug.Println(lm.IgnoreBotPWR)
|
||||||
data["success"] = true
|
data["success"] = true
|
||||||
data["pin"] = "NO-BO-TS"
|
data["pin"] = "NO-BO-TS"
|
||||||
return
|
return
|
||||||
@ -338,13 +339,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("Ignoring PWR request due to expired internal PIN: %s", pin)
|
app.debug.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, 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("Password Reset failed (%d): %v", status, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", err)
|
||||||
} else {
|
} else {
|
||||||
status, err = app.jf.SetPassword(pwr.ID, "", pin)
|
status, err = app.jf.SetPassword(pwr.ID, "", pin)
|
||||||
}
|
}
|
||||||
@ -358,7 +359,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
|||||||
username = resp.UsersReset[0]
|
username = resp.UsersReset[0]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only log PWRs we know the user for.
|
// Only log PWRs we know the user for.
|
||||||
@ -378,21 +379,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("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err)
|
app.err.Printf(lm.FailedGetUser, username, lm.Jellyfin, 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("Failed to get user \"%s\" from ombi (%d): %v", username, status, err)
|
app.err.Printf(lm.FailedGetUser, username, lm.Ombi, 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("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
|
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
app.debug.Printf(lm.ChangePassword, lm.Ombi, ombiUser["userName"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -460,7 +461,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("Failed to generate captcha: %v", err)
|
app.err.Printf(lm.FailedGenerateCaptcha, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -470,7 +471,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("Failed to render captcha: %v", err)
|
app.err.Printf(lm.FailedGenerateCaptcha, err)
|
||||||
respondBool(500, false, gc)
|
respondBool(500, false, gc)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -503,8 +504,12 @@ 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 || (!isPWR && inv.Captchas == nil) {
|
if !ok {
|
||||||
app.debug.Printf("Couldn't find invite \"%s\"", code)
|
app.debug.Printf(lm.InvalidInviteCode, 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]
|
||||||
@ -512,7 +517,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("Couldn't find Captcha \"%s\"", id)
|
app.debug.Printf(lm.CaptchaNotFound, id, code)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return strings.ToLower(c.Answer) == strings.ToLower(text)
|
return strings.ToLower(c.Answer) == strings.ToLower(text)
|
||||||
@ -534,8 +539,11 @@ 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 {
|
||||||
app.err.Printf("Failed to read reCAPTCHA status (%d): %+v\n", resp.Status, err)
|
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@ -543,18 +551,19 @@ 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("Failed to unmarshal reCAPTCHA response: %+v\n", err)
|
app.err.Printf(lm.FailedVerifyReCAPTCHA, 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 != "" {
|
||||||
app.debug.Printf("Invalidating reCAPTCHA request: Hostnames didn't match (Wanted \"%s\", got \"%s\"\n", hostname, data.Hostname)
|
err = fmt.Errorf(lm.InvalidHostname, 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("reCAPTCHA returned errors: %+v\n", data.ErrorCodes)
|
app.err.Printf(lm.AdditionalErrors, lm.ReCAPTCHA, data.ErrorCodes)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -607,13 +616,108 @@ 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, "") {
|
||||||
inv, ok := app.storage.GetInvitesKey(code)
|
invite, ok := app.storage.GetInvitesKey(gc.Param("invCode"))
|
||||||
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),
|
||||||
@ -623,76 +727,13 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
|
||||||
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 key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
||||||
if !ok {
|
app.NewUserFromConfirmationKey(invite, key, lang, gc)
|
||||||
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 := ""
|
|
||||||
if invite, ok := app.storage.GetInvitesKey(code); ok {
|
email := invite.SendTo
|
||||||
email = invite.SendTo
|
|
||||||
}
|
|
||||||
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
|
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
|
||||||
email = ""
|
email = ""
|
||||||
}
|
}
|
||||||
@ -707,8 +748,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
userPageAddress += "/my/account"
|
userPageAddress += "/my/account"
|
||||||
|
|
||||||
fromUser := ""
|
fromUser := ""
|
||||||
if inv.ReferrerJellyfinID != "" {
|
if invite.ReferrerJellyfinID != "" {
|
||||||
sender, status, err := app.jf.UserByID(inv.ReferrerJellyfinID, false)
|
sender, status, err := app.jf.UserByID(invite.ReferrerJellyfinID, false)
|
||||||
if status == 200 && err == nil {
|
if status == 200 && err == nil {
|
||||||
fromUser = sender.Name
|
fromUser = sender.Name
|
||||||
}
|
}
|
||||||
@ -730,13 +771,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": code,
|
"code": invite.Code,
|
||||||
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
|
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
|
||||||
"userExpiry": inv.UserExpiry,
|
"userExpiry": invite.UserExpiry,
|
||||||
"userExpiryMonths": inv.UserMonths,
|
"userExpiryMonths": invite.UserMonths,
|
||||||
"userExpiryDays": inv.UserDays,
|
"userExpiryDays": invite.UserDays,
|
||||||
"userExpiryHours": inv.UserHours,
|
"userExpiryHours": invite.UserHours,
|
||||||
"userExpiryMinutes": inv.UserMinutes,
|
"userExpiryMinutes": invite.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,
|
||||||
@ -771,7 +812,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.inviteChannelName != ""
|
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
|
||||||
}
|
}
|
||||||
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