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/out
|
||||
tinyproxy.conf
|
||||
static/banner.svg
|
||||
|
@ -11,6 +11,7 @@ before:
|
||||
- go mod download
|
||||
- rm -rf data/web
|
||||
- mkdir -p data/web/css
|
||||
- cp images/banner.svg static/banner.svg
|
||||
- bash -c 'cp -r static/* data/web/'
|
||||
- npm install
|
||||
- npm install esbuild
|
||||
|
1
Makefile
1
Makefile
@ -167,6 +167,7 @@ copy:
|
||||
mv $(DATA)/crash.html $(DATA)/html/
|
||||
$(info copying static data)
|
||||
mkdir -p $(DATA)/web
|
||||
cp images/banner.svg static/banner.svg
|
||||
cp -r static/* $(DATA)/web/
|
||||
$(info copying systemd service)
|
||||
cp jfa-go.service $(DATA)/
|
||||
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
@ -120,7 +121,7 @@ func (app *appContext) GetActivities(gc *gin.Context) {
|
||||
err := app.storage.db.Find(&results, query)
|
||||
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to read activities from DB: %v\n", err)
|
||||
app.err.Printf(lm.FailedDBReadActivities, err)
|
||||
}
|
||||
|
||||
resp := GetActivitiesRespDTO{
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
// @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)
|
||||
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() {
|
||||
app.debug.Printf("Ignoring backup DL request due to fname: %v\n", err)
|
||||
app.debug.Printf(lm.IgnoreInvalidFilename, fname, err)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
@ -83,7 +84,7 @@ func (app *appContext) RestoreLocalBackup(gc *gin.Context) {
|
||||
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))
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -103,15 +104,15 @@ func (app *appContext) RestoreLocalBackup(gc *gin.Context) {
|
||||
func (app *appContext) RestoreBackup(gc *gin.Context) {
|
||||
file, err := gc.FormFile("backups-file")
|
||||
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)
|
||||
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()
|
||||
fullpath := filepath.Join(path, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX+time.Now().Local().Format(BACKUP_DATEFMT)+BACKUP_SUFFIX)
|
||||
gc.SaveUploadedFile(file, fullpath)
|
||||
app.debug.Printf("Saved to \"%s\"\n", fullpath)
|
||||
app.debug.Printf(lm.Write, fullpath)
|
||||
LOADBAK = fullpath
|
||||
app.restart(gc)
|
||||
}
|
||||
|
217
api-invites.go
217
api-invites.go
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/itchyny/timefmt-go"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
@ -29,6 +30,7 @@ func GenerateInviteCode() string {
|
||||
return inviteCode
|
||||
}
|
||||
|
||||
// checkInvites performs general housekeeping on invites, i.e. deleting expired ones and cleaning captcha data.
|
||||
func (app *appContext) checkInvites() {
|
||||
currentTime := time.Now()
|
||||
for _, data := range app.storage.GetInvites() {
|
||||
@ -52,60 +54,11 @@ func (app *appContext) checkInvites() {
|
||||
if !currentTime.After(expiry) {
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
app.deleteExpiredInvite(data)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
currentTime := time.Now()
|
||||
inv, match := app.storage.GetInvitesKey(code)
|
||||
@ -114,54 +67,8 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
}
|
||||
expiry := inv.ValidTill
|
||||
if currentTime.After(expiry) {
|
||||
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
||||
notify := inv.Notify
|
||||
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
app.debug.Printf("%s: Expiry notification", code)
|
||||
var wait sync.WaitGroup
|
||||
for address, settings := range notify {
|
||||
if !settings["notify-expiry"] {
|
||||
continue
|
||||
}
|
||||
wait.Add(1)
|
||||
go func(addr string) {
|
||||
defer wait.Done()
|
||||
msg, err := app.email.constructExpiry(code, inv, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
|
||||
} else {
|
||||
// Check whether notify "address" is an email address of Jellyfin ID
|
||||
if strings.Contains(addr, "@") {
|
||||
err = app.email.send(msg, addr)
|
||||
} else {
|
||||
err = app.sendByID(msg, addr)
|
||||
}
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to %s", addr)
|
||||
}
|
||||
}
|
||||
}(address)
|
||||
}
|
||||
wait.Wait()
|
||||
}
|
||||
if inv.IsReferral && inv.ReferrerJellyfinID != "" && inv.UseReferralExpiry {
|
||||
user, ok := app.storage.GetEmailsKey(inv.ReferrerJellyfinID)
|
||||
if ok {
|
||||
user.ReferralTemplateKey = ""
|
||||
app.storage.SetEmailsKey(inv.ReferrerJellyfinID, user)
|
||||
}
|
||||
}
|
||||
app.deleteExpiredInvite(inv)
|
||||
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 {
|
||||
del := false
|
||||
newInv := inv
|
||||
@ -187,6 +94,67 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
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.
|
||||
// @Produce json
|
||||
// @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
|
||||
func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
var req generateInviteDTO
|
||||
app.debug.Println("Generating new invite")
|
||||
app.debug.Println(lm.GenerateInvite)
|
||||
gc.BindJSON(&req)
|
||||
currentTime := time.Now()
|
||||
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) {
|
||||
addressValid := false
|
||||
discord := ""
|
||||
app.debug.Printf("%s: Sending invite message", invite.Code)
|
||||
if discordEnabled && (!strings.Contains(req.SendTo, "@") || strings.HasPrefix(req.SendTo, "@")) {
|
||||
users := app.discord.GetUsers(req.SendTo)
|
||||
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 {
|
||||
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
|
||||
invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipMultiUser, req.SendTo)
|
||||
} else {
|
||||
invite.SendTo = req.SendTo
|
||||
addressValid = true
|
||||
@ -249,8 +216,10 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
if addressValid {
|
||||
msg, err := app.email.constructInvite(invite.Code, invite, app, false)
|
||||
if err != nil {
|
||||
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||
app.err.Printf("%s: Failed to construct invite message: %v", invite.Code, err)
|
||||
// Slight misuse of the template
|
||||
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, req.SendTo, err)
|
||||
|
||||
app.err.Printf(lm.FailedConstructInviteMessage, invite.Code, err)
|
||||
} else {
|
||||
var err error
|
||||
if discord != "" {
|
||||
@ -259,10 +228,10 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
err = app.email.send(msg, req.SendTo)
|
||||
}
|
||||
if err != nil {
|
||||
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||
app.err.Printf("%s: %s: %v", invite.Code, invite.SendTo, err)
|
||||
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, req.SendTo, err)
|
||||
app.err.Println(invite.SendTo)
|
||||
} 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
|
||||
// @tags Invites
|
||||
func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
app.debug.Println("Invites requested")
|
||||
currentTime := time.Now()
|
||||
app.checkInvites()
|
||||
var invites []inviteDTO
|
||||
@ -332,7 +300,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
if err != nil {
|
||||
date, err := timefmt.Parse(pair[1], app.datePattern+" "+app.timePattern)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to parse usedBy time: %v", err)
|
||||
app.err.Printf(lm.FailedParseTime, err)
|
||||
}
|
||||
unix = date.Unix()
|
||||
}
|
||||
@ -347,7 +315,6 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
invite.SendTo = inv.SendTo
|
||||
}
|
||||
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
|
||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
addressOrID = gc.GetString("jfId")
|
||||
@ -397,10 +364,9 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
func (app *appContext) SetProfile(gc *gin.Context) {
|
||||
var req inviteProfileDTO
|
||||
gc.BindJSON(&req)
|
||||
app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile)
|
||||
// "" means "Don't apply 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)
|
||||
return
|
||||
}
|
||||
@ -424,11 +390,11 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
gc.BindJSON(&req)
|
||||
changed := false
|
||||
for code, settings := range req {
|
||||
app.debug.Printf("%s: Notification settings change requested", code)
|
||||
invite, ok := app.storage.GetInvitesKey(code)
|
||||
if !ok {
|
||||
app.err.Printf("%s Notification setting change failed: Invalid code", code)
|
||||
respond(400, "Invalid invite code", gc)
|
||||
msg := fmt.Sprintf(lm.InvalidInviteCode, code)
|
||||
app.err.Println(msg)
|
||||
respond(400, msg, gc)
|
||||
return
|
||||
}
|
||||
var address string
|
||||
@ -436,9 +402,8 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
if jellyfinLogin {
|
||||
var addressAvailable bool = app.getAddressOrName(gc.GetString("jfId")) != ""
|
||||
if !addressAvailable {
|
||||
app.err.Printf("%s: Couldn't find contact method for admin. Make sure one is set.", code)
|
||||
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
||||
respond(500, "Missing user contact method", gc)
|
||||
app.err.Printf(lm.FailedGetContactMethod, gc.GetString("jfId"))
|
||||
respond(500, fmt.Sprintf(lm.FailedGetContactMethod, "admin"), gc)
|
||||
return
|
||||
}
|
||||
address = gc.GetString("jfId")
|
||||
@ -453,15 +418,12 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
} /*else {
|
||||
if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
|
||||
*/
|
||||
if _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] {
|
||||
invite.Notify[address]["notify-expiry"] = settings["notify-expiry"]
|
||||
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address)
|
||||
for _, notifyType := range []string{"notify-expiry", "notify-creation"} {
|
||||
if _, ok := settings[notifyType]; ok && invite.Notify[address][notifyType] != settings[notifyType] {
|
||||
invite.Notify[address][notifyType] = settings[notifyType]
|
||||
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 {
|
||||
app.storage.SetInvitesKey(code, invite)
|
||||
@ -480,7 +442,6 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||
var req deleteInviteDTO
|
||||
gc.BindJSON(&req)
|
||||
app.debug.Printf("%s: Deletion requested", req.Code)
|
||||
inv, ok := app.storage.GetInvitesKey(req.Code)
|
||||
if ok {
|
||||
app.storage.DeleteInvitesKey(req.Code)
|
||||
@ -495,10 +456,10 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||
Time: time.Now(),
|
||||
}, gc, false)
|
||||
|
||||
app.info.Printf("%s: Invite deleted", req.Code)
|
||||
app.info.Printf(lm.DeleteInvite, req.Code)
|
||||
respondBool(200, true, gc)
|
||||
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)
|
||||
}
|
||||
|
@ -1,10 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"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.
|
||||
@ -15,14 +18,12 @@ import (
|
||||
// @Security Bearer
|
||||
// @tags Jellyseerr
|
||||
func (app *appContext) JellyseerrUsers(gc *gin.Context) {
|
||||
app.debug.Println("Jellyseerr users requested")
|
||||
users, err := app.js.GetUsers()
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to get users from Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyseerr, err)
|
||||
respond(500, "Couldn't get users", gc)
|
||||
return
|
||||
}
|
||||
app.debug.Printf("Jellyseerr users retrieved: %d", len(users))
|
||||
userlist := make([]ombiUser, len(users))
|
||||
i := 0
|
||||
for _, u := range users {
|
||||
@ -60,14 +61,14 @@ func (app *appContext) SetJellyseerrProfile(gc *gin.Context) {
|
||||
}
|
||||
u, err := app.js.UserByID(jellyseerrID)
|
||||
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)
|
||||
return
|
||||
}
|
||||
profile.Jellyseerr.User = u.UserTemplate
|
||||
n, err := app.js.GetNotificationPreferencesByID(jellyseerrID)
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't get user's notification prefs from Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedGetJellyseerrNotificationPrefs, gc.Param("id"), err)
|
||||
respond(500, "Couldn't get user notification prefs", gc)
|
||||
return
|
||||
}
|
||||
@ -98,3 +99,67 @@ func (app *appContext) DeleteJellyseerrProfile(gc *gin.Context) {
|
||||
app.storage.SetProfileKey(profileName, profile)
|
||||
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/hrfee/jfa-go/jellyseerr"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
@ -134,7 +135,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
||||
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
|
||||
customMessage, ok := app.storage.GetCustomContentKey(id)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -328,7 +329,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
||||
jellyseerr.FieldTelegram: tgUser.ChatID,
|
||||
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
|
||||
linkExistingOmbiDiscordTelegram(app)
|
||||
@ -361,11 +362,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
||||
tgUser.Contact = req.Telegram
|
||||
app.storage.SetTelegramKey(req.ID, tgUser)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Telegram {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
|
||||
app.debug.Printf(lm.SetContactPrefForService, lm.Telegram, tgUser.Username, req.Telegram)
|
||||
jsPrefs[jellyseerr.FieldTelegramEnabled] = req.Telegram
|
||||
}
|
||||
}
|
||||
@ -374,11 +371,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
||||
dcUser.Contact = req.Discord
|
||||
app.storage.SetDiscordKey(req.ID, dcUser)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Discord {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||
app.debug.Printf(lm.SetContactPrefForService, lm.Discord, dcUser.Username, req.Discord)
|
||||
jsPrefs[jellyseerr.FieldDiscordEnabled] = req.Discord
|
||||
}
|
||||
}
|
||||
@ -387,11 +380,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
||||
mxUser.Contact = req.Matrix
|
||||
app.storage.SetMatrixKey(req.ID, mxUser)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Matrix {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
|
||||
app.debug.Printf(lm.SetContactPrefForService, lm.Matrix, mxUser.UserID, req.Matrix)
|
||||
}
|
||||
}
|
||||
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
|
||||
app.storage.SetEmailsKey(req.ID, email)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Email {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
|
||||
app.debug.Printf(lm.SetContactPrefForService, lm.Email, email.Addr, req.Email)
|
||||
jsPrefs[jellyseerr.FieldEmailEnabled] = req.Email
|
||||
}
|
||||
}
|
||||
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
||||
err := app.js.ModifyNotifications(req.ID, jsPrefs)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to sync contact prefs with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
@ -446,7 +431,7 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
token, ok := app.telegram.TokenVerified(pin)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -469,7 +454,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
||||
}
|
||||
pin := gc.Param("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)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
@ -487,7 +472,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
||||
// @Router /invite/{invCode}/discord/invite [get]
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||
if app.discord.inviteChannelName == "" {
|
||||
if app.discord.InviteChannel.Name == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
@ -555,7 +540,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -563,12 +548,12 @@ func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
user, ok := app.matrix.tokens[pin]
|
||||
if !ok {
|
||||
app.debug.Println("Matrix: PIN not found")
|
||||
app.debug.Printf(lm.InvalidPIN, pin)
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -596,7 +581,7 @@ func (app *appContext) MatrixLogin(gc *gin.Context) {
|
||||
}
|
||||
token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password)
|
||||
if err != nil {
|
||||
app.err.Printf("Matrix: Failed to generate token: %v", err)
|
||||
app.err.Printf(lm.FailedGenerateToken, err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@ -607,7 +592,7 @@ func (app *appContext) MatrixLogin(gc *gin.Context) {
|
||||
matrix.Key("token").SetValue(token)
|
||||
matrix.Key("user_id").SetValue(req.Username)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -631,7 +616,7 @@ func (app *appContext) MatrixConnect(gc *gin.Context) {
|
||||
}
|
||||
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
|
||||
if err != nil {
|
||||
app.err.Printf("Matrix: Failed to create room: %v", err)
|
||||
app.err.Printf(lm.FailedCreateRoom, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
@ -701,7 +686,7 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
|
||||
jellyseerr.FieldDiscord: req.DiscordID,
|
||||
jellyseerr.FieldDiscordEnabled: true,
|
||||
}); 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{
|
||||
@ -739,7 +724,7 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
|
||||
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldDiscordEnabled: false,
|
||||
}); 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{
|
||||
@ -775,7 +760,7 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
|
||||
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldTelegramEnabled: false,
|
||||
}); 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{
|
||||
|
95
api-ombi.go
95
api-ombi.go
@ -1,18 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/jfa-go/ombi"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
)
|
||||
|
||||
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)
|
||||
if err != nil || code != 200 {
|
||||
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 {
|
||||
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 {
|
||||
ombiAddr := ""
|
||||
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 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
|
||||
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
|
||||
ombiUsers, code, err := app.ombi.GetUsers()
|
||||
ombiUsers, code, err := ombi.GetUsers()
|
||||
if err != nil || code != 200 {
|
||||
return nil, code, err
|
||||
}
|
||||
@ -66,10 +74,9 @@ func (app *appContext) getOmbiImportedUser(name string) (map[string]interface{},
|
||||
// @Security Bearer
|
||||
// @tags Ombi
|
||||
func (app *appContext) OmbiUsers(gc *gin.Context) {
|
||||
app.debug.Println("Ombi users requested")
|
||||
users, status, err := app.ombi.GetUsers()
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -105,7 +112,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
|
||||
}
|
||||
template, code, err := app.ombi.TemplateByID(req.ID)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -136,7 +143,11 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
|
||||
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 {
|
||||
switch v.(type) {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
@ -14,7 +16,6 @@ import (
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
func (app *appContext) GetProfiles(gc *gin.Context) {
|
||||
app.debug.Println("Profiles requested")
|
||||
out := getProfilesDTO{
|
||||
DefaultProfile: app.storage.GetDefaultProfile().Name,
|
||||
Profiles: map[string]profileDTO{},
|
||||
@ -52,10 +53,11 @@ func (app *appContext) GetProfiles(gc *gin.Context) {
|
||||
func (app *appContext) SetDefaultProfile(gc *gin.Context) {
|
||||
req := profileChangeDTO{}
|
||||
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 {
|
||||
app.err.Printf("Profile not found: \"%s\"", req.Name)
|
||||
respond(500, "Profile not found", gc)
|
||||
msg := fmt.Sprintf(lm.FailedGetProfile, req.Name)
|
||||
app.err.Println(msg)
|
||||
respond(500, msg, gc)
|
||||
return
|
||||
}
|
||||
app.storage.db.ForEach(&badgerhold.Query{}, func(profile *Profile) error {
|
||||
@ -79,13 +81,12 @@ func (app *appContext) SetDefaultProfile(gc *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
func (app *appContext) CreateProfile(gc *gin.Context) {
|
||||
app.info.Println("Profile creation requested")
|
||||
var req newProfileDTO
|
||||
gc.BindJSON(&req)
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
user, status, err := app.jf.UserByID(req.ID, false)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -94,12 +95,12 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
|
||||
Policy: user.Policy,
|
||||
Homescreen: req.Homescreen,
|
||||
}
|
||||
app.debug.Printf("Creating profile from user \"%s\"", user.Name)
|
||||
app.debug.Printf(lm.CreateProfileFromUser, user.Name)
|
||||
if req.Homescreen {
|
||||
profile.Configuration = user.Configuration
|
||||
profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -145,13 +146,13 @@ func (app *appContext) EnableReferralForProfile(gc *gin.Context) {
|
||||
inv, ok := app.storage.GetInvitesKey(invCode)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
profile, ok := app.storage.GetProfileKey(profileName)
|
||||
if !ok {
|
||||
respond(400, "Invalid profile", gc)
|
||||
app.err.Printf("\"%s\": Failed to enable referrals: profile not found", profileName)
|
||||
app.err.Printf(lm.FailedGetProfile, profileName)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@ -9,6 +10,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/hrfee/jfa-go/jellyseerr"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"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)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -133,8 +135,9 @@ func (app *appContext) SetMyContactMethods(gc *gin.Context) {
|
||||
func (app *appContext) LogoutUser(gc *gin.Context) {
|
||||
cookie, err := gc.Cookie("user-refresh")
|
||||
if err != nil {
|
||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||
respond(500, "Couldn't fetch cookies", gc)
|
||||
msg := fmt.Sprintf(lm.FailedGetCookies, "user-refresh", err)
|
||||
app.debug.Println(msg)
|
||||
respond(500, msg, gc)
|
||||
return
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to parse key: %s", err)
|
||||
app.err.Printf(lm.FailedParseJWT, err)
|
||||
fail()
|
||||
// respond(500, "unknownError", gc)
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
app.err.Printf("Failed to parse key: %s", err)
|
||||
app.err.Println(lm.FailedCastJWT)
|
||||
fail()
|
||||
// respond(500, "unknownError", gc)
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
||||
app.err.Printf("Invalid key")
|
||||
app.err.Println(lm.InvalidJWT)
|
||||
fail()
|
||||
// respond(400, "invalidKey", gc)
|
||||
return
|
||||
@ -212,7 +215,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
||||
Time: time.Now(),
|
||||
}, gc, true)
|
||||
|
||||
app.info.Println("Email list modified")
|
||||
app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId"))
|
||||
gc.Redirect(http.StatusSeeOther, "/my/account")
|
||||
return
|
||||
}
|
||||
@ -231,7 +234,6 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
||||
func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
||||
var req ModifyMyEmailDTO
|
||||
gc.BindJSON(&req)
|
||||
app.debug.Println("Email modification requested")
|
||||
if !strings.ContainsRune(req.Email, '@') {
|
||||
respond(400, "Invalid Email Address", gc)
|
||||
return
|
||||
@ -251,7 +253,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
||||
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to generate confirmation token: %v", err)
|
||||
app.err.Printf(lm.FailedSignJWT, err)
|
||||
respond(500, "errorUnknown", gc)
|
||||
return
|
||||
}
|
||||
@ -262,15 +264,15 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
||||
if status == 200 && err == nil {
|
||||
name = user.Name
|
||||
}
|
||||
app.debug.Printf("%s: Email confirmation required", id)
|
||||
app.debug.Printf(lm.EmailConfirmationRequired, id)
|
||||
respond(401, "confirmEmail", gc)
|
||||
msg, err := app.email.constructConfirmation("", name, key, app, false)
|
||||
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 {
|
||||
app.err.Printf("%s: Failed to send user confirmation email: %v", name, err)
|
||||
app.err.Printf(lm.FailedSendConfirmationEmail, id, req.Email, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent user confirmation email to \"%s\"", name, req.Email)
|
||||
app.err.Printf(lm.SentConfirmationEmail, id, req.Email)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -290,7 +292,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @tags User Page
|
||||
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
|
||||
if app.discord.inviteChannelName == "" {
|
||||
if app.discord.InviteChannel.Name == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
@ -338,7 +340,7 @@ func (app *appContext) GetMyPIN(gc *gin.Context) {
|
||||
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
dcUser, ok := app.discord.AssignedUserVerified(pin, gc.GetString("jfId"))
|
||||
app.discord.DeleteVerifiedUser(pin)
|
||||
app.discord.DeleteVerifiedToken(pin)
|
||||
if !ok {
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
@ -358,7 +360,7 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
||||
jellyseerr.FieldDiscord: dcUser.ID,
|
||||
jellyseerr.FieldDiscordEnabled: dcUser.Contact,
|
||||
}); 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{
|
||||
@ -413,7 +415,7 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
|
||||
jellyseerr.FieldTelegram: tgUser.ChatID,
|
||||
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
|
||||
}); 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{
|
||||
@ -477,12 +479,12 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
user, ok := app.matrix.tokens[pin]
|
||||
if !ok {
|
||||
app.debug.Println("Matrix: PIN not found")
|
||||
app.debug.Printf(lm.InvalidPIN, pin)
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -523,7 +525,7 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
|
||||
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldDiscordEnabled: false,
|
||||
}); 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{
|
||||
@ -551,7 +553,7 @@ func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
|
||||
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldTelegramEnabled: false,
|
||||
}); 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{
|
||||
@ -606,7 +608,6 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
||||
contactMethodAllowed := app.config.Section("user_page").Key("allow_pwr_contact_method").MustBool(true)
|
||||
address := gc.Param("address")
|
||||
if address == "" {
|
||||
app.debug.Println("Ignoring empty request for PWR")
|
||||
cancel.Stop()
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
@ -616,7 +617,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
||||
|
||||
jfUser, ok := app.ReverseUserSearch(address, usernameAllowed, emailAllowed, contactMethodAllowed)
|
||||
if !ok {
|
||||
app.debug.Printf("Ignoring PWR request: User not found")
|
||||
app.debug.Printf(lm.FailedGetUsers, lm.Jellyfin, "no results")
|
||||
|
||||
for range timerWait {
|
||||
respondBool(204, true, gc)
|
||||
@ -626,7 +627,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
||||
}
|
||||
pwr, err = app.GenInternalReset(jfUser.ID)
|
||||
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 {
|
||||
respondBool(204, true, gc)
|
||||
return
|
||||
@ -647,16 +648,16 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
||||
}, app, false,
|
||||
)
|
||||
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 {
|
||||
respondBool(204, true, gc)
|
||||
return
|
||||
}
|
||||
return
|
||||
} 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 {
|
||||
app.info.Printf("Sent password reset message to \"%s\"", address)
|
||||
app.info.Printf(lm.SentPWRMessage, pwr.Username, "?")
|
||||
}
|
||||
for range timerWait {
|
||||
respondBool(204, true, gc)
|
||||
@ -683,14 +684,13 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
||||
validation := app.validator.validate(req.New)
|
||||
for _, val := range validation {
|
||||
if !val {
|
||||
app.debug.Printf("%s: Change password failed: Invalid password", gc.GetString("jfId"))
|
||||
gc.JSON(400, validation)
|
||||
return
|
||||
}
|
||||
}
|
||||
user, status, err := app.jf.UserByID(gc.GetString("jfId"), false)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -718,16 +718,16 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
||||
func() {
|
||||
ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId"))
|
||||
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
|
||||
}
|
||||
ombiUser["password"] = req.New
|
||||
status, err = app.ombi.ModifyUser(ombiUser)
|
||||
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
|
||||
}
|
||||
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")
|
||||
@ -735,7 +735,7 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
|
||||
} else {
|
||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||
app.debug.Printf(lm.FailedGetCookies, "user-refresh", err)
|
||||
}
|
||||
respondBool(204, true, gc)
|
||||
}
|
||||
@ -761,7 +761,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
|
||||
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
|
||||
err = app.storage.db.Get(user.ReferralTemplateKey, &inv)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -782,6 +782,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
|
||||
// If UseReferralExpiry is enabled, we delete it and return nothing.
|
||||
app.storage.DeleteInvitesKey(inv.Code)
|
||||
if inv.UseReferralExpiry {
|
||||
app.debug.Printf(lm.DeleteOldReferral, inv.Code)
|
||||
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
|
||||
if ok {
|
||||
user.ReferralTemplateKey = ""
|
||||
@ -791,6 +792,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
app.debug.Printf(lm.RenewOldReferral, inv.Code)
|
||||
inv.Code = GenerateInviteCode()
|
||||
inv.Created = time.Now()
|
||||
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
|
||||
|
837
api-users.go
837
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/itchyny/timefmt-go"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
@ -122,14 +124,14 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
isInternal := false
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -138,7 +140,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
if reset, ok := app.internalPWRs[req.PIN]; ok {
|
||||
isInternal = true
|
||||
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)
|
||||
delete(app.internalPWRs, req.PIN)
|
||||
return
|
||||
@ -148,7 +150,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
|
||||
status, err := app.jf.ResetPasswordAdmin(userID)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -156,7 +158,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
} else {
|
||||
resp, status, err := app.jf.ResetPassword(req.PIN)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -176,7 +178,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
user, status, err = app.jf.UserByName(username, false)
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -195,31 +197,33 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
}
|
||||
status, err = app.jf.SetPassword(user.ID, prevPassword, req.Password)
|
||||
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)
|
||||
return
|
||||
}
|
||||
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 {
|
||||
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)
|
||||
return
|
||||
}
|
||||
} */
|
||||
ombiUser, status, err := app.getOmbiUser(user.ID)
|
||||
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)
|
||||
return
|
||||
}
|
||||
ombiUser["password"] = req.Password
|
||||
status, err = app.ombi.ModifyUser(ombiUser)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
@ -231,7 +235,6 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
app.info.Println("Config requested")
|
||||
resp := app.configBase
|
||||
// Load language options
|
||||
formOptions := app.storage.lang.User.getOptions()
|
||||
@ -341,7 +344,6 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) ModifyConfig(gc *gin.Context) {
|
||||
app.info.Println("Config modification requested")
|
||||
var req configDTO
|
||||
gc.BindJSON(&req)
|
||||
// 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")
|
||||
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)
|
||||
return
|
||||
}
|
||||
app.debug.Println("Config saved")
|
||||
app.info.Printf(lm.ModifyConfig, app.configPath)
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
if req["restart-program"] != nil && req["restart-program"].(bool) {
|
||||
app.info.Println("Restarting...")
|
||||
if TRAY {
|
||||
TRAYRESTART <- true
|
||||
} else {
|
||||
RESTART <- true
|
||||
}
|
||||
// Safety Sleep (Ensure shutdown tasks get done)
|
||||
time.Sleep(time.Second)
|
||||
app.Restart()
|
||||
}
|
||||
app.loadConfig()
|
||||
// Reinitialize password validator on config change, as opposed to every applicable request like in python.
|
||||
if _, ok := req["password_validation"]; ok {
|
||||
app.debug.Println("Reinitializing validator")
|
||||
validatorConf := ValidatorConf{
|
||||
"length": app.config.Section("password_validation").Key("min_length").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
|
||||
func (app *appContext) ApplyUpdate(gc *gin.Context) {
|
||||
if !app.update.CanUpdate {
|
||||
respond(400, "Update is manual", gc)
|
||||
app.info.Printf(lm.FailedApplyUpdate, lm.UpdateManual)
|
||||
respond(400, lm.UpdateManual, gc)
|
||||
return
|
||||
}
|
||||
err := app.update.update()
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to apply update: %v", err)
|
||||
app.err.Printf(lm.FailedApplyUpdate, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
@ -452,8 +447,9 @@ func (app *appContext) ApplyUpdate(gc *gin.Context) {
|
||||
func (app *appContext) Logout(gc *gin.Context) {
|
||||
cookie, err := gc.Cookie("refresh")
|
||||
if err != nil {
|
||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||
respond(500, "Couldn't fetch cookies", gc)
|
||||
msg := fmt.Sprintf(lm.FailedGetCookies, "refresh", err)
|
||||
app.debug.Println(msg)
|
||||
respond(500, msg, gc)
|
||||
return
|
||||
}
|
||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||
@ -526,11 +522,7 @@ func (app *appContext) ServeLang(gc *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) restart(gc *gin.Context) {
|
||||
app.info.Println("Restarting...")
|
||||
err := app.Restart()
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't restart, try restarting manually: %v", err)
|
||||
}
|
||||
app.Restart()
|
||||
}
|
||||
|
||||
// @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!
|
||||
func (app *appContext) Restart() error {
|
||||
app.info.Println(lm.Restarting)
|
||||
if TRAY {
|
||||
TRAYRESTART <- true
|
||||
} else {
|
||||
|
65
auth.go
65
auth.go
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
)
|
||||
@ -41,6 +42,8 @@ func (app *appContext) webAuth() gin.HandlerFunc {
|
||||
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.
|
||||
func CreateToken(userId, jfId string, admin bool) (string, string, error) {
|
||||
var token, refresh string
|
||||
@ -72,32 +75,26 @@ func (app *appContext) decodeValidateAuthHeader(gc *gin.Context) (claims jwt.Map
|
||||
ok = false
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
if header[0] != "Bearer" {
|
||||
app.debug.Println("Invalid authorization header")
|
||||
app.authLog(lm.InvalidAuthHeader)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
token, err := jwt.Parse(string(header[1]), checkToken)
|
||||
if err != nil {
|
||||
app.debug.Printf("Auth denied: %s", err)
|
||||
app.authLog(fmt.Sprintf(lm.FailedParseJWT, err))
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
claims, ok = token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
app.debug.Println("Invalid JWT")
|
||||
app.authLog(lm.FailedCastJWT)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
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)
|
||||
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))
|
||||
respond(401, "Unauthorized", gc)
|
||||
ok = false
|
||||
@ -115,7 +112,7 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
||||
}
|
||||
isAdminToken := claims["admin"].(bool)
|
||||
if !isAdminToken {
|
||||
app.debug.Printf("Auth denied: Token was not for admin access")
|
||||
app.authLog(lm.NonAdminToken)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@ -130,14 +127,13 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
app.debug.Printf("Couldn't find user ID \"%s\"", userID)
|
||||
app.authLog(fmt.Sprintf(lm.NonAdminUser, userID))
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
gc.Set("jfId", jfID)
|
||||
gc.Set("userId", userID)
|
||||
gc.Set("userMode", false)
|
||||
app.debug.Println("Auth succeeded")
|
||||
gc.Next()
|
||||
}
|
||||
|
||||
@ -160,7 +156,7 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context, userpage bool)
|
||||
password = creds[1]
|
||||
ok = false
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -173,16 +169,16 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
|
||||
user, status, err := app.authJf.Authenticate(username, password)
|
||||
if status != 200 || err != nil {
|
||||
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)
|
||||
return
|
||||
}
|
||||
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)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -199,7 +195,7 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
|
||||
// @tags Auth
|
||||
// @Security getTokenAuth
|
||||
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)
|
||||
if !ok {
|
||||
return
|
||||
@ -209,13 +205,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
for _, user := range app.adminUsers {
|
||||
if user.Username == username && user.Password == password {
|
||||
match = true
|
||||
app.debug.Println("Found existing user")
|
||||
userID = user.UserID
|
||||
break
|
||||
}
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -233,7 +228,7 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
}
|
||||
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
|
||||
if !accountsAdmin {
|
||||
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", username)
|
||||
app.authLog(fmt.Sprintf(lm.NonAdminUser, username))
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@ -243,12 +238,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
newUser := User{
|
||||
UserID: userID,
|
||||
}
|
||||
app.debug.Printf("Token generated for user \"%s\"", username)
|
||||
app.debug.Printf(lm.GenerateToken, username)
|
||||
app.adminUsers = append(app.adminUsers, newUser)
|
||||
}
|
||||
token, refresh, err := CreateToken(userID, jfID, true)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -261,35 +256,29 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
|
||||
ok = false
|
||||
cookie, err := gc.Cookie(cookieName)
|
||||
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)
|
||||
return
|
||||
}
|
||||
for _, token := range app.invalidTokens {
|
||||
if cookie == token {
|
||||
app.debug.Println("getTokenRefresh: Invalid token")
|
||||
respond(401, "Invalid token", gc)
|
||||
app.authLog(lm.LocallyInvalidatedJWT)
|
||||
respond(401, lm.InvalidJWT, gc)
|
||||
return
|
||||
}
|
||||
}
|
||||
token, err := jwt.Parse(cookie, checkToken)
|
||||
if err != nil {
|
||||
app.debug.Println("getTokenRefresh: Invalid token")
|
||||
respond(400, "Invalid token", gc)
|
||||
app.authLog(lm.FailedParseJWT)
|
||||
respond(400, lm.InvalidJWT, gc)
|
||||
return
|
||||
}
|
||||
claims, ok = token.Claims.(jwt.MapClaims)
|
||||
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)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
||||
app.debug.Printf("getTokenRefresh: Invalid token: %+v", err)
|
||||
respond(401, "Invalid token", gc)
|
||||
app.authLog(lm.InvalidJWT)
|
||||
respond(401, lm.InvalidJWT, gc)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
@ -304,7 +293,7 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
|
||||
// @Router /token/refresh [get]
|
||||
// @tags Auth
|
||||
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")
|
||||
if !ok {
|
||||
return
|
||||
@ -313,7 +302,7 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||
jfID := claims["jfid"].(string)
|
||||
jwt, refresh, err := CreateToken(userID, jfID, true)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
31
backups.go
31
backups.go
@ -7,6 +7,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -60,12 +62,12 @@ func (app *appContext) getBackups() *BackupList {
|
||||
path := app.config.Section("backups").Key("path").String()
|
||||
err := os.MkdirAll(path, 0755)
|
||||
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
|
||||
}
|
||||
items, err := os.ReadDir(path)
|
||||
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
|
||||
}
|
||||
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))
|
||||
if err != nil {
|
||||
app.debug.Printf("Failed to parse backup filename \"%s\": %v\n", item.Name(), err)
|
||||
app.debug.Printf(lm.FailedParseTime, err)
|
||||
continue
|
||||
}
|
||||
backups.dates[i] = t
|
||||
@ -101,36 +103,36 @@ func (app *appContext) makeBackup() (fileDetails CreateBackupDTO) {
|
||||
sort.Sort(backups)
|
||||
for _, item := range backups.files[:toDelete] {
|
||||
fullpath := filepath.Join(path, item.Name())
|
||||
app.debug.Printf("Deleting old backup \"%s\"\n", item.Name())
|
||||
err := os.Remove(fullpath)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to delete old backup \"%s\": %v\n", fullpath, err)
|
||||
app.err.Printf(lm.FailedDeleteOldBackup, fullpath, err)
|
||||
return
|
||||
}
|
||||
app.debug.Printf(lm.DeleteOldBackup, fullpath)
|
||||
}
|
||||
}
|
||||
fullpath := filepath.Join(path, fname)
|
||||
f, err := os.Create(fullpath)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to open backup file \"%s\": %v\n", fullpath, err)
|
||||
app.err.Printf(lm.FailedOpen, fullpath, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = app.storage.db.Badger().Backup(f, 0)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to create backup: %v\n", err)
|
||||
app.err.Printf(lm.FailedCreateBackup, err)
|
||||
return
|
||||
}
|
||||
|
||||
fstat, err := f.Stat()
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to get info on new backup: %v\n", err)
|
||||
app.err.Printf(lm.FailedStat, fullpath, err)
|
||||
return
|
||||
}
|
||||
fileDetails.Size = fileSize(fstat.Size())
|
||||
fileDetails.Name = fname
|
||||
fileDetails.Path = fullpath
|
||||
// fmt.Printf("Created backup %+v\n", fileDetails)
|
||||
app.debug.Printf(lm.CreateBackup, fileDetails)
|
||||
return
|
||||
}
|
||||
|
||||
@ -139,25 +141,25 @@ func (app *appContext) loadPendingBackup() {
|
||||
return
|
||||
}
|
||||
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)
|
||||
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()
|
||||
defer app.storage.db.Close()
|
||||
|
||||
f, err := os.Open(LOADBAK)
|
||||
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)
|
||||
f.Close()
|
||||
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 = ""
|
||||
}
|
||||
|
||||
@ -165,7 +167,6 @@ func newBackupDaemon(app *appContext) *GenericDaemon {
|
||||
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
|
||||
d := NewGenericDaemon(interval, app,
|
||||
func(app *appContext) {
|
||||
app.debug.Println("Backups: Creating backup")
|
||||
app.makeBackup()
|
||||
},
|
||||
)
|
||||
|
14
config.go
14
config.go
@ -7,8 +7,10 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/easyproxy"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
@ -140,7 +142,7 @@ func (app *appContext) loadConfig() error {
|
||||
}
|
||||
}
|
||||
if allDisabled {
|
||||
fmt.Println("SETALLTRUE")
|
||||
app.info.Println(lm.EnableAllPWRMethods)
|
||||
for _, v := range pwrMethods {
|
||||
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.proxyTransport, err = easyproxy.NewTransport(app.proxyConfig)
|
||||
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.MustSetValue("updates", "enabled", "true")
|
||||
|
@ -1120,6 +1120,15 @@
|
||||
"value": "",
|
||||
"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": {
|
||||
"name": "Language",
|
||||
"required": false,
|
||||
|
319
discord.go
319
discord.go
@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
dg "github.com/bwmarrin/discordgo"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
@ -16,7 +17,7 @@ type DiscordDaemon struct {
|
||||
username string
|
||||
tokens map[string]VerifToken // Map of pins to tokens.
|
||||
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
|
||||
channelID, channelName, inviteChannelID, inviteChannelName string
|
||||
Channel, InviteChannel struct{ ID, Name string }
|
||||
guildID 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.
|
||||
@ -92,13 +93,11 @@ func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) run() {
|
||||
d.bot.AddHandler(d.messageHandler)
|
||||
|
||||
d.bot.AddHandler(d.commandHandler)
|
||||
|
||||
d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers | dg.IntentsGuildInvites
|
||||
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
|
||||
}
|
||||
// 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
|
||||
guild, err := d.bot.Guild(d.guildID)
|
||||
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.serverName = guild.Name
|
||||
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
|
||||
d.channelName = channel
|
||||
d.Channel.Name = channel
|
||||
d.serverChannelName += "/" + channel
|
||||
}
|
||||
if d.app.config.Section("discord").Key("provide_invite").MustBool(false) {
|
||||
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"))
|
||||
@ -145,7 +144,7 @@ func (d *DiscordDaemon) ListRoles() (roles [][2]string, err error) {
|
||||
var r []*dg.Role
|
||||
r, err = d.bot.GuildRoles(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get roles: %v", err)
|
||||
d.app.err.Printf(lm.FailedGetDiscordRoles, err)
|
||||
return
|
||||
}
|
||||
for _, role := range r {
|
||||
@ -168,44 +167,62 @@ func (d *DiscordDaemon) ApplyRole(userID string) error {
|
||||
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.
|
||||
func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconURL string) {
|
||||
var inv *dg.Invite
|
||||
var err error
|
||||
if d.inviteChannelName == "" {
|
||||
d.app.err.Println("Discord: Cannot create invite without channel specified in settings.")
|
||||
if d.InviteChannel.Name == "" {
|
||||
d.app.err.Println(lm.FailedCreateDiscordInviteChannel, lm.InviteChannelEmpty)
|
||||
return
|
||||
}
|
||||
if d.inviteChannelID == "" {
|
||||
if d.InviteChannel.ID == "" {
|
||||
channels, err := d.bot.GuildChannels(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Couldn't get channel list: %v", err)
|
||||
d.app.err.Printf(lm.FailedGetDiscordChannels, err)
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for _, channel := range channels {
|
||||
// channel, err := d.bot.Channel(ch.ID)
|
||||
// if err != nil {
|
||||
// d.app.err.Printf("Discord: Couldn't get channel: %v", err)
|
||||
// d.app.err.Printf(lm.FailedGetDiscordChannel, ch.ID, err)
|
||||
// return
|
||||
// }
|
||||
if channel.Name == d.inviteChannelName {
|
||||
d.inviteChannelID = channel.ID
|
||||
if channel.Name == d.InviteChannel.Name {
|
||||
d.InviteChannel.ID = channel.ID
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
// channel, err := d.bot.Channel(d.inviteChannelID)
|
||||
// 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
|
||||
// }
|
||||
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],
|
||||
// Channel: channel,
|
||||
// Inviter: d.bot.State.User,
|
||||
@ -214,13 +231,13 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
|
||||
Temporary: false,
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create invite: %v", err)
|
||||
d.app.err.Printf(lm.FailedGenerateDiscordInvite, err)
|
||||
return
|
||||
}
|
||||
inviteURL = "https://discord.gg/" + inv.Code
|
||||
guild, err := d.bot.Guild(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
||||
d.app.err.Printf(lm.FailedGetDiscordGuild, err)
|
||||
return
|
||||
}
|
||||
iconURL = guild.IconURL("256")
|
||||
@ -255,7 +272,7 @@ func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
|
||||
1000,
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get members: %v", err)
|
||||
d.app.err.Printf(lm.FailedGetDiscordGuildMembers, err)
|
||||
return nil
|
||||
}
|
||||
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) {
|
||||
u, err := d.bot.User(ID)
|
||||
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
|
||||
}
|
||||
user.ID = ID
|
||||
@ -294,7 +311,7 @@ func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
|
||||
user.Discriminator = u.Discriminator
|
||||
channel, err := d.bot.UserChannelCreate(ID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create DM channel: %v", err)
|
||||
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, ID, err)
|
||||
return
|
||||
}
|
||||
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))
|
||||
i := 0
|
||||
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{
|
||||
Name: d.app.storage.lang.Telegram[code].Meta.Name,
|
||||
Value: code,
|
||||
@ -392,7 +409,7 @@ func (d *DiscordDaemon) registerCommands() {
|
||||
profiles := d.app.storage.GetProfiles()
|
||||
d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(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{
|
||||
Name: profile.Name,
|
||||
Value: profile.Name,
|
||||
@ -409,9 +426,9 @@ func (d *DiscordDaemon) registerCommands() {
|
||||
for i, cmd := range d.commandDescriptions {
|
||||
command, err := d.bot.ApplicationCommandCreate(d.bot.State.User.ID, d.guildID, cmd)
|
||||
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 {
|
||||
d.app.debug.Printf("Discord: registered command \"%s\"", cmd.Name)
|
||||
d.app.debug.Printf(lm.RegisterDiscordCommand, cmd.Name)
|
||||
d.commandIDs[i] = command.ID
|
||||
}
|
||||
}
|
||||
@ -420,12 +437,12 @@ func (d *DiscordDaemon) registerCommands() {
|
||||
func (d *DiscordDaemon) deregisterCommands() {
|
||||
existingCommands, err := d.bot.ApplicationCommands(d.bot.State.User.ID, d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get commands: %v", err)
|
||||
d.app.err.Printf(lm.FailedGetDiscordCommands, err)
|
||||
return
|
||||
}
|
||||
for _, cmd := range existingCommands {
|
||||
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()
|
||||
d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(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{
|
||||
Name: 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])
|
||||
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 {
|
||||
d.commandIDs[3] = cmd.ID
|
||||
}
|
||||
@ -452,19 +469,20 @@ func (d *DiscordDaemon) UpdateCommands() {
|
||||
|
||||
func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
|
||||
if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok {
|
||||
if i.GuildID != "" && d.channelName != "" {
|
||||
if d.channelID == "" {
|
||||
if i.GuildID != "" && d.Channel.Name != "" {
|
||||
if d.Channel.ID == "" {
|
||||
channel, err := s.Channel(i.ChannelID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
|
||||
d.channelName = ""
|
||||
d.app.err.Printf(lm.FailedGetDiscordChannel, i.ChannelID, err)
|
||||
d.app.err.Println(lm.MonitorAllDiscordChannels)
|
||||
d.Channel.Name = ""
|
||||
}
|
||||
if channel.Name == d.channelName {
|
||||
d.channelID = channel.ID
|
||||
if channel.Name == d.Channel.Name {
|
||||
d.Channel.ID = channel.ID
|
||||
}
|
||||
}
|
||||
if d.channelID != i.ChannelID {
|
||||
d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
|
||||
if d.Channel.ID != i.ChannelID {
|
||||
d.app.debug.Printf(lm.IgnoreOutOfChannelMessage, lm.Discord)
|
||||
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) {
|
||||
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
|
||||
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
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -521,7 +539,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
||||
},
|
||||
})
|
||||
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)
|
||||
return
|
||||
@ -535,7 +553,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
||||
},
|
||||
})
|
||||
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.JellyfinID = user.JellyfinID
|
||||
@ -566,7 +584,7 @@ func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang str
|
||||
},
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
|
||||
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
|
||||
}
|
||||
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
|
||||
requesterEmail, ok := d.app.storage.GetEmailsKey(requester.JellyfinID)
|
||||
if !ok {
|
||||
d.app.err.Printf("Failed to verify admin")
|
||||
}
|
||||
if !requesterEmail.Admin {
|
||||
d.app.err.Printf("User is not admin")
|
||||
//add response message
|
||||
if !(ok && requesterEmail.Admin) {
|
||||
d.app.err.Printf(lm.FailedGenerateInvite, fmt.Sprintf(lm.NonAdminUser, requester.JellyfinID))
|
||||
// FIXME: add response message
|
||||
return
|
||||
}
|
||||
|
||||
@ -629,7 +644,7 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
||||
ValidTill: validTill,
|
||||
UserLabel: userLabel,
|
||||
Profile: "Default",
|
||||
Label: fmt.Sprintf("Discord: %s", RenderDiscordUsername(recipient)),
|
||||
Label: fmt.Sprintf("%s: %s", lm.Discord, RenderDiscordUsername(recipient)),
|
||||
}
|
||||
if profileName != "" {
|
||||
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) {
|
||||
d.app.debug.Printf("%s: Sending invite message", invite.Code)
|
||||
invname, err := d.bot.GuildMember(d.guildID, recipient.ID)
|
||||
invite.SendTo = invname.User.Username
|
||||
msg, err := d.app.email.constructInvite(invite.Code, invite, d.app, false)
|
||||
if err != nil {
|
||||
invite.SendTo = fmt.Sprintf("Failed to send to %s", RenderDiscordUsername(recipient))
|
||||
d.app.err.Printf("%s: Failed to construct invite message: %v", invite.Code, err)
|
||||
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, invite.Code, err)
|
||||
d.app.err.Println(invite.SendTo)
|
||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Data: &dg.InteractionResponseData{
|
||||
@ -653,14 +667,14 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
||||
},
|
||||
})
|
||||
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 {
|
||||
var err error
|
||||
err = d.app.discord.SendDM(msg, recipient.ID)
|
||||
if err != nil {
|
||||
invite.SendTo = fmt.Sprintf("Failed to send to %s", RenderDiscordUsername(recipient))
|
||||
d.app.err.Printf("%s: %s: %v", invite.Code, invite.SendTo, err)
|
||||
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, RenderDiscordUsername(recipient), err)
|
||||
d.app.err.Println(invite.SendTo)
|
||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Data: &dg.InteractionResponseData{
|
||||
@ -669,10 +683,10 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
||||
},
|
||||
})
|
||||
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 {
|
||||
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{
|
||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Data: &dg.InteractionResponseData{
|
||||
@ -681,7 +695,7 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
||||
},
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
channels := make([]string, len(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.
|
||||
func (d *DiscordDaemon) UserVerified(pin string) (user DiscordUser, ok bool) {
|
||||
user, ok = d.verifiedTokens[pin]
|
||||
func (d *DiscordDaemon) UserVerified(pin string) (ContactMethodUser, bool) {
|
||||
u, ok := 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.
|
||||
@ -900,7 +780,44 @@ func (d *DiscordDaemon) UserExists(id string) bool {
|
||||
return err != nil || c > 0
|
||||
}
|
||||
|
||||
// DeleteVerifiedUser removes the token with the given PIN.
|
||||
func (d *DiscordDaemon) DeleteVerifiedUser(pin string) {
|
||||
delete(d.verifiedTokens, pin)
|
||||
// Exists returns whether or not the given user exists.
|
||||
func (d *DiscordDaemon) Exists(user ContactMethodUser) bool {
|
||||
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/html"
|
||||
"github.com/hrfee/jfa-go/easyproxy"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/itchyny/timefmt-go"
|
||||
"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))
|
||||
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 {
|
||||
app.err.Printf("Error while initiating SMTP mailer: %v", err)
|
||||
app.err.Printf(lm.FailedInitSMTP, err)
|
||||
}
|
||||
} else if method == "mailgun" {
|
||||
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.
|
||||
template["pin_code"] = pwr.Pin
|
||||
} else {
|
||||
app.info.Println("Couldn't generate PWR link: %v", err)
|
||||
app.info.Println(lm.FailedGeneratePWRLink, err)
|
||||
template["pin"] = pwr.Pin
|
||||
}
|
||||
} else {
|
||||
|
@ -1,6 +1,10 @@
|
||||
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
|
||||
|
||||
@ -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) run() {
|
||||
d.app.info.Printf("%s started", d.name)
|
||||
d.app.info.Printf(lm.StartDaemon, d.name)
|
||||
for {
|
||||
select {
|
||||
case <-d.ShutdownChannel:
|
7
go.mod
7
go.mod
@ -1,6 +1,6 @@
|
||||
module github.com/hrfee/jfa-go
|
||||
|
||||
go 1.20
|
||||
go 1.22.4
|
||||
|
||||
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/logmessages => ./logmessages
|
||||
|
||||
replace github.com/hrfee/jfa-go/linecache => ./linecache
|
||||
|
||||
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/easyproxy v0.0.0-00010101000000-000000000000
|
||||
github.com/hrfee/jfa-go/linecache v0.0.0-20230626224816-f72960635dc3
|
||||
github.com/hrfee/jfa-go/logger v0.0.0-20230626224816-f72960635dc3
|
||||
github.com/hrfee/jfa-go/logger v0.0.0-20240731152135-2d066ea7cd32
|
||||
github.com/hrfee/jfa-go/ombi v0.0.0-20230626224816-f72960635dc3
|
||||
github.com/hrfee/mediabrowser v0.3.13
|
||||
github.com/itchyny/timefmt-go v0.1.5
|
||||
@ -91,6 +93,7 @@ require (
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hrfee/jfa-go/jellyseerr v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.6 // indirect
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dgraph-io/badger/v3"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
@ -12,7 +13,7 @@ import (
|
||||
// meant to be called with other such housekeeping functions, so assumes
|
||||
// the user cache is fresh.
|
||||
func (app *appContext) clearEmails() {
|
||||
app.debug.Println("Housekeeping: removing unused email addresses")
|
||||
app.debug.Println(lm.HousekeepingEmail)
|
||||
emails := app.storage.GetEmails()
|
||||
for _, email := range emails {
|
||||
_, _, 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.
|
||||
func (app *appContext) clearDiscord() {
|
||||
app.debug.Println("Housekeeping: removing unused Discord IDs")
|
||||
app.debug.Println(lm.HousekeepingDiscord)
|
||||
discordUsers := app.storage.GetDiscord()
|
||||
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
|
||||
switch err.(type) {
|
||||
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)
|
||||
default:
|
||||
if user.Policy.IsDisabled {
|
||||
app.discord.RemoveRole(discordUser.MethodID().(string))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
@ -44,7 +50,7 @@ func (app *appContext) clearDiscord() {
|
||||
|
||||
// clearMatrix does the same as clearEmails, but for Matrix Users.
|
||||
func (app *appContext) clearMatrix() {
|
||||
app.debug.Println("Housekeeping: removing unused Matrix IDs")
|
||||
app.debug.Println(lm.HousekeepingMatrix)
|
||||
matrixUsers := app.storage.GetMatrix()
|
||||
for _, matrixUser := range matrixUsers {
|
||||
_, _, 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.
|
||||
func (app *appContext) clearTelegram() {
|
||||
app.debug.Println("Housekeeping: removing unused Telegram IDs")
|
||||
app.debug.Println(lm.HousekeepingTelegram)
|
||||
telegramUsers := app.storage.GetTelegram()
|
||||
for _, telegramUser := range telegramUsers {
|
||||
_, _, err := app.jf.UserByID(telegramUser.JellyfinID, false)
|
||||
@ -75,7 +81,7 @@ func (app *appContext) clearTelegram() {
|
||||
}
|
||||
|
||||
func (app *appContext) clearPWRCaptchas() {
|
||||
app.debug.Println("Housekeeping: Clearing old PWR Captchas")
|
||||
app.debug.Println(lm.HousekeepingCaptcha)
|
||||
captchas := map[string]Captcha{}
|
||||
for k, capt := range app.pwrCaptchas {
|
||||
if capt.Generated.Add(CAPTCHA_VALIDITY * time.Second).After(time.Now()) {
|
||||
@ -86,7 +92,7 @@ func (app *appContext) clearPWRCaptchas() {
|
||||
}
|
||||
|
||||
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)
|
||||
maxAgeDays := app.config.Section("activity_log").Key("delete_after_days").MustInt(90)
|
||||
minAge := time.Now().AddDate(0, 0, -maxAgeDays)
|
||||
@ -103,7 +109,7 @@ func (app *appContext) clearActivities() {
|
||||
}
|
||||
}
|
||||
if err == badger.ErrTxnTooBig {
|
||||
app.debug.Printf("Activities: Delete txn was too big, doing it manually.")
|
||||
app.debug.Printf(lm.ActivityLogTxnTooBig)
|
||||
list := []Activity{}
|
||||
if errorSource == 0 {
|
||||
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 {
|
||||
d := NewGenericDaemon(interval, app,
|
||||
func(app *appContext) {
|
||||
app.debug.Println("Housekeeping: Checking for expired invites")
|
||||
app.debug.Println(lm.HousekeepingInvites)
|
||||
app.checkInvites()
|
||||
},
|
||||
func(app *appContext) { app.clearActivities() },
|
||||
@ -128,7 +134,7 @@ func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaem
|
||||
d.Name("Housekeeping daemon")
|
||||
|
||||
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)
|
||||
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)
|
@ -246,6 +246,7 @@
|
||||
<div class="card ~neutral @low mb-2 unfocused">
|
||||
<span class="heading">{{ .lang.Ombi.title }}</span>
|
||||
<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">
|
||||
<input type="checkbox" class="mr-2" id="ombi-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
||||
</label>
|
||||
@ -258,6 +259,23 @@
|
||||
<input type="text" class="input ~neutral @low mt-4" id="ombi-api_key">
|
||||
<p class="support mb-2 mt-1">{{ .lang.Ombi.apiKeyNotice }}</p>
|
||||
</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">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
@ -423,6 +441,7 @@
|
||||
<div id="password-resets" class="card ~neutral @low mb-2 unfocused related-to-email">
|
||||
<span class="heading">{{ .lang.PasswordResets.title }}</span>
|
||||
<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">
|
||||
<input type="checkbox" class="mr-2" id="password_resets-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
||||
</label>
|
||||
@ -541,4 +560,3 @@
|
||||
<script src="js/setup.js" type="module"></script>
|
||||
</body>
|
||||
</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"
|
||||
|
||||
"github.com/hrfee/jfa-go/jellyseerr"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||
user, imported, err := app.js.GetOrImportUser(jfID)
|
||||
if err != nil {
|
||||
app.debug.Printf("Failed to get or trigger import for Jellyseerr (user \"%s\"): %v", jfID, err)
|
||||
app.debug.Printf(lm.FailedImportUser, lm.Jellyseerr, jfID, err)
|
||||
return
|
||||
}
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
@ -27,7 +28,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||
if ok && email.Addr != "" && user.Email != email.Addr {
|
||||
err = app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: email.Addr})
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
|
||||
app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err)
|
||||
} else {
|
||||
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
|
||||
}
|
||||
@ -51,7 +52,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||
if len(contactMethods) != 0 {
|
||||
err := app.js.ModifyNotifications(jfID, contactMethods)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,7 +60,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||
func (app *appContext) SynchronizeJellyseerrUsers() {
|
||||
users, status, err := app.jf.GetUsers(false)
|
||||
if err != nil || status != 200 {
|
||||
app.err.Printf("Failed to get users (%d): %s", status, err)
|
||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||
return
|
||||
}
|
||||
// I'm sure Jellyseerr can handle it,
|
1
lang.go
1
lang.go
@ -122,6 +122,7 @@ type setupLang struct {
|
||||
Login langSection `json:"login"`
|
||||
JellyfinEmby langSection `json:"jellyfinEmby"`
|
||||
Ombi langSection `json:"ombi"`
|
||||
Jellyseerr langSection `json:"jellyseerr"`
|
||||
Email langSection `json:"email"`
|
||||
Messages langSection `json:"messages"`
|
||||
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",
|
||||
"confirmationRequiredMessage": "Bitte überprüfe deinen Posteingang und bestätige deine E-Mail-Adresse.",
|
||||
"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.",
|
||||
"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",
|
||||
@ -30,7 +30,20 @@
|
||||
"joinTheServer": "Server beitreten:",
|
||||
"userPageSuccessMessage": "Du kannst die Details deines Accounts später in {myAccount} Seite einsehen und ändern.",
|
||||
"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": {
|
||||
"length": {
|
||||
@ -67,6 +80,10 @@
|
||||
"errorNoEmail": "E-Mail Adresse erforderlich.",
|
||||
"errorCaptcha": "Captcha falsch.",
|
||||
"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",
|
||||
"listenAddress": "Listen Address",
|
||||
"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",
|
||||
"darkTheme": "Dark",
|
||||
"useHTTPS": "Use HTTPS",
|
||||
@ -94,7 +94,13 @@
|
||||
"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.",
|
||||
"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": {
|
||||
"title": "Messages",
|
||||
@ -134,6 +140,7 @@
|
||||
"passwordResets": {
|
||||
"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.",
|
||||
"moreInfo": "More information about the different ways of resetting passwords can be found on {n}.",
|
||||
"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\".",
|
||||
"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/jellyseerr"
|
||||
"github.com/hrfee/jfa-go/logger"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/jfa-go/ombi"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
@ -101,8 +102,9 @@ type appContext struct {
|
||||
// Keeping jf name because I can't think of a better one
|
||||
jf *mediabrowser.MediaBrowser
|
||||
authJf *mediabrowser.MediaBrowser
|
||||
ombi *ombi.Ombi
|
||||
js *jellyseerr.Jellyseerr
|
||||
ombi *OmbiWrapper
|
||||
js *JellyseerrWrapper
|
||||
thirdPartyServices []ThirdPartyService
|
||||
datePattern string
|
||||
timePattern string
|
||||
storage Storage
|
||||
@ -111,6 +113,7 @@ type appContext struct {
|
||||
telegram *TelegramDaemon
|
||||
discord *DiscordDaemon
|
||||
matrix *MatrixDaemon
|
||||
contactMethods []ContactMethodLinker
|
||||
info, debug, err *logger.Logger
|
||||
host string
|
||||
port int
|
||||
@ -213,22 +216,21 @@ func start(asDaemon, firstCall bool) {
|
||||
firstRun = true
|
||||
dConfig, err := fs.ReadFile(localFS, "config-default.ini")
|
||||
if err != nil {
|
||||
app.err.Fatalf("Couldn't find default config file")
|
||||
app.err.Fatalf(lm.NoConfig)
|
||||
}
|
||||
nConfig, err := os.Create(app.configPath)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
err = os.MkdirAll(filepath.Dir(app.configPath), 0760)
|
||||
}
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't open config file for writing: \"%s\"", app.configPath)
|
||||
app.err.Fatalf("Error: %s", err)
|
||||
app.err.Fatalf(lm.FailedWriting, app.configPath, err)
|
||||
}
|
||||
defer nConfig.Close()
|
||||
_, err = nConfig.Write(dConfig)
|
||||
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.Section("").Key("first_run").SetValue("true")
|
||||
tempConfig.SaveTo(app.configPath)
|
||||
@ -237,8 +239,9 @@ func start(asDaemon, firstCall bool) {
|
||||
var debugMode bool
|
||||
var address string
|
||||
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) {
|
||||
firstRun = true
|
||||
@ -270,7 +273,7 @@ func start(asDaemon, firstCall bool) {
|
||||
os.Remove(SOCK)
|
||||
listener, err := net.Listen("unix", SOCK)
|
||||
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)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
@ -286,13 +289,13 @@ func start(asDaemon, firstCall bool) {
|
||||
for {
|
||||
con, err := listener.Accept()
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't read message on %s: %s", SOCK, err)
|
||||
app.err.Printf(lm.FailedSocketRead, SOCK, err)
|
||||
continue
|
||||
}
|
||||
buf := make([]byte, 512)
|
||||
nr, err := con.Read(buf)
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't read message on %s: %s", SOCK, err)
|
||||
app.err.Printf(lm.FailedSocketRead, SOCK, err)
|
||||
continue
|
||||
}
|
||||
command := string(buf[0:nr])
|
||||
@ -317,13 +320,13 @@ func start(asDaemon, firstCall bool) {
|
||||
err = app.storage.loadLang(langFS, os.DirFS(externalLang))
|
||||
}
|
||||
if err != nil {
|
||||
app.info.Fatalf("Failed to load language files: %+v\n", err)
|
||||
app.info.Fatalf(lm.FailedLangLoad, err)
|
||||
}
|
||||
|
||||
if !firstRun {
|
||||
app.host = app.config.Section("ui").Key("host").String()
|
||||
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)
|
||||
} else {
|
||||
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)
|
||||
|
||||
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) {
|
||||
app.debug.Printf("Connecting to Ombi")
|
||||
app.ombi = &OmbiWrapper{}
|
||||
app.debug.Printf(lm.UsingOmbi)
|
||||
ombiServer := app.config.Section("ombi").Key("server").String()
|
||||
app.ombi = ombi.NewOmbi(
|
||||
app.ombi.Ombi = ombi.NewOmbi(
|
||||
ombiServer,
|
||||
app.config.Section("ombi").Key("api_key").String(),
|
||||
common.NewTimeoutHandler("Ombi", ombiServer, true),
|
||||
)
|
||||
|
||||
app.thirdPartyServices = append(app.thirdPartyServices, app.ombi)
|
||||
}
|
||||
|
||||
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()
|
||||
app.js = jellyseerr.NewJellyseerr(
|
||||
app.js.Jellyseerr = jellyseerr.NewJellyseerr(
|
||||
jellyseerrServer,
|
||||
app.config.Section("jellyseerr").Key("api_key").String(),
|
||||
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
|
||||
)
|
||||
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
|
||||
// app.js.LogRequestBodies = true
|
||||
app.thirdPartyServices = append(app.thirdPartyServices, app.js)
|
||||
|
||||
}
|
||||
|
||||
@ -398,10 +404,9 @@ func start(asDaemon, firstCall bool) {
|
||||
if stringServerType == "emby" {
|
||||
serverType = mediabrowser.EmbyServer
|
||||
timeoutHandler = mediabrowser.NewNamedTimeoutHandler("Emby", "\""+server+"\"", true)
|
||||
app.info.Println("Using Emby server type")
|
||||
fmt.Println(warning("WARNING: Emby compatibility is experimental, and support is limited.\nPassword resets are not available."))
|
||||
app.info.Println(lm.UsingEmby)
|
||||
} else {
|
||||
app.info.Println("Using Jellyfin server type")
|
||||
app.info.Println(lm.UsingJellyfin)
|
||||
}
|
||||
|
||||
app.jf, err = mediabrowser.NewServer(
|
||||
@ -415,7 +420,7 @@ func start(asDaemon, firstCall bool) {
|
||||
cacheTimeout,
|
||||
)
|
||||
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 {
|
||||
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)
|
||||
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)
|
||||
|
||||
@ -448,8 +453,9 @@ func start(asDaemon, firstCall bool) {
|
||||
user.Username = app.config.Section("ui").Key("username").String()
|
||||
user.Password = app.config.Section("ui").Key("password").String()
|
||||
app.adminUsers = append(app.adminUsers, user)
|
||||
app.info.Println(lm.UsingLocalAuth)
|
||||
} 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)
|
||||
if debugMode {
|
||||
app.authJf.Verbose = true
|
||||
@ -512,34 +518,42 @@ func start(asDaemon, firstCall bool) {
|
||||
defer backupDaemon.Shutdown()
|
||||
}
|
||||
|
||||
if telegramEnabled {
|
||||
app.telegram, err = newTelegramDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to authenticate with Telegram: %v", err)
|
||||
telegramEnabled = false
|
||||
} else {
|
||||
go app.telegram.run()
|
||||
defer app.telegram.Shutdown()
|
||||
}
|
||||
}
|
||||
// NOTE: The order in which these are placed in app.contactMethods matters.
|
||||
// Add new ones to the end.
|
||||
if discordEnabled {
|
||||
app.discord, err = newDiscordDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to authenticate with Discord: %v", err)
|
||||
app.err.Printf(lm.FailedInitDiscord, err)
|
||||
discordEnabled = false
|
||||
} else {
|
||||
app.debug.Println(lm.InitDiscord)
|
||||
go app.discord.run()
|
||||
defer app.discord.Shutdown()
|
||||
app.contactMethods = append(app.contactMethods, app.discord)
|
||||
}
|
||||
}
|
||||
if telegramEnabled {
|
||||
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 {
|
||||
app.matrix, err = newMatrixDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to initialize Matrix daemon: %v", err)
|
||||
app.err.Printf(lm.FailedInitMatrix, err)
|
||||
matrixEnabled = false
|
||||
} else {
|
||||
app.debug.Println(lm.InitMatrix)
|
||||
go app.matrix.run()
|
||||
defer app.matrix.Shutdown()
|
||||
app.contactMethods = append(app.contactMethods, app.matrix)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -558,7 +572,7 @@ func start(asDaemon, firstCall bool) {
|
||||
app.storage.lang.SetupPath = "setup"
|
||||
err := app.storage.loadLangSetup(langFS)
|
||||
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
|
||||
mime.AddExtensionType(".js", "application/javascript")
|
||||
|
||||
app.info.Println("Initializing router")
|
||||
app.info.Println(lm.InitRouter)
|
||||
router := app.loadRouter(address, debugMode)
|
||||
app.info.Println("Loading routes")
|
||||
app.info.Println(lm.LoadRoutes)
|
||||
if !firstRun {
|
||||
app.loadRoutes(router)
|
||||
} else {
|
||||
app.loadSetup(router)
|
||||
app.info.Printf("Loading setup @ %s", address)
|
||||
app.info.Printf(lm.LoadingSetup, address)
|
||||
}
|
||||
go func() {
|
||||
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("")
|
||||
if err := SRV.ListenAndServeTLS(cert, key); err != nil {
|
||||
filesToCheck := []string{cert, key}
|
||||
fileNames := []string{"Certificate", "Key"}
|
||||
fileNames := []string{lm.InvalidSSLCert, lm.InvalidSSLKey}
|
||||
for i, v := range filesToCheck {
|
||||
_, err := os.Stat(v)
|
||||
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 {
|
||||
app.err.Printf("Failure serving with SSL/TLS: %s", err)
|
||||
app.err.Printf(lm.FailServeSSL, err)
|
||||
} else {
|
||||
app.err.Fatalf("Failure serving with SSL/TLS: %s", err)
|
||||
app.err.Fatalf(lm.FailServeSSL, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := SRV.ListenAndServe(); err != nil {
|
||||
if err == http.ErrServerClosed {
|
||||
app.err.Printf("Failure serving: %s", err)
|
||||
app.err.Printf(lm.FailServe, err)
|
||||
} else {
|
||||
app.err.Fatalf("Failure serving: %s", err)
|
||||
app.err.Fatalf(lm.FailServe, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
if firstRun {
|
||||
app.info.Printf("Loaded, visit %s to start.", address)
|
||||
app.info.Printf(lm.ServingSetup, address)
|
||||
} else {
|
||||
app.info.Printf("Loaded @ %s", address)
|
||||
app.info.Printf(lm.Serving, address)
|
||||
}
|
||||
|
||||
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)
|
||||
defer cancel()
|
||||
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
|
||||
}
|
||||
|
||||
@ -631,7 +645,7 @@ func shutdown() {
|
||||
}
|
||||
|
||||
func (app *appContext) shutdown() {
|
||||
app.info.Println("Shutting down...")
|
||||
app.info.Println(lm.Quitting)
|
||||
shutdown()
|
||||
}
|
||||
|
||||
@ -715,15 +729,17 @@ func printVersion() {
|
||||
fmt.Println(info("jfa-go version: %s (%s)%s\n", hiwhite(version), white(commit), tray))
|
||||
}
|
||||
|
||||
const SYSTEMD_SERVICE = "jfa-go.service"
|
||||
|
||||
func main() {
|
||||
f, err := logOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to start logging: %v\n", err)
|
||||
fmt.Printf(lm.FailedLogging, err)
|
||||
}
|
||||
defer f()
|
||||
printVersion()
|
||||
SOCK = filepath.Join(temp, SOCK)
|
||||
fmt.Println("Socket:", SOCK)
|
||||
fmt.Printf(lm.SocketPath+"\n", SOCK)
|
||||
if flagPassed("test") {
|
||||
TEST = true
|
||||
}
|
||||
@ -752,24 +768,26 @@ func main() {
|
||||
} else if flagPassed("stop") {
|
||||
con, err := net.Dial("unix", SOCK)
|
||||
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)
|
||||
}
|
||||
_, err = con.Write([]byte("stop"))
|
||||
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)
|
||||
}
|
||||
fmt.Println("Sent.")
|
||||
fmt.Println(lm.SocketWrite)
|
||||
} else if flagPassed("daemon") {
|
||||
start(true, true)
|
||||
} else if flagPassed("systemd") {
|
||||
service, err := fs.ReadFile(localFS, "jfa-go.service")
|
||||
service, err := fs.ReadFile(localFS, SYSTEMD_SERVICE)
|
||||
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)
|
||||
}
|
||||
absPath, err := filepath.Abs(os.Args[0])
|
||||
absPath, err := os.Executable()
|
||||
if err != nil {
|
||||
absPath = os.Args[0]
|
||||
}
|
||||
@ -780,13 +798,13 @@ func main() {
|
||||
}
|
||||
}
|
||||
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 {
|
||||
fmt.Printf("Couldn't write jfa-go.service: %v\n", err)
|
||||
fmt.Printf(lm.FailedWriting+"\n", SYSTEMD_SERVICE, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
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".
|
||||
You can then run:
|
||||
|
||||
|
71
matrix.go
71
matrix.go
@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
@ -31,15 +32,6 @@ type UnverifiedUser struct {
|
||||
User *MatrixUser
|
||||
}
|
||||
|
||||
type MatrixUser struct {
|
||||
RoomID string
|
||||
Encrypted bool
|
||||
UserID string
|
||||
Lang string
|
||||
Contact bool
|
||||
JellyfinID string `badgerhold:"key"`
|
||||
}
|
||||
|
||||
var matrixFilter = mautrix.Filter{
|
||||
Room: mautrix.RoomFilter{
|
||||
Timeline: mautrix.FilterPart{
|
||||
@ -118,13 +110,13 @@ func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string
|
||||
|
||||
func (d *MatrixDaemon) run() {
|
||||
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)
|
||||
HandleSyncerCrypto(startTime, d, syncer)
|
||||
syncer.OnEventType(event.EventMessage, d.handleMessage)
|
||||
|
||||
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,
|
||||
)
|
||||
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
|
||||
}
|
||||
@ -203,7 +195,7 @@ func (d *MatrixDaemon) CreateRoom(userID string) (roomID id.RoomID, encrypted bo
|
||||
func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
||||
roomID, encrypted, err := d.CreateRoom(userID)
|
||||
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
|
||||
}
|
||||
lang := "en-us"
|
||||
@ -226,7 +218,7 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
||||
roomID,
|
||||
)
|
||||
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
|
||||
}
|
||||
ok = true
|
||||
@ -276,6 +268,57 @@ func (d *MatrixDaemon) UserExists(userID string) bool {
|
||||
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.
|
||||
|
||||
// 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
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto"
|
||||
"maunium.net/go/mautrix/event"
|
||||
@ -65,22 +68,22 @@ type olmLogger struct {
|
||||
}
|
||||
|
||||
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{}) {
|
||||
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{}) {
|
||||
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{}) {
|
||||
if strings.HasPrefix(message, "Got membership state event") {
|
||||
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) {
|
||||
@ -155,7 +158,7 @@ func HandleSyncerCrypto(startTime int64, d *MatrixDaemon, syncer *mautrix.Defaul
|
||||
// return
|
||||
// }
|
||||
if err != nil {
|
||||
d.app.err.Printf("Failed to decrypt Matrix message: %v", err)
|
||||
d.app.err.Printf(lm.FailedDecryptMatrixMessage, err)
|
||||
return
|
||||
}
|
||||
d.handleMessage(source, decrypted)
|
||||
@ -180,7 +183,7 @@ func EncryptRoom(d *MatrixDaemon, room *mautrix.RespCreateRoom, userID id.UserID
|
||||
if err == nil {
|
||||
encrypted = true
|
||||
} else {
|
||||
d.app.debug.Printf("Matrix: Failed to enable encryption in room: %v", err)
|
||||
d.app.debug.Printf(lm.FailedEnableMatrixEncryption, room.RoomID, err)
|
||||
return
|
||||
}
|
||||
d.isEncrypted[room.RoomID] = encrypted
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// NOTE: This is the one file where log messages are not part of logmessages/logmessages.go
|
||||
|
||||
func runMigrations(app *appContext) {
|
||||
migrateProfiles(app)
|
||||
migrateBootstrap(app)
|
||||
|
@ -31,7 +31,7 @@ type newUserDTO struct {
|
||||
|
||||
type newUserResponse struct {
|
||||
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.
|
||||
}
|
||||
|
||||
|
34
pwreset.go
34
pwreset.go
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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.
|
||||
@ -39,16 +40,16 @@ func (app *appContext) GenResetLink(pin string) (string, error) {
|
||||
}
|
||||
|
||||
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()
|
||||
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
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't initialise password reset daemon")
|
||||
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
||||
return
|
||||
}
|
||||
defer watcher.Close()
|
||||
@ -56,7 +57,7 @@ func (app *appContext) StartPWR() {
|
||||
go pwrMonitor(app, watcher)
|
||||
err = watcher.Add(path)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to start password reset daemon: %s", err)
|
||||
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
||||
}
|
||||
|
||||
waitForRestart()
|
||||
@ -84,43 +85,36 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
||||
var pwr PasswordReset
|
||||
data, err := os.ReadFile(event.Name)
|
||||
if err != nil {
|
||||
app.debug.Printf("PWR: Failed to read file: %v", err)
|
||||
app.debug.Printf(lm.FailedReading, event.Name, err)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(data, &pwr)
|
||||
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
|
||||
}
|
||||
app.info.Printf("New password reset for user \"%s\"", pwr.Username)
|
||||
if currentTime := time.Now(); pwr.Expiry.After(currentTime) {
|
||||
user, status, err := app.jf.UserByName(pwr.Username, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
if !(status == 200 || status == 204) || err != nil || user.ID == "" {
|
||||
app.err.Printf(lm.FailedGetUser, pwr.Username, lm.Jellyfin, err)
|
||||
return
|
||||
}
|
||||
uid := user.ID
|
||||
if uid == "" {
|
||||
app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username)
|
||||
return
|
||||
}
|
||||
name := app.getAddressOrName(uid)
|
||||
if name != "" {
|
||||
msg, err := app.email.constructReset(pwr, app, false)
|
||||
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct password reset message for \"%s\"", pwr.Username)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
|
||||
} else if err := app.sendByID(msg, uid); err != nil {
|
||||
app.err.Printf("Failed to send password reset message to \"%s\"", name)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, name, err)
|
||||
} else {
|
||||
app.info.Printf("Sent password reset message to \"%s\"", name)
|
||||
app.err.Printf(lm.SentPWRMessage, pwr.Username, name)
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
return
|
||||
}
|
||||
app.err.Printf("Password reset daemon: %s", err)
|
||||
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
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/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
@ -21,17 +22,17 @@ func (app *appContext) loadHTML(router *gin.Engine) {
|
||||
templatePath := "html"
|
||||
htmlFiles, err := fs.ReadDir(localFS, templatePath)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath)
|
||||
app.err.Fatalf(lm.FailedReading, templatePath, err)
|
||||
return
|
||||
}
|
||||
loadInternal := []string{}
|
||||
loadExternal := []string{}
|
||||
for _, f := range htmlFiles {
|
||||
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()))
|
||||
} 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())))
|
||||
}
|
||||
}
|
||||
@ -39,13 +40,13 @@ func (app *appContext) loadHTML(router *gin.Engine) {
|
||||
if len(loadInternal) != 0 {
|
||||
tmpl, err = template.ParseFS(localFS, loadInternal...)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to load templates: %v", err)
|
||||
app.err.Fatalf(lm.FailedLoadTemplates, lm.Internal, err)
|
||||
}
|
||||
}
|
||||
if len(loadExternal) != 0 {
|
||||
tmpl, err = tmpl.ParseFiles(loadExternal...)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to load external templates: %v", err)
|
||||
app.err.Fatalf(lm.FailedLoadTemplates, lm.External, err)
|
||||
}
|
||||
}
|
||||
router.SetHTMLTemplate(tmpl)
|
||||
@ -96,7 +97,7 @@ func (app *appContext) loadRouter(address string, debug bool) *gin.Engine {
|
||||
router.Use(static.Serve("/", app.webFS))
|
||||
router.NoRoute(app.NoRouteHandler)
|
||||
if *PPROF {
|
||||
app.debug.Println("Loading pprof")
|
||||
app.debug.Println(lm.RegisterPprof)
|
||||
pprof.Register(router)
|
||||
}
|
||||
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+"/token/login", app.getTokenLogin)
|
||||
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.GET(p+"/invite/:invCode", app.InviteProxy)
|
||||
if app.config.Section("captcha").Key("enabled").MustBool(false) {
|
||||
@ -165,7 +166,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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)
|
||||
api.DELETE(p+"/users", app.DeleteUsers)
|
||||
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.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
|
||||
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/hrfee/jfa-go/easyproxy"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
)
|
||||
|
||||
@ -104,7 +105,7 @@ func (app *appContext) TestJF(gc *gin.Context) {
|
||||
case 404:
|
||||
msg = "error404"
|
||||
}
|
||||
app.info.Printf("Auth failed with code %d (%s)", status, err)
|
||||
app.err.Printf(lm.FailedAuthJellyfin, req.Server, status, err)
|
||||
if msg != "" {
|
||||
respond(status, msg, gc)
|
||||
} else {
|
||||
@ -151,16 +152,20 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
|
||||
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
||||
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
||||
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.Proxy, &fallback.Proxy, &english.Proxy)
|
||||
patchLang(&lang.EndPage, &fallback.EndPage, &english.EndPage)
|
||||
patchLang(&lang.Language, &fallback.Language, &english.Language)
|
||||
patchLang(&lang.Login, &fallback.Login, &english.Login)
|
||||
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.Messages, &fallback.Messages, &english.Messages)
|
||||
patchLang(&lang.Notifications, &fallback.Notifications, &english.Notifications)
|
||||
patchLang(&lang.UserPage, &fallback.UserPage, &english.UserPage)
|
||||
patchLang(&lang.WelcomeEmails, &fallback.WelcomeEmails, &english.WelcomeEmails)
|
||||
patchLang(&lang.PasswordResets, &fallback.PasswordResets, &english.PasswordResets)
|
||||
patchLang(&lang.InviteEmails, &fallback.InviteEmails, &english.InviteEmails)
|
||||
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 == "" {
|
||||
patchLang(&lang.Strings, &english.Strings)
|
||||
patchLang(&lang.StartPage, &english.StartPage)
|
||||
patchLang(&lang.EndPage, &english.EndPage)
|
||||
patchLang(&lang.General, &english.General)
|
||||
patchLang(&lang.Updates, &english.Updates)
|
||||
patchLang(&lang.Proxy, &english.Proxy)
|
||||
patchLang(&lang.EndPage, &english.EndPage)
|
||||
patchLang(&lang.Language, &english.Language)
|
||||
patchLang(&lang.Login, &english.Login)
|
||||
patchLang(&lang.JellyfinEmby, &english.JellyfinEmby)
|
||||
patchLang(&lang.Ombi, &english.Ombi)
|
||||
patchLang(&lang.Jellyseerr, &english.Jellyseerr)
|
||||
patchLang(&lang.Email, &english.Email)
|
||||
patchLang(&lang.Messages, &english.Messages)
|
||||
patchLang(&lang.Notifications, &english.Notifications)
|
||||
patchLang(&lang.UserPage, &english.UserPage)
|
||||
patchLang(&lang.WelcomeEmails, &english.WelcomeEmails)
|
||||
patchLang(&lang.PasswordResets, &english.PasswordResets)
|
||||
patchLang(&lang.InviteEmails, &english.InviteEmails)
|
||||
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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
@ -13,6 +14,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/jellyseerr"
|
||||
"github.com/hrfee/jfa-go/logger"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
"gopkg.in/ini.v1"
|
||||
@ -175,10 +177,10 @@ func (app *appContext) ConnectDB() {
|
||||
opts.ValueDir = app.storage.db_path
|
||||
db, err := badgerhold.Open(opts)
|
||||
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.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.
|
||||
@ -508,6 +510,15 @@ func (st *Storage) GetDefaultProfile() Profile {
|
||||
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.
|
||||
func (st *Storage) GetCustomContent() []CustomContent {
|
||||
result := []CustomContent{}
|
||||
@ -583,12 +594,35 @@ func (st *Storage) DeleteActivityKey(k string) {
|
||||
st.db.Delete(k, Activity{})
|
||||
}
|
||||
|
||||
type TelegramUser struct {
|
||||
JellyfinID string `badgerhold:"key"`
|
||||
ChatID int64 `badgerhold:"index"`
|
||||
Username string `badgerhold:"index"`
|
||||
Lang string
|
||||
Contact bool // Whether to contact through telegram or not
|
||||
type ThirdPartyService interface {
|
||||
// ok implies user imported, err can be any issue that occurs during
|
||||
ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool)
|
||||
AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error)
|
||||
Enabled(app *appContext, profile *Profile) bool
|
||||
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 {
|
||||
@ -601,6 +635,24 @@ type DiscordUser struct {
|
||||
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 {
|
||||
Addr string `badgerhold:"index"`
|
||||
Label string // User Label.
|
||||
@ -685,6 +737,16 @@ type Invite struct {
|
||||
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 {
|
||||
Answer string
|
||||
Image []byte // image/png
|
||||
@ -718,22 +780,27 @@ type Lang struct {
|
||||
func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
||||
err = st.loadLangCommon(filesystems...)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("common: %v", err)
|
||||
return
|
||||
}
|
||||
err = st.loadLangAdmin(filesystems...)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("admin: %v", err)
|
||||
return
|
||||
}
|
||||
err = st.loadLangEmail(filesystems...)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("email: %v", err)
|
||||
return
|
||||
}
|
||||
err = st.loadLangUser(filesystems...)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("user: %v", err)
|
||||
return
|
||||
}
|
||||
err = st.loadLangPWR(filesystems...)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("pwr: %v", err)
|
||||
return
|
||||
}
|
||||
err = st.loadLangTelegram(filesystems...)
|
||||
|
75
telegram.go
75
telegram.go
@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
@ -15,9 +16,27 @@ const (
|
||||
)
|
||||
|
||||
type TelegramVerifiedToken struct {
|
||||
ChatID int64
|
||||
Username string
|
||||
JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
|
||||
JellyfinID string `badgerhold:"key"` // optional, for ensuring a user-requested change is only accessed by them.
|
||||
ChatID int64 `badgerhold:"index"`
|
||||
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.
|
||||
@ -96,12 +115,12 @@ func (t *TelegramDaemon) NewAssignedAuthToken(id string) string {
|
||||
}
|
||||
|
||||
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.Timeout = 60
|
||||
updates, err := t.bot.GetUpdatesChan(u)
|
||||
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
|
||||
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"})
|
||||
err := t.Reply(upd, content)
|
||||
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)
|
||||
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
|
||||
}
|
||||
@ -232,14 +251,14 @@ func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string)
|
||||
if !ok || time.Now().After(token.Expiry) {
|
||||
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
|
||||
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)
|
||||
return
|
||||
}
|
||||
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
|
||||
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{
|
||||
ChatID: upd.Message.Chat.ID,
|
||||
@ -273,7 +292,39 @@ func (t *TelegramDaemon) UserExists(username string) bool {
|
||||
return err != nil || c > 0
|
||||
}
|
||||
|
||||
// DeleteVerifiedToken removes the token with the given PIN.
|
||||
func (t *TelegramDaemon) DeleteVerifiedToken(pin string) {
|
||||
delete(t.verifiedTokens, pin)
|
||||
// Exists returns whether or not the given user exists.
|
||||
func (t *TelegramDaemon) Exists(user ContactMethodUser) bool {
|
||||
return t.UserExists(user.Name())
|
||||
}
|
||||
|
||||
// DeleteVerifiedToken removes the token with the given PIN.
|
||||
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);
|
||||
html("language-description", window.lang.var("language", "description", `<a target="_blank" href="https://weblate.jfa-go.com">Weblate</a>`));
|
||||
html("email-description", window.lang.var("email", "description", `<a target="_blank" href="https://mailgun.com">Mailgun</a>`));
|
||||
html("email-dateformat-notice", window.lang.var("email", "dateFormatNotice", `<a target="_blank" href="https://strftime.ninja/">strftime.ninja</a>`));
|
||||
html("updates-description", window.lang.var("updates", "description", `<a target="_blank" href="https://builds.hrfee.dev/view/hrfee/jfa-go">buildrone</a>`));
|
||||
html("messages-description", window.lang.var("messages", "description", `<a target="_blank" href="https://wiki.jfa-go.com">Wiki</a>`));
|
||||
replaceLink("language-description", "language", "description", "https://weblate.jfa-go.com", "Weblate");
|
||||
replaceLink("email-description", "email", "description", "https://mailgun.com", "Mailgun");
|
||||
replaceLink("email-dateformat-notice", "email", "dateFormatNotice", "https://strftime.timpetricola.com/", "strftime.timpetricola.com");
|
||||
replaceLink("updates-description", "updates", "description", "https://builds.hrfee.dev/view/hrfee/jfa-go", "buildrone");
|
||||
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 = {
|
||||
"jellyfin": {
|
||||
@ -318,6 +322,12 @@ const settings = {
|
||||
"server": new Input(get("ombi-server"), "", "", "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": {
|
||||
"tls": new Checkbox(get("advanced-tls"), "", false, "advanced", "tls"),
|
||||
"tls_port": new Input(get("advanced-tls_port"), "", "", "tls", true, "advanced"),
|
||||
|
@ -4,7 +4,8 @@
|
||||
"target": "es2017",
|
||||
"lib": ["dom", "es2017"],
|
||||
"typeRoots": ["./typings", "../node_modules/@types"],
|
||||
"moduleResolution": "nodenext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
)
|
||||
|
||||
@ -560,15 +562,16 @@ func (app *appContext) checkForUpdates() {
|
||||
if err != nil && strings.Contains(err.Error(), "strconv.ParseInt") {
|
||||
app.err.Println("No new updates available.")
|
||||
} 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
|
||||
}
|
||||
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)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get update (%d): %v", status, err)
|
||||
app.err.Printf(lm.FailedGetUpdate, err)
|
||||
return
|
||||
}
|
||||
app.tag = tag
|
||||
|
18
user-auth.go
18
user-auth.go
@ -1,9 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
func (app *appContext) userAuth() gin.HandlerFunc {
|
||||
@ -13,7 +15,7 @@ func (app *appContext) userAuth() gin.HandlerFunc {
|
||||
func (app *appContext) userAuthenticate(gc *gin.Context) {
|
||||
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true)
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -27,7 +29,6 @@ func (app *appContext) userAuthenticate(gc *gin.Context) {
|
||||
|
||||
gc.Set("jfId", jfID)
|
||||
gc.Set("userMode", true)
|
||||
app.debug.Println("Auth succeeded")
|
||||
gc.Next()
|
||||
}
|
||||
|
||||
@ -41,11 +42,11 @@ func (app *appContext) userAuthenticate(gc *gin.Context) {
|
||||
// @Security getUserTokenAuth
|
||||
func (app *appContext) getUserTokenLogin(gc *gin.Context) {
|
||||
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)
|
||||
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)
|
||||
if !ok {
|
||||
return
|
||||
@ -58,12 +59,11 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
|
||||
|
||||
token, refresh, err := CreateToken(user.ID, user.ID, false)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
app.debug.Printf("Token generated for non-admin user \"%s\"", username)
|
||||
uri := "/my"
|
||||
if strings.HasPrefix(gc.Request.RequestURI, app.URLBase) {
|
||||
uri = "/accounts/my"
|
||||
@ -81,12 +81,12 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
|
||||
func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
|
||||
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(true)
|
||||
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)
|
||||
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")
|
||||
if !ok {
|
||||
return
|
||||
@ -96,7 +96,7 @@ func (app *appContext) getUserTokenRefresh(gc *gin.Context) {
|
||||
|
||||
jwt, refresh, err := CreateToken(jfID, jfID, false)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"time"
|
||||
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
)
|
||||
@ -21,17 +22,17 @@ func (app *appContext) checkUsers() {
|
||||
if len(app.storage.GetUserExpiries()) == 0 {
|
||||
return
|
||||
}
|
||||
app.info.Println("Daemon: Checking for user expiry")
|
||||
app.info.Println(lm.CheckUserExpiries)
|
||||
users, status, err := app.jf.GetUsers(false)
|
||||
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
|
||||
}
|
||||
mode := "disable"
|
||||
term := "Disabling"
|
||||
phrase := lm.DisableExpiredUser
|
||||
if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" {
|
||||
mode = "delete"
|
||||
term = "Deleting"
|
||||
phrase = lm.DeleteExpiredUser
|
||||
}
|
||||
contact := false
|
||||
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() {
|
||||
id := expiry.JellyfinID
|
||||
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)
|
||||
} else if time.Now().After(expiry.Expiry) {
|
||||
found := false
|
||||
@ -58,11 +59,10 @@ func (app *appContext) checkUsers() {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
app.info.Printf("Expired user already deleted, ignoring.")
|
||||
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
|
||||
continue
|
||||
}
|
||||
app.info.Printf("%s expired user \"%s\"", term, user.Name)
|
||||
app.info.Printf(phrase, user.Name)
|
||||
|
||||
// Record activity
|
||||
activity := Activity{
|
||||
@ -72,18 +72,24 @@ func (app *appContext) checkUsers() {
|
||||
}
|
||||
|
||||
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
|
||||
// Store the user name, since there's no longer a user ID to reference back to
|
||||
activity.Value = user.Name
|
||||
} else if mode == "disable" {
|
||||
user.Policy.IsDisabled = true
|
||||
// Admins can't be disabled
|
||||
// so they're not an admin anymore, sorry
|
||||
user.Policy.IsAdministrator = false
|
||||
status, err = app.jf.SetPolicy(id, user.Policy)
|
||||
err, _, _ = app.SetUserDisabled(user, true)
|
||||
activity.Type = ActivityDisabled
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -98,11 +104,11 @@ func (app *appContext) checkUsers() {
|
||||
name := app.getAddressOrName(user.ID)
|
||||
msg, err := app.email.constructUserExpired(app, false)
|
||||
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 {
|
||||
app.err.Printf("Failed to send expiry message to \"%s\": %s", name, err)
|
||||
app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, name, err)
|
||||
} 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
|
||||
}
|
169
views.go
169
views.go
@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
@ -16,6 +17,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/gomarkdown/markdown"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"github.com/steambap/captcha"
|
||||
@ -66,10 +68,9 @@ func (app *appContext) pushResources(gc *gin.Context, page Page) {
|
||||
toPush = []string{}
|
||||
}
|
||||
if pusher := gc.Writer.Pusher(); pusher != nil {
|
||||
app.debug.Println("Using HTTP2 Server push")
|
||||
for _, f := range toPush {
|
||||
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
|
||||
l, err := fs.ReadFile(localFS, "LICENSE")
|
||||
if err != nil {
|
||||
app.debug.Printf("Failed to load LICENSE: %s", err)
|
||||
app.debug.Printf(lm.FailedReading, "LICENSE", err)
|
||||
license = ""
|
||||
}
|
||||
license = string(l)
|
||||
fontLicense, err := fs.ReadFile(localFS, filepath.Join("web", "fonts", "OFL.txt"))
|
||||
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"
|
||||
@ -236,7 +237,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
|
||||
"server_channel": app.discord.serverChannelName,
|
||||
}))
|
||||
data["discordServerName"] = app.discord.serverName
|
||||
data["discordInviteLink"] = app.discord.inviteChannelName != ""
|
||||
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
|
||||
}
|
||||
if data["linkResetEnabled"].(bool) {
|
||||
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)
|
||||
// If it's a bot, pretend to be a success so the preview is nice.
|
||||
if isBot {
|
||||
app.debug.Println("PWR: Ignoring magic link visit from bot")
|
||||
app.debug.Println(lm.IgnoreBotPWR)
|
||||
data["success"] = true
|
||||
data["pin"] = "NO-BO-TS"
|
||||
return
|
||||
@ -338,13 +339,13 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
||||
if !isInternal && !setPassword {
|
||||
resp, status, err = app.jf.ResetPassword(pin)
|
||||
} 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)
|
||||
return
|
||||
} else {
|
||||
status, err = app.jf.ResetPasswordAdmin(pwr.ID)
|
||||
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 {
|
||||
status, err = app.jf.SetPassword(pwr.ID, "", pin)
|
||||
}
|
||||
@ -358,7 +359,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
||||
username = resp.UsersReset[0]
|
||||
}
|
||||
} 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.
|
||||
@ -378,21 +379,21 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
jfUser, status, err := app.jf.UserByName(username, false)
|
||||
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
|
||||
}
|
||||
ombiUser, status, err := app.getOmbiUser(jfUser.ID)
|
||||
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
|
||||
}
|
||||
ombiUser["password"] = pin
|
||||
status, err = app.ombi.ModifyUser(ombiUser)
|
||||
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
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to generate captcha: %v", err)
|
||||
app.err.Printf(lm.FailedGenerateCaptcha, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
@ -470,7 +471,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
|
||||
captchaID := genAuthToken()
|
||||
var buf bytes.Buffer
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -503,8 +504,12 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
|
||||
ok := true
|
||||
if !isPWR {
|
||||
inv, ok := app.storage.GetInvitesKey(code)
|
||||
if !ok || (!isPWR && inv.Captchas == nil) {
|
||||
app.debug.Printf("Couldn't find invite \"%s\"", code)
|
||||
if !ok {
|
||||
app.debug.Printf(lm.InvalidInviteCode, code)
|
||||
return false
|
||||
}
|
||||
if !isPWR && inv.Captchas == nil {
|
||||
app.debug.Printf(lm.CaptchaNotFound, id, code)
|
||||
return false
|
||||
}
|
||||
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]
|
||||
}
|
||||
if !ok {
|
||||
app.debug.Printf("Couldn't find Captcha \"%s\"", id)
|
||||
app.debug.Printf(lm.CaptchaNotFound, id, code)
|
||||
return false
|
||||
}
|
||||
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")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
app.err.Printf("Failed to read reCAPTCHA status (%d): %+v\n", resp.Status, err)
|
||||
if err == nil && resp.StatusCode != 200 {
|
||||
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
|
||||
}
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
|
||||
return false
|
||||
}
|
||||
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)
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to unmarshal reCAPTCHA response: %+v\n", err)
|
||||
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
|
||||
return false
|
||||
}
|
||||
|
||||
hostname := app.config.Section("captcha").Key("recaptcha_hostname").MustString("")
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -607,23 +616,7 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
app.pushResources(gc, FormPage)
|
||||
code := gc.Param("invCode")
|
||||
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. */
|
||||
// if app.checkInvite(code, false, "") {
|
||||
inv, ok := app.storage.GetInvitesKey(code)
|
||||
if !ok {
|
||||
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
||||
"urlBase": app.getURLBase(gc),
|
||||
"cssClass": app.cssClass,
|
||||
"cssVersion": cssVersion,
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
||||
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),
|
||||
@ -638,7 +631,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
invKeys, ok := app.ConfirmationKeys[code]
|
||||
invKeys, ok := app.ConfirmationKeys[invite.Code]
|
||||
if !ok {
|
||||
fail()
|
||||
return
|
||||
@ -651,24 +644,46 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
token, err := jwt.Parse(key, checkToken)
|
||||
if err != nil {
|
||||
fail()
|
||||
app.err.Printf("Failed to parse key: %s", err)
|
||||
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) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
||||
if !(ok && token.Valid && claims["invite"].(string) == invite.Code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
||||
fail()
|
||||
app.debug.Printf("Invalid key")
|
||||
app.debug.Printf(lm.InvalidJWT)
|
||||
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)
|
||||
|
||||
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)
|
||||
@ -683,16 +698,42 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
"jfLink": jfLink,
|
||||
})
|
||||
}
|
||||
delete(invKeys, key)
|
||||
app.confirmationKeysLock.Lock()
|
||||
app.ConfirmationKeys[code] = invKeys
|
||||
app.confirmationKeysLock.Unlock()
|
||||
// Re-fetch invKeys just incase an update occurred
|
||||
invKeys, ok = app.ConfirmationKeys[invite.Code]
|
||||
if !ok {
|
||||
fail()
|
||||
return
|
||||
}
|
||||
email := ""
|
||||
if invite, ok := app.storage.GetInvitesKey(code); ok {
|
||||
email = invite.SendTo
|
||||
delete(invKeys, key)
|
||||
app.ConfirmationKeys[invite.Code] = invKeys
|
||||
app.confirmationKeysLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
app.pushResources(gc, FormPage)
|
||||
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. */
|
||||
// if app.checkInvite(code, false, "") {
|
||||
invite, ok := app.storage.GetInvitesKey(gc.Param("invCode"))
|
||||
if !ok {
|
||||
gcHTML(gc, 404, "invalidCode.html", gin.H{
|
||||
"urlBase": app.getURLBase(gc),
|
||||
"cssClass": app.cssClass,
|
||||
"cssVersion": cssVersion,
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
||||
app.NewUserFromConfirmationKey(invite, key, lang, gc)
|
||||
return
|
||||
}
|
||||
|
||||
email := invite.SendTo
|
||||
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
|
||||
email = ""
|
||||
}
|
||||
@ -707,8 +748,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
userPageAddress += "/my/account"
|
||||
|
||||
fromUser := ""
|
||||
if inv.ReferrerJellyfinID != "" {
|
||||
sender, status, err := app.jf.UserByID(inv.ReferrerJellyfinID, false)
|
||||
if invite.ReferrerJellyfinID != "" {
|
||||
sender, status, err := app.jf.UserByID(invite.ReferrerJellyfinID, false)
|
||||
if status == 200 && err == nil {
|
||||
fromUser = sender.Name
|
||||
}
|
||||
@ -730,13 +771,13 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
"strings": app.storage.lang.User[lang].Strings,
|
||||
"validationStrings": app.storage.lang.User[lang].validationStringsJSON,
|
||||
"notifications": app.storage.lang.User[lang].notificationsJSON,
|
||||
"code": code,
|
||||
"code": invite.Code,
|
||||
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
|
||||
"userExpiry": inv.UserExpiry,
|
||||
"userExpiryMonths": inv.UserMonths,
|
||||
"userExpiryDays": inv.UserDays,
|
||||
"userExpiryHours": inv.UserHours,
|
||||
"userExpiryMinutes": inv.UserMinutes,
|
||||
"userExpiry": invite.UserExpiry,
|
||||
"userExpiryMonths": invite.UserMonths,
|
||||
"userExpiryDays": invite.UserDays,
|
||||
"userExpiryHours": invite.UserHours,
|
||||
"userExpiryMinutes": invite.UserMinutes,
|
||||
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
|
||||
"langName": lang,
|
||||
"passwordReset": false,
|
||||
@ -771,7 +812,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
"server_channel": app.discord.serverChannelName,
|
||||
}))
|
||||
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 {
|
||||
data["customSuccessCard"] = true
|
||||
|
Loading…
Reference in New Issue
Block a user