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

Compare commits

..

No commits in common. "62e27c394d8f2b301ab5b69a2e13cd6760b6fbf8" and "2d066ea7cd3251b49e2772126e34ef607b18cbe2" have entirely different histories.

52 changed files with 1920 additions and 2179 deletions

172
.drone.yml.old Normal file
View File

@ -0,0 +1,172 @@
---
name: jfa-go
kind: pipeline
type: docker
steps:
- name: fetch
image: docker:git
commands:
- git fetch --tags
- name: release
image: hrfee/jfa-go-build-docker:latest
volumes:
- name: ssh_key
path: /id_rsa
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
GITHUB_TOKEN:
from_secret: github_token
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
commands:
- curl -sL https://git.io/goreleaser > ../goreleaser
- chmod +x ../goreleaser
- ./scripts/version.sh ../goreleaser
- wget https://builds.hrfee.pw/upload.py -P ../
- pip3 install requests
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "rm /repo/incoming/*.deb"'
- bash -c 'python3 ../upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'
volumes:
- name: ssh_key
host:
path: /root/.ssh/id_rsa_packaging
trigger:
event:
- tag
---
name: docker-buildx
kind: pipeline
type: docker
steps:
- name: build-deploy
image: appleboy/drone-ssh
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
settings:
host:
from_secret: ssh2_host
username:
from_secret: ssh2_username
port:
from_secret: ssh2_port
envs:
- buildrone_key
key:
from_secret: ssh2_key
command_timeout: 50m
script:
- /mnt/buildx/jfa-go/build.sh stable
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py
- pip3 install requests
- bash -c 'cd /mnt/buildx/jfa-go/jfa-go && python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-stable=true'
- rm -f /mnt/buildx/jfa-go/jfa-go/upload.py
trigger:
event:
- tag
---
name: jfa-go-git
kind: pipeline
type: docker
steps:
- name: build
image: hrfee/jfa-go-build-docker:latest
volumes:
- name: ssh_key
path: /id_rsa
- name: ssh_key2
path: /id_rsa2
commands:
- curl -sL https://git.io/goreleaser > goreleaser
- chmod +x goreleaser
- ./scripts/version.sh ./goreleaser --snapshot --skip=publish --clean
- wget https://builds.hrfee.pw/upload.py
- pip3 install requests
- bash -c 'sftp -i /id_rsa2 -o StrictHostKeyChecking=no root@161.97.102.153:/mnt/redoc <<< $"put docs/swagger.json jfa-go.json"'
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
# - bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "reprepro -Vb /repo remove trusty-unstable jfa-go"'
# - bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "reprepro -Vb /repo remove trusty-unstable jfa-go-tray"'
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "rm /repo/incoming/*.deb"'
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.zip ./dist/*.rpm ./dist/*.apk --tag internal-git=true'
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
JFA_GO_BUILT_BY:
from_secret: BUILT_BY
JFA_GO_SNAPSHOT: y
volumes:
- name: ssh_key
host:
path: /root/.ssh/id_rsa_packaging
- name: ssh_key2
host:
path: /root/.ssh/docker-build
trigger:
branch:
- main
- go1.16
event:
exclude:
- pull_request
---
name: docker-buildx-unstable
kind: pipeline
type: docker
steps:
- name: build-deploy
image: appleboy/drone-ssh
environment:
BUILDRONE_KEY:
from_secret: BUILDRONE_KEY
settings:
host:
from_secret: ssh2_host
username:
from_secret: ssh2_username
port:
from_secret: ssh2_port
envs:
- buildrone_key
key:
from_secret: ssh2_key
command_timeout: 50m
script:
- /mnt/buildx/jfa-go/build.sh
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py
- pip3 install requests
- bash -c 'cd /mnt/buildx/jfa-go/jfa-go && python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-unstable=true'
- rm -f /mnt/buildx/jfa-go/jfa-go/upload.py
trigger:
branch:
- main
event:
exclude:
- pull_request
---
name: jfa-go-pr
kind: pipeline
type: docker
steps:
- name: build
image: hrfee/jfa-go-build-docker:latest
commands:
- curl -sL https://git.io/goreleaser > goreleaser
- chmod +x goreleaser
- ./scripts/version.sh ./goreleaser --snapshot --skip=publish --clean
trigger:
event:
include:
- pull_request

1
.gitignore vendored
View File

@ -25,4 +25,3 @@ scripts/langmover/lang
scripts/langmover/lang2
scripts/langmover/out
tinyproxy.conf
static/banner.svg

View File

@ -11,7 +11,6 @@ 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

View File

@ -167,7 +167,6 @@ 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)/

View File

@ -2,7 +2,6 @@ package main
import (
"github.com/gin-gonic/gin"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/timshannon/badgerhold/v4"
)
@ -121,7 +120,7 @@ func (app *appContext) GetActivities(gc *gin.Context) {
err := app.storage.db.Find(&results, query)
if err != nil {
app.err.Printf(lm.FailedDBReadActivities, err)
app.err.Printf("Failed to read activities from DB: %v\n", err)
}
resp := GetActivitiesRespDTO{

View File

@ -8,7 +8,6 @@ import (
"time"
"github.com/gin-gonic/gin"
lm "github.com/hrfee/jfa-go/logmessages"
)
// @Summary Creates a backup of the database.
@ -36,7 +35,7 @@ func (app *appContext) GetBackup(gc *gin.Context) {
ok := (strings.HasPrefix(fname, BACKUP_PREFIX) || strings.HasPrefix(fname, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX)) && strings.HasSuffix(fname, BACKUP_SUFFIX)
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(lm.IgnoreInvalidFilename, fname, err)
app.debug.Printf("Ignoring backup DL request due to fname: %v\n", err)
respondBool(400, false, gc)
return
}
@ -84,7 +83,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(lm.IgnoreInvalidFilename, fname, err)
app.debug.Printf("Ignoring backup DL request due to fname: %v\n", err)
respondBool(400, false, gc)
return
}
@ -104,15 +103,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(lm.FailedGetUpload, err)
app.err.Printf("Failed to get file from form data: %v\n", err)
respondBool(400, false, gc)
return
}
app.debug.Printf(lm.GetUpload, file.Filename)
app.debug.Printf("Got uploaded file \"%s\"\n", file.Filename)
path := app.config.Section("backups").Key("path").String()
fullpath := filepath.Join(path, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX+time.Now().Local().Format(BACKUP_DATEFMT)+BACKUP_SUFFIX)
gc.SaveUploadedFile(file, fullpath)
app.debug.Printf(lm.Write, fullpath)
app.debug.Printf("Saved to \"%s\"\n", fullpath)
LOADBAK = fullpath
app.restart(gc)
}

View File

@ -8,7 +8,6 @@ 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"
@ -30,7 +29,6 @@ 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() {
@ -54,11 +52,60 @@ func (app *appContext) checkInvites() {
if !currentTime.After(expiry) {
continue
}
app.deleteExpiredInvite(data)
app.debug.Printf("Housekeeping: Deleting old invite %s", data.Code)
// Disable referrals for the user if UseReferralExpiry is enabled, so no new ones are made.
if data.IsReferral && data.UseReferralExpiry && data.ReferrerJellyfinID != "" {
user, ok := app.storage.GetEmailsKey(data.ReferrerJellyfinID)
if ok {
user.ReferralTemplateKey = ""
app.storage.SetEmailsKey(data.ReferrerJellyfinID, user)
}
}
notify := data.Notify
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
app.debug.Printf("%s: Expiry notification", data.Code)
var wait sync.WaitGroup
for address, settings := range notify {
if !settings["notify-expiry"] {
continue
}
wait.Add(1)
go func(addr string) {
defer wait.Done()
msg, err := app.email.constructExpiry(data.Code, data, app, false)
if err != nil {
app.err.Printf("%s: Failed to construct expiry notification: %v", data.Code, err)
} else {
// Check whether notify "address" is an email address of Jellyfin ID
if strings.Contains(addr, "@") {
err = app.email.send(msg, addr)
} else {
err = app.sendByID(msg, addr)
}
if err != nil {
app.err.Printf("%s: Failed to send expiry notification: %v", data.Code, err)
} else {
app.info.Printf("Sent expiry notification to %s", addr)
}
}
}(address)
}
wait.Wait()
}
app.storage.DeleteInvitesKey(data.Code)
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityDeleteInvite,
SourceType: ActivityDaemon,
InviteCode: data.Code,
Value: data.Label,
Time: time.Now(),
}, nil, false)
}
}
// checkInvite checks the validity of a specific invite, optionally removing it if invalid(ated).
func (app *appContext) checkInvite(code string, used bool, username string) bool {
currentTime := time.Now()
inv, match := app.storage.GetInvitesKey(code)
@ -67,8 +114,54 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
}
expiry := inv.ValidTill
if currentTime.After(expiry) {
app.deleteExpiredInvite(inv)
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
notify := inv.Notify
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
app.debug.Printf("%s: Expiry notification", code)
var wait sync.WaitGroup
for address, settings := range notify {
if !settings["notify-expiry"] {
continue
}
wait.Add(1)
go func(addr string) {
defer wait.Done()
msg, err := app.email.constructExpiry(code, inv, app, false)
if err != nil {
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
} else {
// Check whether notify "address" is an email address of Jellyfin ID
if strings.Contains(addr, "@") {
err = app.email.send(msg, addr)
} else {
err = app.sendByID(msg, addr)
}
if err != nil {
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
} else {
app.info.Printf("Sent expiry notification to %s", addr)
}
}
}(address)
}
wait.Wait()
}
if inv.IsReferral && inv.ReferrerJellyfinID != "" && inv.UseReferralExpiry {
user, ok := app.storage.GetEmailsKey(inv.ReferrerJellyfinID)
if ok {
user.ReferralTemplateKey = ""
app.storage.SetEmailsKey(inv.ReferrerJellyfinID, user)
}
}
match = false
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
@ -94,67 +187,6 @@ 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"
@ -164,7 +196,7 @@ func (app *appContext) sendAdminExpiryNotification(data Invite) *sync.WaitGroup
// @tags Invites
func (app *appContext) GenerateInvite(gc *gin.Context) {
var req generateInviteDTO
app.debug.Println(lm.GenerateInvite)
app.debug.Println("Generating new invite")
gc.BindJSON(&req)
currentTime := time.Now()
validTill := currentTime.AddDate(0, req.Months, req.Days)
@ -198,12 +230,13 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
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(lm.FailedSendToTooltipNoUser, req.SendTo)
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
} else if len(users) > 1 {
invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipMultiUser, req.SendTo)
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
} else {
invite.SendTo = req.SendTo
addressValid = true
@ -216,10 +249,8 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
if addressValid {
msg, err := app.email.constructInvite(invite.Code, invite, app, false)
if err != nil {
// Slight misuse of the template
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, req.SendTo, err)
app.err.Printf(lm.FailedConstructInviteMessage, invite.Code, err)
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: Failed to construct invite message: %v", invite.Code, err)
} else {
var err error
if discord != "" {
@ -228,10 +259,10 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
err = app.email.send(msg, req.SendTo)
}
if err != nil {
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, req.SendTo, err)
app.err.Println(invite.SendTo)
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: %s: %v", invite.Code, invite.SendTo, err)
} else {
app.info.Printf(lm.SentInviteMessage, invite.Code, req.SendTo)
app.info.Printf("%s: Sent invite email to \"%s\"", invite.Code, req.SendTo)
}
}
}
@ -266,6 +297,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
// @Security Bearer
// @tags Invites
func (app *appContext) GetInvites(gc *gin.Context) {
app.debug.Println("Invites requested")
currentTime := time.Now()
app.checkInvites()
var invites []inviteDTO
@ -300,7 +332,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(lm.FailedParseTime, err)
app.err.Printf("Failed to parse usedBy time: %v", err)
}
unix = date.Unix()
}
@ -315,6 +347,7 @@ 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")
@ -364,9 +397,10 @@ 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(lm.FailedGetProfile, req.Profile)
app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile)
respond(500, "Profile not found", gc)
return
}
@ -390,11 +424,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 {
msg := fmt.Sprintf(lm.InvalidInviteCode, code)
app.err.Println(msg)
respond(400, msg, gc)
app.err.Printf("%s Notification setting change failed: Invalid code", code)
respond(400, "Invalid invite code", gc)
return
}
var address string
@ -402,8 +436,9 @@ func (app *appContext) SetNotify(gc *gin.Context) {
if jellyfinLogin {
var addressAvailable bool = app.getAddressOrName(gc.GetString("jfId")) != ""
if !addressAvailable {
app.err.Printf(lm.FailedGetContactMethod, gc.GetString("jfId"))
respond(500, fmt.Sprintf(lm.FailedGetContactMethod, "admin"), gc)
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)
return
}
address = gc.GetString("jfId")
@ -418,12 +453,15 @@ func (app *appContext) SetNotify(gc *gin.Context) {
} /*else {
if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
*/
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-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)
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)
@ -442,6 +480,7 @@ 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)
@ -456,10 +495,10 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
Time: time.Now(),
}, gc, false)
app.info.Printf(lm.DeleteInvite, req.Code)
app.info.Printf("%s: Invite deleted", req.Code)
respondBool(200, true, gc)
return
}
app.err.Printf(lm.FailedDeleteInvite, req.Code, "invalid code")
app.err.Printf("%s: Deletion failed: Invalid code", req.Code)
respond(400, "Code doesn't exist", gc)
}

View File

@ -1,13 +1,10 @@
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.
@ -18,12 +15,14 @@ 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(lm.FailedGetUsers, lm.Jellyseerr, err)
app.err.Printf("Failed to get users from Jellyseerr: %v", err)
respond(500, "Couldn't get users", gc)
return
}
app.debug.Printf("Jellyseerr users retrieved: %d", len(users))
userlist := make([]ombiUser, len(users))
i := 0
for _, u := range users {
@ -61,14 +60,14 @@ func (app *appContext) SetJellyseerrProfile(gc *gin.Context) {
}
u, err := app.js.UserByID(jellyseerrID)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyseerr, err)
app.err.Printf("Couldn't get user from Jellyseerr: %v", err)
respond(500, "Couldn't get user", gc)
return
}
profile.Jellyseerr.User = u.UserTemplate
n, err := app.js.GetNotificationPreferencesByID(jellyseerrID)
if err != nil {
app.err.Printf(lm.FailedGetJellyseerrNotificationPrefs, gc.Param("id"), err)
app.err.Printf("Couldn't get user's notification prefs from Jellyseerr: %v", err)
respond(500, "Couldn't get user notification prefs", gc)
return
}
@ -99,67 +98,3 @@ 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)
}

View File

@ -6,7 +6,6 @@ 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"
)
@ -135,7 +134,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(lm.FailedGetCustomMessage, id)
app.err.Printf("Failed to get custom message with ID \"%s\"", id)
respondBool(400, false, gc)
return
}
@ -329,7 +328,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
jellyseerr.FieldTelegram: tgUser.ChatID,
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
linkExistingOmbiDiscordTelegram(app)
@ -362,7 +361,11 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
tgUser.Contact = req.Telegram
app.storage.SetTelegramKey(req.ID, tgUser)
if change {
app.debug.Printf(lm.SetContactPrefForService, lm.Telegram, tgUser.Username, req.Telegram)
msg := ""
if !req.Telegram {
msg = " not"
}
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
jsPrefs[jellyseerr.FieldTelegramEnabled] = req.Telegram
}
}
@ -371,7 +374,11 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
dcUser.Contact = req.Discord
app.storage.SetDiscordKey(req.ID, dcUser)
if change {
app.debug.Printf(lm.SetContactPrefForService, lm.Discord, dcUser.Username, req.Discord)
msg := ""
if !req.Discord {
msg = " not"
}
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
jsPrefs[jellyseerr.FieldDiscordEnabled] = req.Discord
}
}
@ -380,7 +387,11 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
mxUser.Contact = req.Matrix
app.storage.SetMatrixKey(req.ID, mxUser)
if change {
app.debug.Printf(lm.SetContactPrefForService, lm.Matrix, mxUser.UserID, req.Matrix)
msg := ""
if !req.Matrix {
msg = " not"
}
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
}
}
if email, ok := app.storage.GetEmailsKey(req.ID); ok {
@ -388,14 +399,18 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
email.Contact = req.Email
app.storage.SetEmailsKey(req.ID, email)
if change {
app.debug.Printf(lm.SetContactPrefForService, lm.Email, email.Addr, req.Email)
msg := ""
if !req.Email {
msg = " not"
}
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
jsPrefs[jellyseerr.FieldEmailEnabled] = req.Email
}
}
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
err := app.js.ModifyNotifications(req.ID, jsPrefs)
if err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
app.err.Printf("Failed to sync contact prefs with Jellyseerr: %v", err)
}
}
respondBool(200, true, gc)
@ -431,7 +446,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.DeleteVerifiedToken(pin)
app.discord.DeleteVerifiedUser(pin)
respondBool(400, false, gc)
return
}
@ -454,7 +469,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.MethodID().(string)) {
if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.ID) {
delete(app.discord.verifiedTokens, pin)
respondBool(400, false, gc)
return
@ -472,7 +487,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.InviteChannel.Name == "" {
if app.discord.inviteChannelName == "" {
respondBool(400, false, gc)
return
}
@ -540,7 +555,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.Printf(lm.InvalidInviteCode, code)
app.debug.Println("Matrix: Invite code was invalid")
respondBool(401, false, gc)
return
}
@ -548,12 +563,12 @@ func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
pin := gc.Param("pin")
user, ok := app.matrix.tokens[pin]
if !ok {
app.debug.Printf(lm.InvalidPIN, pin)
app.debug.Println("Matrix: PIN not found")
respondBool(200, false, gc)
return
}
if user.User.UserID != userID {
app.debug.Printf(lm.UnauthorizedPIN, pin)
app.debug.Println("Matrix: User ID of PIN didn't match")
respondBool(200, false, gc)
return
}
@ -581,7 +596,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(lm.FailedGenerateToken, err)
app.err.Printf("Matrix: Failed to generate token: %v", err)
respond(401, "Unauthorized", gc)
return
}
@ -592,7 +607,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(lm.FailedWriting, app.configPath, err)
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
respondBool(500, false, gc)
return
}
@ -616,7 +631,7 @@ func (app *appContext) MatrixConnect(gc *gin.Context) {
}
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
if err != nil {
app.err.Printf(lm.FailedCreateRoom, err)
app.err.Printf("Matrix: Failed to create room: %v", err)
respondBool(500, false, gc)
return
}
@ -686,7 +701,7 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
jellyseerr.FieldDiscord: req.DiscordID,
jellyseerr.FieldDiscordEnabled: true,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
@ -724,7 +739,7 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
jellyseerr.FieldDiscordEnabled: false,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
@ -760,7 +775,7 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
jellyseerr.FieldTelegramEnabled: false,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{

View File

@ -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,14 +22,6 @@ 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 {
@ -39,13 +31,13 @@ func (ombi *OmbiWrapper) getUser(username string, email string) (map[string]inte
return ombiUser, code, err
}
}
return nil, 400, errors.New(lm.NotFound)
return nil, 400, fmt.Errorf("couldn't find user")
}
// Returns a user with the given name who has been imported from Jellyfin/Emby by Ombi
func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, int, error) {
func (app *appContext) getOmbiImportedUser(name string) (map[string]interface{}, int, error) {
// Ombi User Types: 3/4 = Emby, 5 = Jellyfin
ombiUsers, code, err := ombi.GetUsers()
ombiUsers, code, err := app.ombi.GetUsers()
if err != nil || code != 200 {
return nil, code, err
}
@ -74,9 +66,10 @@ func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, i
// @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(lm.FailedGetUsers, lm.Ombi, err)
app.err.Printf("Failed to get users from Ombi (%d): %v", status, err)
respond(500, "Couldn't get users", gc)
return
}
@ -112,7 +105,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(lm.FailedGetUsers, lm.Ombi, err)
app.err.Printf("Couldn't get user from Ombi (%d): %v", code, err)
respond(500, "Couldn't get user", gc)
return
}
@ -143,11 +136,7 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
respondBool(204, true, gc)
}
type OmbiWrapper struct {
*ombi.Ombi
}
func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[string]interface{}) (status int, err error) {
func (app *appContext) applyOmbiProfile(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{}:
@ -158,66 +147,6 @@ func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[s
}
}
}
status, err = ombi.ModifyUser(user)
status, err = app.ombi.ModifyUser(user)
return
}
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)
}

View File

@ -1,11 +1,9 @@
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/timshannon/badgerhold/v4"
)
@ -16,6 +14,7 @@ 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{},
@ -53,11 +52,10 @@ func (app *appContext) GetProfiles(gc *gin.Context) {
func (app *appContext) SetDefaultProfile(gc *gin.Context) {
req := profileChangeDTO{}
gc.BindJSON(&req)
app.info.Printf(lm.SetDefaultProfile, req.Name)
app.info.Printf("Setting default profile to \"%s\"", req.Name)
if _, ok := app.storage.GetProfileKey(req.Name); !ok {
msg := fmt.Sprintf(lm.FailedGetProfile, req.Name)
app.err.Println(msg)
respond(500, msg, gc)
app.err.Printf("Profile not found: \"%s\"", req.Name)
respond(500, "Profile not found", gc)
return
}
app.storage.db.ForEach(&badgerhold.Query{}, func(profile *Profile) error {
@ -81,12 +79,13 @@ 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(lm.FailedGetUsers, lm.Jellyfin, err)
app.err.Printf("Failed to get user from Jellyfin (%d): %v", status, err)
respond(500, "Couldn't get user", gc)
return
}
@ -95,12 +94,12 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
Policy: user.Policy,
Homescreen: req.Homescreen,
}
app.debug.Printf(lm.CreateProfileFromUser, user.Name)
app.debug.Printf("Creating profile from user \"%s\"", 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(lm.FailedGetJellyfinDisplayPrefs, req.ID, err)
app.err.Printf("Failed to get DisplayPrefs (%d): %v", status, err)
respond(500, "Couldn't get displayprefs", gc)
return
}
@ -146,13 +145,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(lm.InvalidInviteCode, invCode)
app.err.Printf("\"%s\": Failed to enable referrals: invite not found", profileName)
return
}
profile, ok := app.storage.GetProfileKey(profileName)
if !ok {
respond(400, "Invalid profile", gc)
app.err.Printf(lm.FailedGetProfile, profileName)
app.err.Printf("\"%s\": Failed to enable referrals: profile not found", profileName)
return
}

View File

@ -1,7 +1,6 @@
package main
import (
"fmt"
"net/http"
"os"
"strings"
@ -10,7 +9,6 @@ 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"
)
@ -31,7 +29,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(lm.FailedGetUsers, lm.Jellyfin, err)
app.err.Printf("Failed to get Jellyfin user (%d): %+v\n", status, err)
respond(500, "Failed to get user", gc)
return
}
@ -135,9 +133,8 @@ func (app *appContext) SetMyContactMethods(gc *gin.Context) {
func (app *appContext) LogoutUser(gc *gin.Context) {
cookie, err := gc.Cookie("user-refresh")
if err != nil {
msg := fmt.Sprintf(lm.FailedGetCookies, "user-refresh", err)
app.debug.Println(msg)
respond(500, msg, gc)
app.debug.Printf("Couldn't get cookies: %s", err)
respond(500, "Couldn't fetch cookies", gc)
return
}
app.invalidTokens = append(app.invalidTokens, cookie)
@ -177,21 +174,21 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
}
token, err := jwt.Parse(key, checkToken)
if err != nil {
app.err.Printf(lm.FailedParseJWT, err)
app.err.Printf("Failed to parse key: %s", err)
fail()
// respond(500, "unknownError", gc)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
app.err.Println(lm.FailedCastJWT)
app.err.Printf("Failed to parse key: %s", err)
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.Println(lm.InvalidJWT)
app.err.Printf("Invalid key")
fail()
// respond(400, "invalidKey", gc)
return
@ -215,7 +212,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
Time: time.Now(),
}, gc, true)
app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId"))
app.info.Println("Email list modified")
gc.Redirect(http.StatusSeeOther, "/my/account")
return
}
@ -234,6 +231,7 @@ 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
@ -253,7 +251,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
if err != nil {
app.err.Printf(lm.FailedSignJWT, err)
app.err.Printf("Failed to generate confirmation token: %v", err)
respond(500, "errorUnknown", gc)
return
}
@ -264,15 +262,15 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
if status == 200 && err == nil {
name = user.Name
}
app.debug.Printf(lm.EmailConfirmationRequired, id)
app.debug.Printf("%s: Email confirmation required", id)
respond(401, "confirmEmail", gc)
msg, err := app.email.constructConfirmation("", name, key, app, false)
if err != nil {
app.err.Printf(lm.FailedConstructConfirmationEmail, id, err)
app.err.Printf("%s: Failed to construct confirmation email: %v", name, err)
} else if err := app.email.send(msg, req.Email); err != nil {
app.err.Printf(lm.FailedSendConfirmationEmail, id, req.Email, err)
app.err.Printf("%s: Failed to send user confirmation email: %v", name, err)
} else {
app.err.Printf(lm.SentConfirmationEmail, id, req.Email)
app.info.Printf("%s: Sent user confirmation email to \"%s\"", name, req.Email)
}
return
}
@ -292,7 +290,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
// @Security Bearer
// @tags User Page
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
if app.discord.InviteChannel.Name == "" {
if app.discord.inviteChannelName == "" {
respondBool(400, false, gc)
return
}
@ -340,7 +338,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.DeleteVerifiedToken(pin)
app.discord.DeleteVerifiedUser(pin)
if !ok {
respondBool(200, false, gc)
return
@ -360,7 +358,7 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
jellyseerr.FieldDiscord: dcUser.ID,
jellyseerr.FieldDiscordEnabled: dcUser.Contact,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
@ -415,7 +413,7 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
jellyseerr.FieldTelegram: tgUser.ChatID,
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
@ -479,12 +477,12 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
pin := gc.Param("pin")
user, ok := app.matrix.tokens[pin]
if !ok {
app.debug.Printf(lm.InvalidPIN, pin)
app.debug.Println("Matrix: PIN not found")
respondBool(200, false, gc)
return
}
if user.User.UserID != userID {
app.debug.Printf(lm.UnauthorizedPIN, pin)
app.debug.Println("Matrix: User ID of PIN didn't match")
respondBool(200, false, gc)
return
}
@ -525,7 +523,7 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
jellyseerr.FieldDiscordEnabled: false,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
@ -553,7 +551,7 @@ func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
jellyseerr.FieldTelegramEnabled: false,
}); err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
app.storage.SetActivityKey(shortuuid.New(), Activity{
@ -608,6 +606,7 @@ 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
@ -617,7 +616,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
jfUser, ok := app.ReverseUserSearch(address, usernameAllowed, emailAllowed, contactMethodAllowed)
if !ok {
app.debug.Printf(lm.FailedGetUsers, lm.Jellyfin, "no results")
app.debug.Printf("Ignoring PWR request: User not found")
for range timerWait {
respondBool(204, true, gc)
@ -627,7 +626,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
}
pwr, err = app.GenInternalReset(jfUser.ID)
if err != nil {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
app.err.Printf("Failed to get user from Jellyfin: %v", err)
for range timerWait {
respondBool(204, true, gc)
return
@ -648,16 +647,16 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
}, app, false,
)
if err != nil {
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
app.err.Printf("Failed to construct password reset message for \"%s\": %v", pwr.Username, err)
for range timerWait {
respondBool(204, true, gc)
return
}
return
} else if err := app.sendByID(msg, jfUser.ID); err != nil {
app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, "?", err)
app.err.Printf("Failed to send password reset message to \"%s\": %v", address, err)
} else {
app.info.Printf(lm.SentPWRMessage, pwr.Username, "?")
app.info.Printf("Sent password reset message to \"%s\"", address)
}
for range timerWait {
respondBool(204, true, gc)
@ -684,13 +683,14 @@ 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(lm.FailedGetUser, gc.GetString("jfId"), lm.Jellyfin, err)
app.err.Printf("Failed to change password: couldn't find user (%d): %+v", status, err)
respondBool(500, false, gc)
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(lm.FailedGetUser, user.Name, lm.Ombi, err)
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", user.Name, status, err)
return
}
ombiUser["password"] = req.New
status, err = app.ombi.ModifyUser(ombiUser)
if status != 200 || err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
return
}
app.debug.Printf(lm.ChangePassword, lm.Ombi, ombiUser["userName"])
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
}()
}
cookie, err := gc.Cookie("user-refresh")
@ -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(lm.FailedGetCookies, "user-refresh", err)
app.debug.Printf("Couldn't get cookies: %s", 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(lm.FailedGetReferralTemplate, user.ReferralTemplateKey, err)
app.debug.Printf("Ignoring referral request, couldn't find template.")
respondBool(400, false, gc)
return
}
@ -782,7 +782,6 @@ 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 = ""
@ -792,7 +791,6 @@ 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)

File diff suppressed because it is too large Load Diff

63
api.go
View File

@ -1,12 +1,10 @@
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"
@ -124,14 +122,14 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
}
}
if !valid || req.PIN == "" {
app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", lm.InvalidPassword)
app.info.Printf("%s: Password reset failed: Invalid password", req.PIN)
gc.JSON(400, validation)
return
}
isInternal := false
if captcha && !app.verifyCaptcha(req.PIN, req.PIN, req.CaptchaText, true) {
app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", lm.IncorrectCaptcha)
app.info.Printf("%s: PWR Failed: Captcha Incorrect", req.PIN)
respond(400, "errorCaptcha", gc)
return
}
@ -140,7 +138,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(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, reset.PIN))
app.info.Printf("Password reset failed: PIN \"%s\" has expired", reset.PIN)
respondBool(401, false, gc)
delete(app.internalPWRs, req.PIN)
return
@ -150,7 +148,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
status, err := app.jf.ResetPasswordAdmin(userID)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
app.err.Printf("Password Reset failed (%d): %v", status, err)
respondBool(status, false, gc)
return
}
@ -158,7 +156,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(lm.FailedChangePassword, lm.Jellyfin, userID, err)
app.err.Printf("Password Reset failed (%d): %v", status, err)
respondBool(status, false, gc)
return
}
@ -178,7 +176,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
user, status, err = app.jf.UserByName(username, false)
}
if status != 200 || err != nil {
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
app.err.Printf("Failed to get user \"%s\" (%d): %v", username, status, err)
respondBool(500, false, gc)
return
}
@ -197,33 +195,31 @@ 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(lm.FailedChangePassword, lm.Jellyfin, user.ID, err)
app.err.Printf("Failed to change password for \"%s\" (%d): %v", username, status, err)
respondBool(500, false, gc)
return
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
// This makes no sense so has been commented out.
// It probably did at some point in the past.
/* Silently fail for changing ombi passwords
// Silently fail for changing ombi passwords
if (status != 200 && status != 204) || err != nil {
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err)
app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err)
respondBool(200, true, gc)
return
} */
}
ombiUser, status, err := app.getOmbiUser(user.ID)
if status != 200 || err != nil {
app.err.Printf(lm.FailedGetUser, user.ID, lm.Ombi, err)
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", username, status, err)
respondBool(200, true, gc)
return
}
ombiUser["password"] = req.Password
status, err = app.ombi.ModifyUser(ombiUser)
if status != 200 || err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Ombi, user.ID, err)
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
respondBool(200, true, gc)
return
}
app.debug.Printf(lm.ChangePassword, lm.Ombi, user.ID)
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
}
respondBool(200, true, gc)
}
@ -235,6 +231,7 @@ 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()
@ -344,6 +341,7 @@ 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.
@ -368,18 +366,26 @@ 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(lm.FailedWriting, app.configPath, err)
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
respond(500, err.Error(), gc)
return
}
app.info.Printf(lm.ModifyConfig, app.configPath)
app.debug.Println("Config saved")
gc.JSON(200, map[string]bool{"success": true})
if req["restart-program"] != nil && req["restart-program"].(bool) {
app.Restart()
app.info.Println("Restarting...")
if TRAY {
TRAYRESTART <- true
} else {
RESTART <- true
}
// Safety Sleep (Ensure shutdown tasks get done)
time.Sleep(time.Second)
}
app.loadConfig()
// 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),
@ -419,13 +425,12 @@ func (app *appContext) CheckUpdate(gc *gin.Context) {
// @tags Configuration
func (app *appContext) ApplyUpdate(gc *gin.Context) {
if !app.update.CanUpdate {
app.info.Printf(lm.FailedApplyUpdate, lm.UpdateManual)
respond(400, lm.UpdateManual, gc)
respond(400, "Update is manual", gc)
return
}
err := app.update.update()
if err != nil {
app.err.Printf(lm.FailedApplyUpdate, err)
app.err.Printf("Failed to apply update: %v", err)
respondBool(500, false, gc)
return
}
@ -447,9 +452,8 @@ func (app *appContext) ApplyUpdate(gc *gin.Context) {
func (app *appContext) Logout(gc *gin.Context) {
cookie, err := gc.Cookie("refresh")
if err != nil {
msg := fmt.Sprintf(lm.FailedGetCookies, "refresh", err)
app.debug.Println(msg)
respond(500, msg, gc)
app.debug.Printf("Couldn't get cookies: %s", err)
respond(500, "Couldn't fetch cookies", gc)
return
}
app.invalidTokens = append(app.invalidTokens, cookie)
@ -522,7 +526,11 @@ func (app *appContext) ServeLang(gc *gin.Context) {
// @Security Bearer
// @tags Other
func (app *appContext) restart(gc *gin.Context) {
app.Restart()
app.info.Println("Restarting...")
err := app.Restart()
if err != nil {
app.err.Printf("Couldn't restart, try restarting manually: %v", err)
}
}
// @Summary Returns the last 100 lines of the log.
@ -536,7 +544,6 @@ 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
View File

@ -9,7 +9,6 @@ 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"
)
@ -42,8 +41,6 @@ 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
@ -75,26 +72,32 @@ 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.authLog(lm.InvalidAuthHeader)
app.debug.Println("Invalid authorization header")
respond(401, "Unauthorized", gc)
return
}
token, err := jwt.Parse(string(header[1]), checkToken)
if err != nil {
app.authLog(fmt.Sprintf(lm.FailedParseJWT, err))
app.debug.Printf("Auth denied: %s", err)
respond(401, "Unauthorized", gc)
return
}
claims, ok = token.Claims.(jwt.MapClaims)
if !ok {
app.authLog(lm.FailedCastJWT)
app.debug.Println("Invalid JWT")
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.authLog(lm.InvalidJWT)
app.debug.Printf("Auth denied: Invalid token")
// app.debug.Printf("Expiry: %+v, OK: %t, Valid: %t, ClaimType: %s\n", expiry, ok, token.Valid, claims["type"].(string))
respond(401, "Unauthorized", gc)
ok = false
@ -112,7 +115,7 @@ func (app *appContext) authenticate(gc *gin.Context) {
}
isAdminToken := claims["admin"].(bool)
if !isAdminToken {
app.authLog(lm.NonAdminToken)
app.debug.Printf("Auth denied: Token was not for admin access")
respond(401, "Unauthorized", gc)
return
}
@ -127,13 +130,14 @@ func (app *appContext) authenticate(gc *gin.Context) {
}
}
if !match {
app.authLog(fmt.Sprintf(lm.NonAdminUser, userID))
app.debug.Printf("Couldn't find user ID \"%s\"", 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()
}
@ -156,7 +160,7 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context, userpage bool)
password = creds[1]
ok = false
if username == "" || password == "" {
app.logIpDebug(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.EmptyUserOrPass))
app.logIpDebug(gc, userpage, "Auth denied: blank username/password")
respond(401, "Unauthorized", gc)
return
}
@ -169,16 +173,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, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
app.logIpInfo(gc, userpage, "Auth denied: Invalid username/password (Jellyfin)")
respond(401, "Unauthorized", gc)
return
}
if status == 403 {
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.UserDisabled))
app.logIpInfo(gc, userpage, "Auth denied: Jellyfin account disabled")
respond(403, "yourAccountWasDisabled", gc)
return
}
app.authLog(fmt.Sprintf(lm.FailedAuthJellyfin, app.jf.Server, status, err))
app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
respond(500, "Jellyfin error", gc)
return
}
@ -195,7 +199,7 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
// @tags Auth
// @Security getTokenAuth
func (app *appContext) getTokenLogin(gc *gin.Context) {
app.logIpInfo(gc, false, fmt.Sprintf(lm.RequestingToken, lm.TokenLoginAttempt))
app.logIpInfo(gc, false, "Token requested (login attempt)")
username, password, ok := app.decodeValidateLoginHeader(gc, false)
if !ok {
return
@ -205,12 +209,13 @@ 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, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
app.logIpInfo(gc, false, "Auth denied: Invalid username/password")
respond(401, "Unauthorized", gc)
return
}
@ -228,7 +233,7 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
}
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
if !accountsAdmin {
app.authLog(fmt.Sprintf(lm.NonAdminUser, username))
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", username)
respond(401, "Unauthorized", gc)
return
}
@ -238,12 +243,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
newUser := User{
UserID: userID,
}
app.debug.Printf(lm.GenerateToken, username)
app.debug.Printf("Token generated for user \"%s\"", username)
app.adminUsers = append(app.adminUsers, newUser)
}
token, refresh, err := CreateToken(userID, jfID, true)
if err != nil {
app.err.Printf(lm.FailedGenerateToken, err)
app.err.Printf("getToken failed: Couldn't generate token (%s)", err)
respond(500, "Couldn't generate token", gc)
return
}
@ -256,29 +261,35 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
ok = false
cookie, err := gc.Cookie(cookieName)
if err != nil || cookie == "" {
app.authLog(fmt.Sprintf(lm.FailedGetCookies, cookieName, err))
app.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err)
respond(400, "Couldn't get token", gc)
return
}
for _, token := range app.invalidTokens {
if cookie == token {
app.authLog(lm.LocallyInvalidatedJWT)
respond(401, lm.InvalidJWT, gc)
app.debug.Println("getTokenRefresh: Invalid token")
respond(401, "Invalid token", gc)
return
}
}
token, err := jwt.Parse(cookie, checkToken)
if err != nil {
app.authLog(lm.FailedParseJWT)
respond(400, lm.InvalidJWT, gc)
app.debug.Println("getTokenRefresh: Invalid token")
respond(400, "Invalid token", 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.authLog(lm.InvalidJWT)
respond(401, lm.InvalidJWT, gc)
app.debug.Printf("getTokenRefresh: Invalid token: %+v", err)
respond(401, "Invalid token", gc)
ok = false
return
}
@ -293,7 +304,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, fmt.Sprintf(lm.RequestingToken, lm.TokenRefresh))
app.logIpInfo(gc, false, "Token requested (refresh token)")
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
if !ok {
return
@ -302,7 +313,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(lm.FailedGenerateToken, err)
app.err.Printf("getTokenRefresh failed: Couldn't generate token (%s)", err)
respond(500, "Couldn't generate token", gc)
return
}

View File

@ -7,8 +7,6 @@ import (
"sort"
"strings"
"time"
lm "github.com/hrfee/jfa-go/logmessages"
)
const (
@ -62,12 +60,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(lm.FailedCreateDir, path, err)
app.err.Printf("Failed to create backup directory \"%s\": %v\n", path, err)
return nil
}
items, err := os.ReadDir(path)
if err != nil {
app.err.Printf(lm.FailedReading, path, err)
app.err.Printf("Failed to read backup directory \"%s\": %v\n", path, err)
return nil
}
backups := &BackupList{}
@ -80,7 +78,7 @@ func (app *appContext) getBackups() *BackupList {
}
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(item.Name(), BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX))
if err != nil {
app.debug.Printf(lm.FailedParseTime, err)
app.debug.Printf("Failed to parse backup filename \"%s\": %v\n", item.Name(), err)
continue
}
backups.dates[i] = t
@ -103,36 +101,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(lm.FailedDeleteOldBackup, fullpath, err)
app.err.Printf("Failed to delete old backup \"%s\": %v\n", 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(lm.FailedOpen, fullpath, err)
app.err.Printf("Failed to open backup file \"%s\": %v\n", fullpath, err)
return
}
defer f.Close()
_, err = app.storage.db.Badger().Backup(f, 0)
if err != nil {
app.err.Printf(lm.FailedCreateBackup, err)
app.err.Printf("Failed to create backup: %v\n", err)
return
}
fstat, err := f.Stat()
if err != nil {
app.err.Printf(lm.FailedStat, fullpath, err)
app.err.Printf("Failed to get info on new backup: %v\n", err)
return
}
fileDetails.Size = fileSize(fstat.Size())
fileDetails.Name = fname
fileDetails.Path = fullpath
app.debug.Printf(lm.CreateBackup, fileDetails)
// fmt.Printf("Created backup %+v\n", fileDetails)
return
}
@ -141,25 +139,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(lm.FailedMoveOldDB, oldPath, err)
app.err.Fatalf("Failed to move existing database: %v\n", 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(lm.FailedOpen, LOADBAK, err)
app.err.Fatalf("Failed to open backup file \"%s\": %v\n", LOADBAK, err)
}
err = app.storage.db.Badger().Load(f, 256)
f.Close()
if err != nil {
app.err.Fatalf(lm.FailedRestoreDB, LOADBAK, err)
app.err.Fatalf("Failed to restore backup file \"%s\": %v\n", LOADBAK, err)
}
app.info.Printf(lm.RestoreDB, LOADBAK)
app.info.Printf("Restored backup \"%s\".", LOADBAK)
LOADBAK = ""
}
@ -167,6 +165,7 @@ 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()
},
)

View File

@ -7,10 +7,8 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
"github.com/hrfee/jfa-go/easyproxy"
lm "github.com/hrfee/jfa-go/logmessages"
"gopkg.in/ini.v1"
)
@ -142,7 +140,7 @@ func (app *appContext) loadConfig() error {
}
}
if allDisabled {
app.info.Println(lm.EnableAllPWRMethods)
fmt.Println("SETALLTRUE")
for _, v := range pwrMethods {
app.config.Section("user_page").Key(v).SetValue("true")
}
@ -177,15 +175,9 @@ func (app *appContext) loadConfig() error {
app.proxyConfig.Password = app.config.Section("advanced").Key("proxy_password").MustString("")
app.proxyTransport, err = easyproxy.NewTransport(app.proxyConfig)
if err != nil {
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.err.Printf("Failed to initialize Proxy: %v\n", err)
}
app.proxyEnabled = true
}
app.MustSetValue("updates", "enabled", "true")

View File

@ -1120,15 +1120,6 @@
"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,

View File

@ -4,7 +4,6 @@ import (
"time"
"github.com/dgraph-io/badger/v3"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser"
"github.com/timshannon/badgerhold/v4"
)
@ -13,7 +12,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(lm.HousekeepingEmail)
app.debug.Println("Housekeeping: removing unused email addresses")
emails := app.storage.GetEmails()
for _, email := range emails {
_, _, err := app.jf.UserByID(email.JellyfinID, false)
@ -29,20 +28,15 @@ func (app *appContext) clearEmails() {
// clearDiscord does the same as clearEmails, but for Discord Users.
func (app *appContext) clearDiscord() {
app.debug.Println(lm.HousekeepingDiscord)
app.debug.Println("Housekeeping: removing unused Discord IDs")
discordUsers := app.storage.GetDiscord()
for _, discordUser := range discordUsers {
user, _, err := app.jf.UserByID(discordUser.JellyfinID, false)
_, _, err := app.jf.UserByID(discordUser.JellyfinID, false)
// Make sure the user doesn't exist, and no other error has occured
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
}
}
@ -50,7 +44,7 @@ func (app *appContext) clearDiscord() {
// clearMatrix does the same as clearEmails, but for Matrix Users.
func (app *appContext) clearMatrix() {
app.debug.Println(lm.HousekeepingMatrix)
app.debug.Println("Housekeeping: removing unused Matrix IDs")
matrixUsers := app.storage.GetMatrix()
for _, matrixUser := range matrixUsers {
_, _, err := app.jf.UserByID(matrixUser.JellyfinID, false)
@ -66,7 +60,7 @@ func (app *appContext) clearMatrix() {
// clearTelegram does the same as clearEmails, but for Telegram Users.
func (app *appContext) clearTelegram() {
app.debug.Println(lm.HousekeepingTelegram)
app.debug.Println("Housekeeping: removing unused Telegram IDs")
telegramUsers := app.storage.GetTelegram()
for _, telegramUser := range telegramUsers {
_, _, err := app.jf.UserByID(telegramUser.JellyfinID, false)
@ -81,7 +75,7 @@ func (app *appContext) clearTelegram() {
}
func (app *appContext) clearPWRCaptchas() {
app.debug.Println(lm.HousekeepingCaptcha)
app.debug.Println("Housekeeping: Clearing old PWR Captchas")
captchas := map[string]Captcha{}
for k, capt := range app.pwrCaptchas {
if capt.Generated.Add(CAPTCHA_VALIDITY * time.Second).After(time.Now()) {
@ -92,7 +86,7 @@ func (app *appContext) clearPWRCaptchas() {
}
func (app *appContext) clearActivities() {
app.debug.Println(lm.HousekeepingActivity)
app.debug.Println("Housekeeping: Cleaning up Activity log...")
keepCount := app.config.Section("activity_log").Key("keep_n_records").MustInt(1000)
maxAgeDays := app.config.Section("activity_log").Key("delete_after_days").MustInt(90)
minAge := time.Now().AddDate(0, 0, -maxAgeDays)
@ -109,7 +103,7 @@ func (app *appContext) clearActivities() {
}
}
if err == badger.ErrTxnTooBig {
app.debug.Printf(lm.ActivityLogTxnTooBig)
app.debug.Printf("Activities: Delete txn was too big, doing it manually.")
list := []Activity{}
if errorSource == 0 {
app.storage.db.Find(&list, badgerhold.Where("Time").Lt(minAge))
@ -125,7 +119,7 @@ func (app *appContext) clearActivities() {
func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon {
d := NewGenericDaemon(interval, app,
func(app *appContext) {
app.debug.Println(lm.HousekeepingInvites)
app.debug.Println("Housekeeping: Checking for expired invites")
app.checkInvites()
},
func(app *appContext) { app.clearActivities() },
@ -134,7 +128,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) || app.config.Section("discord").Key("disable_enable_role").MustBool(false)
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
clearTelegram := app.config.Section("telegram").Key("require_unique").MustBool(false)
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)

View File

@ -6,26 +6,25 @@ import (
"time"
dg "github.com/bwmarrin/discordgo"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/timshannon/badgerhold/v4"
)
type DiscordDaemon struct {
Stopped bool
ShutdownChannel chan string
bot *dg.Session
username string
tokens map[string]VerifToken // Map of pins to tokens.
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
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.
roleID string
app *appContext
commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string)
commandIDs []string
commandDescriptions []*dg.ApplicationCommand
Stopped bool
ShutdownChannel chan string
bot *dg.Session
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
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.
roleID string
app *appContext
commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string)
commandIDs []string
commandDescriptions []*dg.ApplicationCommand
}
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
@ -93,11 +92,13 @@ 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(lm.FailedStartDaemon, lm.Discord, err)
d.app.err.Printf("Discord: Failed to start daemon: %v", err)
return
}
// Wait for everything to populate, it's slow sometimes.
@ -115,17 +116,17 @@ func (d *DiscordDaemon) run() {
d.guildID = d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID
guild, err := d.bot.Guild(d.guildID)
if err != nil {
d.app.err.Printf(lm.FailedGetDiscordGuild, err)
d.app.err.Printf("Discord: Failed to get guild: %v", err)
}
d.serverChannelName = guild.Name
d.serverName = guild.Name
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
d.Channel.Name = channel
d.channelName = 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.InviteChannel.Name = invChannel
d.inviteChannelName = invChannel
}
}
err = d.bot.UpdateGameStatus(0, "/"+d.app.config.Section("discord").Key("start_command").MustString("start"))
@ -144,7 +145,7 @@ func (d *DiscordDaemon) ListRoles() (roles [][2]string, err error) {
var r []*dg.Role
r, err = d.bot.GuildRoles(d.guildID)
if err != nil {
d.app.err.Printf(lm.FailedGetDiscordRoles, err)
d.app.err.Printf("Discord: Failed to get roles: %v", err)
return
}
for _, role := range r {
@ -167,62 +168,44 @@ 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.InviteChannel.Name == "" {
d.app.err.Println(lm.FailedCreateDiscordInviteChannel, lm.InviteChannelEmpty)
if d.inviteChannelName == "" {
d.app.err.Println("Discord: Cannot create invite without channel specified in settings.")
return
}
if d.InviteChannel.ID == "" {
if d.inviteChannelID == "" {
channels, err := d.bot.GuildChannels(d.guildID)
if err != nil {
d.app.err.Printf(lm.FailedGetDiscordChannels, err)
d.app.err.Printf("Discord: Couldn't get channel list: %v", err)
return
}
found := false
for _, channel := range channels {
// channel, err := d.bot.Channel(ch.ID)
// if err != nil {
// d.app.err.Printf(lm.FailedGetDiscordChannel, ch.ID, err)
// d.app.err.Printf("Discord: Couldn't get channel: %v", err)
// return
// }
if channel.Name == d.InviteChannel.Name {
d.InviteChannel.ID = channel.ID
if channel.Name == d.inviteChannelName {
d.inviteChannelID = channel.ID
found = true
break
}
}
if !found {
d.app.err.Printf(lm.FailedGetDiscordChannel, d.InviteChannel.Name, lm.NotFound)
d.app.err.Printf("Discord: Couldn't find invite channel \"%s\"", d.inviteChannelName)
return
}
}
// channel, err := d.bot.Channel(d.inviteChannelID)
// if err != nil {
// d.app.err.Printf(lm.FailedGetDiscordChannel, d.inviteChannelID, err)
// d.app.err.Printf("Discord: Couldn't get invite channel: %v", err)
// return
// }
inv, err = d.bot.ChannelInviteCreate(d.InviteChannel.ID, dg.Invite{
inv, err = d.bot.ChannelInviteCreate(d.inviteChannelID, dg.Invite{
// Guild: d.bot.State.Guilds[len(d.bot.State.Guilds)-1],
// Channel: channel,
// Inviter: d.bot.State.User,
@ -231,13 +214,13 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
Temporary: false,
})
if err != nil {
d.app.err.Printf(lm.FailedGenerateDiscordInvite, err)
d.app.err.Printf("Discord: Failed to create invite: %v", err)
return
}
inviteURL = "https://discord.gg/" + inv.Code
guild, err := d.bot.Guild(d.guildID)
if err != nil {
d.app.err.Printf(lm.FailedGetDiscordGuild, err)
d.app.err.Printf("Discord: Failed to get guild: %v", err)
return
}
iconURL = guild.IconURL("256")
@ -272,7 +255,7 @@ func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
1000,
)
if err != nil {
d.app.err.Printf(lm.FailedGetDiscordGuildMembers, err)
d.app.err.Printf("Discord: Failed to get members: %v", err)
return nil
}
hasDiscriminator := strings.Contains(username, "#")
@ -302,7 +285,7 @@ func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
u, err := d.bot.User(ID)
if err != nil {
d.app.err.Printf(lm.FailedGetUser, ID, lm.Discord, err)
d.app.err.Printf("Discord: Failed to get user: %v", err)
return
}
user.ID = ID
@ -311,7 +294,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(lm.FailedCreateDiscordDMChannel, ID, err)
d.app.err.Printf("Discord: Failed to create DM channel: %v", err)
return
}
user.ChannelID = channel.ID
@ -398,7 +381,7 @@ func (d *DiscordDaemon) registerCommands() {
d.commandDescriptions[1].Options[0].Choices = make([]*dg.ApplicationCommandOptionChoice, len(d.app.storage.lang.Telegram))
i := 0
for code := range d.app.storage.lang.Telegram {
d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Lang, d.app.storage.lang.Telegram[code].Meta.Name+":"+code)
d.app.debug.Printf("Discord: registering lang choice \"%s\":\"%s\"\n", d.app.storage.lang.Telegram[code].Meta.Name, code)
d.commandDescriptions[1].Options[0].Choices[i] = &dg.ApplicationCommandOptionChoice{
Name: d.app.storage.lang.Telegram[code].Meta.Name,
Value: code,
@ -409,7 +392,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(lm.RegisterDiscordChoice, lm.Profile, profile.Name)
d.app.debug.Printf("Discord: registering profile choice \"%s\"", profile.Name)
d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{
Name: profile.Name,
Value: profile.Name,
@ -426,9 +409,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(lm.FailedRegisterDiscordCommand, cmd.Name, err)
d.app.err.Printf("Discord: Cannot create command \"%s\": %v", cmd.Name, err)
} else {
d.app.debug.Printf(lm.RegisterDiscordCommand, cmd.Name)
d.app.debug.Printf("Discord: registered command \"%s\"", cmd.Name)
d.commandIDs[i] = command.ID
}
}
@ -437,12 +420,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(lm.FailedGetDiscordCommands, err)
d.app.err.Printf("Discord: Failed to get commands: %v", 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(lm.FailedDeregDiscordCommand, cmd.Name, err)
d.app.err.Printf("Discord: Failed to deregister command: %v", err)
}
}
}
@ -453,7 +436,7 @@ func (d *DiscordDaemon) UpdateCommands() {
profiles := d.app.storage.GetProfiles()
d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(profiles))
for i, profile := range profiles {
d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Profile, profile.Name)
d.app.debug.Printf("Discord: registering profile choice \"%s\"", profile.Name)
d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{
Name: profile.Name,
Value: profile.Name,
@ -461,7 +444,7 @@ func (d *DiscordDaemon) UpdateCommands() {
}
cmd, err := d.bot.ApplicationCommandEdit(d.bot.State.User.ID, d.guildID, d.commandIDs[3], d.commandDescriptions[3])
if err != nil {
d.app.err.Printf(lm.FailedRegisterDiscordChoices, lm.Profile, err)
d.app.err.Printf("Discord: Failed to update profile list: %v\n", err)
} else {
d.commandIDs[3] = cmd.ID
}
@ -469,20 +452,19 @@ 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.Channel.Name != "" {
if d.Channel.ID == "" {
if i.GuildID != "" && d.channelName != "" {
if d.channelID == "" {
channel, err := s.Channel(i.ChannelID)
if err != nil {
d.app.err.Printf(lm.FailedGetDiscordChannel, i.ChannelID, err)
d.app.err.Println(lm.MonitorAllDiscordChannels)
d.Channel.Name = ""
d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
d.channelName = ""
}
if channel.Name == d.Channel.Name {
d.Channel.ID = channel.ID
if channel.Name == d.channelName {
d.channelID = channel.ID
}
}
if d.Channel.ID != i.ChannelID {
d.app.debug.Printf(lm.IgnoreOutOfChannelMessage, lm.Discord)
if d.channelID != i.ChannelID {
d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
return
}
}
@ -504,7 +486,7 @@ func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang string) {
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
if err != nil {
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, i.Interaction.Member.User.ID, err)
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", i.Interaction.Member.User.Username, err)
return
}
user := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username)
@ -521,7 +503,7 @@ func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang st
},
})
if err != nil {
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
d.app.err.Printf("Discord: Failed to send reply: %v", err)
return
}
}
@ -539,7 +521,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
},
})
if err != nil {
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
}
delete(d.tokens, pin)
return
@ -553,7 +535,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
},
})
if err != nil {
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
}
dcUser := d.users[i.Interaction.Member.User.ID]
dcUser.JellyfinID = user.JellyfinID
@ -584,7 +566,7 @@ func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang str
},
})
if err != nil {
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
d.app.err.Printf("Discord: Failed to send reply: %v", err)
return
}
}
@ -593,7 +575,7 @@ func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang str
func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang string) {
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
if err != nil {
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, i.Interaction.Member.User.ID, err)
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", i.Interaction.Member.User.Username, err)
return
}
requester := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username)
@ -608,9 +590,12 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
//}
// Check whether requestor is linked to the admin account
requesterEmail, ok := d.app.storage.GetEmailsKey(requester.JellyfinID)
if !(ok && requesterEmail.Admin) {
d.app.err.Printf(lm.FailedGenerateInvite, fmt.Sprintf(lm.NonAdminUser, requester.JellyfinID))
// FIXME: add response message
if !ok {
d.app.err.Printf("Failed to verify admin")
}
if !requesterEmail.Admin {
d.app.err.Printf("User is not admin")
//add response message
return
}
@ -644,7 +629,7 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
ValidTill: validTill,
UserLabel: userLabel,
Profile: "Default",
Label: fmt.Sprintf("%s: %s", lm.Discord, RenderDiscordUsername(recipient)),
Label: fmt.Sprintf("Discord: %s", RenderDiscordUsername(recipient)),
}
if profileName != "" {
if _, ok := d.app.storage.GetProfileKey(profileName); ok {
@ -653,12 +638,13 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
}
if recipient != nil && d.app.config.Section("invite_emails").Key("enabled").MustBool(false) {
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(lm.FailedConstructInviteMessage, invite.Code, err)
d.app.err.Println(invite.SendTo)
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)
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
Type: dg.InteractionResponseChannelMessageWithSource,
Data: &dg.InteractionResponseData{
@ -667,14 +653,14 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
},
})
if err != nil {
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err)
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
}
} else {
var err error
err = d.app.discord.SendDM(msg, recipient.ID)
if err != nil {
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, RenderDiscordUsername(recipient), err)
d.app.err.Println(invite.SendTo)
invite.SendTo = fmt.Sprintf("Failed to send to %s", RenderDiscordUsername(recipient))
d.app.err.Printf("%s: %s: %v", invite.Code, invite.SendTo, err)
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
Type: dg.InteractionResponseChannelMessageWithSource,
Data: &dg.InteractionResponseData{
@ -683,10 +669,10 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
},
})
if err != nil {
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err)
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
}
} else {
d.app.info.Printf(lm.SentInviteMessage, invite.Code, RenderDiscordUsername(recipient))
d.app.info.Printf("%s: Sent invite email to \"%s\"", invite.Code, RenderDiscordUsername(recipient))
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
Type: dg.InteractionResponseChannelMessageWithSource,
Data: &dg.InteractionResponseData{
@ -695,7 +681,7 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
},
})
if err != nil {
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err)
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
}
}
}
@ -704,6 +690,140 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
d.app.storage.SetInvitesKey(invite.Code, invite)
}
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 {
@ -757,10 +877,10 @@ func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
}
// UserVerified returns whether or not a token with the given PIN has been verified, and the user itself.
func (d *DiscordDaemon) UserVerified(pin string) (ContactMethodUser, bool) {
u, ok := d.verifiedTokens[pin]
func (d *DiscordDaemon) UserVerified(pin string) (user DiscordUser, ok bool) {
user, ok = d.verifiedTokens[pin]
// delete(d.verifiedTokens, pin)
return &u, ok
return
}
// AssignedUserVerified returns whether or not a user with the given PIN has been verified, and the token itself.
@ -780,44 +900,7 @@ func (d *DiscordDaemon) UserExists(id string) bool {
return err != nil || c > 0
}
// 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)
// DeleteVerifiedUser removes the token with the given PIN.
func (d *DiscordDaemon) DeleteVerifiedUser(pin string) {
delete(d.verifiedTokens, pin)
}

View File

@ -20,7 +20,6 @@ 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"
@ -96,7 +95,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(lm.FailedInitSMTP, err)
app.err.Printf("Error while initiating SMTP mailer: %v", err)
}
} else if method == "mailgun" {
emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String())
@ -581,7 +580,7 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo
// Only used in html email.
template["pin_code"] = pwr.Pin
} else {
app.info.Println(lm.FailedGeneratePWRLink, err)
app.info.Println("Couldn't generate PWR link: %v", err)
template["pin"] = pwr.Pin
}
} else {

View File

@ -1,10 +1,6 @@
package main
import (
"time"
lm "github.com/hrfee/jfa-go/logmessages"
)
import "time"
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
@ -40,7 +36,7 @@ func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app
func (d *GenericDaemon) Name(name string) { d.name = name }
func (d *GenericDaemon) run() {
d.app.info.Printf(lm.StartDaemon, d.name)
d.app.info.Printf("%s started", d.name)
for {
select {
case <-d.ShutdownChannel:

7
go.mod
View File

@ -1,6 +1,6 @@
module github.com/hrfee/jfa-go
go 1.22.4
go 1.20
replace github.com/hrfee/jfa-go/docs => ./docs
@ -10,8 +10,6 @@ replace github.com/hrfee/jfa-go/ombi => ./ombi
replace github.com/hrfee/jfa-go/logger => ./logger
replace github.com/hrfee/jfa-go/logmessages => ./logmessages
replace github.com/hrfee/jfa-go/linecache => ./linecache
replace github.com/hrfee/jfa-go/api => ./api
@ -37,7 +35,7 @@ require (
github.com/hrfee/jfa-go/docs v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/jfa-go/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-20240731152135-2d066ea7cd32
github.com/hrfee/jfa-go/logger v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/jfa-go/ombi v0.0.0-20230626224816-f72960635dc3
github.com/hrfee/mediabrowser v0.3.13
github.com/itchyny/timefmt-go v0.1.5
@ -93,7 +91,6 @@ 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

View File

@ -246,7 +246,6 @@
<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>
@ -259,23 +258,6 @@
<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>
@ -441,7 +423,6 @@
<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>
@ -560,3 +541,4 @@
<script src="js/setup.js" type="module"></script>
</body>
</html>

0
images/jfa-go-icon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

0
images/jfa-go-icon.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -5,21 +5,20 @@ 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(lm.FailedImportUser, lm.Jellyseerr, jfID, err)
app.debug.Printf("Failed to get or trigger import for Jellyseerr (user \"%s\"): %v", jfID, err)
return
}
if imported {
app.debug.Printf(lm.ImportJellyseerrUser, jfID, user.ID)
app.debug.Printf("Jellyseerr: Triggered import for Jellyfin user \"%s\" (ID %d)", jfID, user.ID)
}
notif, err := app.js.GetNotificationPreferencesByID(user.ID)
if err != nil {
app.debug.Printf(lm.FailedGetJellyseerrNotificationPrefs, jfID, err)
app.debug.Printf("Failed to get notification prefs for Jellyseerr (user \"%s\"): %v", jfID, err)
return
}
@ -28,7 +27,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(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err)
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
} else {
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
}
@ -52,7 +51,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
if len(contactMethods) != 0 {
err := app.js.ModifyNotifications(jfID, contactMethods)
if err != nil {
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
}
}
}
@ -60,7 +59,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
func (app *appContext) SynchronizeJellyseerrUsers() {
users, status, err := app.jf.GetUsers(false)
if err != nil || status != 200 {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
app.err.Printf("Failed to get users (%d): %s", status, err)
return
}
// I'm sure Jellyseerr can handle it,

View File

@ -122,7 +122,6 @@ 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"`

View File

@ -1,88 +0,0 @@
{
"meta": {
"name": "کوردی سۆرانی"
},
"strings": {
"pageTitle": "دروستکردنی هەژماری جێڵیفن",
"createAccountHeader": "دروستکردنی هەژمار",
"accountDetails": "زانیارییەکان",
"emailAddress": "ئیمەیل",
"username": "ناوی بەکارهێنەر",
"oldPassword": "وشەی نهێنی کۆن",
"newPassword": "وشەی نهێنی نوێ",
"password": "وشەی نهێنی",
"reEnterPassword": "دووبارە وشەی نهێنی بنووسەوە",
"reEnterPasswordInvalid": "وشە نهێنییەکان یەک ناگرن.",
"createAccountButton": "هەژمار دروستبکە",
"passwordRequirementsHeader": "داواکارییەکانی وشەی نهێنی",
"successHeader": "سەرکەوت!",
"confirmationRequired": "دووپاتکردنەوەی ئیمەیل داواکراوە",
"confirmationRequiredMessage": "تکایە سەیری نامەکانی ئیمەیلەکەت بکە بۆ دووپاتکردنەوەی ناونیشانەکەت.",
"yourAccountIsValidUntil": "هەژمارەکەت تاکو {date} کاردەکات.",
"sendPIN": "ئەم ژمارە نهێنییەی خوارەوە بۆ بۆتەکە بنێرە، پاشان وەرەوە بۆ پەیوەستکردنی هەژمارەکەت.",
"sendPINDiscord": "{command} لە چەناڵی {server_channel}ی دیسکۆردەکەت بنوسە، پاشان ئەم ژمارە نهێنییەی خوارەوە بنێرە.",
"matrixEnterUser": "",
"welcomeUser": "{user}، بەخێربێیت!",
"addContactMethod": "",
"editContactMethod": "",
"joinTheServer": "",
"customMessagePlaceholderHeader": "",
"customMessagePlaceholderContent": "",
"userPageSuccessMessage": "",
"resetPassword": "",
"resetPasswordThroughJellyfin": "",
"resetPasswordThroughLink": "",
"resetPasswordThroughLinkStart": "",
"resetPasswordThroughLinkEnd": "",
"resetPasswordUsername": "",
"resetPasswordEmail": "",
"resetPasswordContactMethod": "",
"resetSent": "",
"resetSentDescription": "",
"changePassword": "",
"referralsDescription": "",
"referralsWithExpiryDescription": "",
"copyReferral": "",
"invitedBy": ""
},
"notifications": {
"errorUserExists": "",
"errorInvalidCode": "",
"errorAccountLinked": "",
"errorEmailLinked": "",
"errorTelegramVerification": "",
"errorDiscordVerification": "",
"errorMatrixVerification": "",
"errorInvalidPIN": "",
"errorUnknown": "",
"errorNoEmail": "",
"errorCaptcha": "",
"errorPassword": "",
"errorNoMatch": "",
"errorOldPassword": "",
"passwordChanged": "",
"verified": ""
},
"validationStrings": {
"length": {
"singular": "",
"plural": ""
},
"uppercase": {
"singular": "",
"plural": ""
},
"lowercase": {
"singular": "",
"plural": ""
},
"number": {
"singular": "",
"plural": ""
},
"special": {
"singular": "",
"plural": ""
}
}
}

View File

@ -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 PIN unten an den Bot und komm dann hierher zurück, um dein Konto zu verknüpfen.",
"sendPIN": "Sende die untenstehende PIN an den Bot und komm dann hierher zurück, um dein Konto zu verbinden.",
"sendPINDiscord": "Gib auf Discord {command} in {server_channel} ein und sende die untenstehende PIN.",
"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,20 +30,7 @@
"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\".",
"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."
"resetPasswordThroughJellyfin": "Um das Passwort zurückzusetzen, besuche {jfLink} und drücke auf die Schaltfläche \"Passwort vergessen\"."
},
"validationStrings": {
"length": {
@ -80,10 +67,6 @@
"errorNoEmail": "E-Mail Adresse erforderlich.",
"errorCaptcha": "Captcha falsch.",
"errorPassword": "Prüfe die Passwortanforderungen.",
"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."
"errorNoMatch": "Passwörter stimmen nicht überein."
}
}

View File

@ -47,7 +47,7 @@
"title": "General",
"listenAddress": "Listen Address",
"urlBase": "URL Base",
"urlBaseNotice": "Only needed if using a reverse proxy on a subfolder (e.g 'jellyf.in/accounts').",
"urlBaseNotice": "Only needed if using a reverse proxy on a subdomain (e.g 'jellyf.in/accounts').",
"lightTheme": "Light",
"darkTheme": "Dark",
"useHTTPS": "Use HTTPS",
@ -94,13 +94,7 @@
"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.",
"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."
"apiKeyNotice": "Find this in the first tab of Ombi settings."
},
"messages": {
"title": "Messages",
@ -140,7 +134,6 @@
"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",

View File

@ -1,3 +0,0 @@
module github.com/hrfee/logmessages
go 1.22.4

View File

@ -1,371 +0,0 @@
package logmessages
/* Log strings for (almost) all the program.
* Helps avoid writing redundant, slightly different
* strings constantly.
* Also would help if I were to ever set up translation
* for logs. Mostly split by file, but obviously there's
* re-use, and occasionally related stuff is grouped.
*/
const (
Jellyseerr = "Jellyseerr"
Jellyfin = "Jellyfin"
Ombi = "Ombi"
Discord = "Discord"
Telegram = "Telegram"
Matrix = "Matrix"
Email = "Email"
// main.go
FailedLogging = "Failed to start log wrapper: %v\n"
NoConfig = "Couldn't find default config file"
Write = "Wrote to \"%s\""
FailedWriting = "Failed to write to \"%s\": %v"
FailedCreateDir = "Failed to create directory \"%s\": %v"
FailedReading = "Failed to read from \"%s\": %v"
FailedOpen = "Failed to open \"%s\": %v"
FailedStat = "Failed to stat \"%s\": %v"
PathNotFound = "Path \"%s\" not found"
CopyConfig = "Copied default configuration to \"%s\""
FailedCopyConfig = "Failed to copy default configuration to \"%s\": %v"
LoadConfig = "Loaded config file \"%s\""
FailedLoadConfig = "Failed to load config file \"%s\": %v"
ModifyConfig = "Config saved to \"%s\""
SocketPath = "Socket Path: \"%s\""
FailedSocketConnect = "Couldn't establish socket connection at \"%s\": %v"
SocketCheckRunning = "Make sure jfa-go is running."
FailedSocketRead = "Couldn't read message on socket \"%s\": %v"
SocketWrite = "Command sent."
FailedSocketWrite = "Coudln't write message on socket \"%s\": %v"
FailedLangLoad = "Failed to load language files: %v"
UsingTLS = "Using TLS/HTTP2"
UsingOmbi = "Starting " + " + Ombi + " + " client"
UsingJellyseerr = "Starting " + Jellyseerr + " client"
UsingEmby = "Using Emby server type (EXPERIMENTAL: PWRs are not available, and support is limited.)"
UsingJellyfin = "Using " + Jellyfin + " server type"
UsingJellyfinAuth = "Using " + Jellyfin + " for authentication"
UsingLocalAuth = "Using local username/pw authentication (NOT RECOMMENDED)"
AuthJellyfin = "Authenticated with " + Jellyfin + " @ \"%s\""
FailedAuthJellyfin = "Failed to authenticate with " + Jellyfin + " @ \"%s\" (code %d): %v"
InitDiscord = "Initialized Discord daemon"
FailedInitDiscord = "Failed to initialize Discord daemon: %v"
InitTelegram = "Initialized Telegram daemon"
FailedInitTelegram = "Failed to initialize Telegram daemon: %v"
InitMatrix = "Initialized Matrix daemon"
FailedInitMatrix = "Failed to initialize Matrix daemon: %v"
InitRouter = "Initializing router"
LoadRoutes = "Loading Routes"
LoadingSetup = "Loading setup @ \"%s\""
ServingSetup = "Loaded, visit \"%s\" to start."
InvalidSSLCert = "Failed loading SSL Certificate \"%s\": %v"
InvalidSSLKey = "Failed loading SSL Keyfile \"%s\": %v"
FailServeSSL = "Failure serving with SSL/TLS: %v"
FailServe = "Failure serving: %v"
Serving = "Loaded @ \"%s\""
QuitReceived = "Restart/Quit signal received, please be patient."
Quitting = "Shutting down..."
Restarting = "Restarting..."
FailedHardRestartWindows = "hard restarts not available on windows"
Quit = "Server shut down."
FailedQuit = "Server shutdown failed: %v"
// api-activities.go
FailedDBReadActivities = "Failed to read activities from DB: %v"
// api-backups.go
IgnoreInvalidFilename = "Invalid filename \"%s\", ignoring: %v"
GetUpload = "Retrieved uploaded file \"%s\""
FailedGetUpload = "Failed to retrieve file from form data: %v"
// api-invites.go
DeleteOldInvite = "Deleting old invite \"%s\""
DeleteInvite = "Deleting invite \"%s\""
FailedDeleteInvite = "Failed to delete invite \"%s\": %v"
GenerateInvite = "Generating new invite"
FailedGenerateInvite = "Failed to generate new invite: %v"
InvalidInviteCode = "Invalid invite code \"%s\""
FailedSendToTooltipNoUser = "Failed: \"%s\" not found"
FailedSendToTooltipMultiUser = "Failed: \"%s\" linked to multiple users"
FailedParseTime = "Failed to parse time value: %v"
FailedGetContactMethod = "Failed to get contact method for \"%s\", make sure one is set."
SetAdminNotify = "Set \"%s\" to %t for admin address \"%s\""
// *jellyseerr*.go
FailedGetUsers = "Failed to get user(s) from %s: %v"
// FIXME: Once done, look back at uses of FailedGetUsers for places where this would make more sense.
FailedGetUser = "Failed to get user \"%s\" from %s: %v"
FailedGetJellyseerrNotificationPrefs = "Failed to get user \"%s\"'s notification prefs from " + Jellyseerr + ": %v"
FailedSyncContactMethods = "Failed to sync contact methods with %s: %v"
ImportJellyseerrUser = "Triggered import for " + Jellyseerr + " user \"%s\" (New ID: %d)"
FailedImportUser = "Failed to get or trigger import for %s user \"%s\": %v"
// api-messages.go
FailedGetCustomMessage = "Failed to get custom message \"%s\""
SetContactPrefForService = "Set contact preference for %s (\"%s\"): %t"
// Matrix
InvalidPIN = "Invalid PIN \"%s\""
ExpiredPIN = "Expired PIN \"%s\""
InvalidPassword = "Invalid Password"
UnauthorizedPIN = "Unauthorized PIN \"%s\""
FailedCreateRoom = "Failed to create room: %v"
// api-profiles.go
SetDefaultProfile = "Setting default profile to \"%s\""
FailedApplyProfile = "Failed to apply profile for %s user \"%s\": %v"
ApplyProfile = "Applying settings from profile \"%s\""
FailedGetProfile = "Failed to find profile \"%s\""
FailedApplyTemplate = "Failed to apply %s template for %s user \"%s\": %v"
FallbackToDefault = ", using default"
CreateProfileFromUser = "Creating profile from user \"%s\""
FailedGetJellyfinDisplayPrefs = "Failed to get DisplayPreferences for user \"%s\" from " + Jellyfin + ": %v"
ProfileNoHomescreen = "No homescreen template in profile \"%s\""
Profile = "profile"
Lang = "language"
User = "user"
ApplyingTemplatesFrom = "Applying templates from %s: \"%s\" to %d users"
DelayingRequests = "Delay will be added between requests (count = %d)"
// api-userpage.go
EmailConfirmationRequired = "User \"%s\" requires email confirmation"
ChangePassword = "Changed password for %s user \"%s\""
FailedChangePassword = "Failed to change password for %s user \"%s\": %v"
GetReferralTemplate = "Found referral template \"%s\""
FailedGetReferralTemplate = "Failed to find referral template \"%s\": %v"
DeleteOldReferral = "Deleting old referral \"%s\""
RenewOldReferral = "Renewing old referral \"%s\""
// api-users.go
CreateUser = "Created %s user \"%s\""
FailedCreateUser = "Failed to create new %s user \"%s\": %v"
LinkUser = "Linked %s user \"%s\""
FailedLinkUser = "Failed to link %s user \"%s\" with \"%s\": %v"
DeleteUser = "Deleted %s user \"%s\""
FailedDeleteUser = "Failed to delete %s user \"%s\": %v"
FailedDeleteUsers = "Failed to delete %s user(s): %v"
UserExists = "user already exists"
AccountLinked = "account already linked and require_unique enabled"
AccountUnverified = "unverified"
FailedSetDiscordMemberRole = "Failed to apply/remove " + Discord + " member role: %v"
FailedSetEmailAddress = "Failed to set email address for %s user \"%s\": %v"
AdditionalErrors = "Additional errors from %s: %v"
IncorrectCaptcha = "captcha incorrect"
ExtendCreateExpiry = "Extended or created expiry for user \"%s\""
UserEmailAdjusted = "Email for user \"%s\" adjusted"
UserAdminAdjusted = "Admin state for user \"%s\" set to %t"
UserLabelAdjusted = "Label for user \"%s\" set to \"%s\""
// api.go
ApplyUpdate = "Applied update"
FailedApplyUpdate = "Failed to apply update: %v"
UpdateManual = "update is manual"
// backups.go
DeleteOldBackup = "Deleted old backup \"%s\""
FailedDeleteOldBackup = "Failed to delete old backup \"%s\": %v"
CreateBackup = "Created database backup \"%+v\""
FailedCreateBackup = "Faled to create database backup: %v"
MoveOldDB = "Moved existing database to \"%s\""
FailedMoveOldDB = "Failed to move existing database to \"%s\": %v"
RestoreDB = "Restored database from \"%s\""
FailedRestoreDB = "Failed to resotre database from \"%s\": %v"
// config.go
EnableAllPWRMethods = "No PWR method preferences set in [user_page], all will be enabled"
InitProxy = "Initialized proxy @ \"%s\""
FailedInitProxy = "Failed to initialize proxy @ \"%s\": %v\nStartup will pause for a bit to grab your attention."
// discord.go
StartDaemon = "Started %s daemon"
FailedStartDaemon = "Failed to start %s daemon: %v"
FailedGetDiscordGuildMembers = "Failed to get " + Discord + " guild members: %v"
FailedGetDiscordGuild = "Failed to get " + Discord + " guild: %v"
FailedGetDiscordRoles = "Failed to get " + Discord + " roles: %v"
FailedCreateDiscordInviteChannel = "Failed to create " + Discord + " invite channel: %v"
InviteChannelEmpty = "no invite channel set in settings"
FailedGetDiscordChannels = "Failed to get " + Discord + " channel(s): %v"
FailedGetDiscordChannel = "Failed to get " + Discord + " channel \"%s\": %v"
MonitorAllDiscordChannels = "Will monitor all " + Discord + " channels"
FailedCreateDiscordDMChannel = "Failed to create " + Discord + " private DM channel with \"%s\": %v"
NotFound = "not found"
RegisterDiscordChoice = "Registered " + Discord + " %s choice \"%s\""
FailedRegisterDiscordChoices = "Failed to register " + Discord + " %s choices: %v"
FailedDeregDiscordChoice = "Failed to deregister " + Discord + " %s choice \"%s\": %v"
RegisterDiscordCommand = "Registered " + Discord + " command \"%s\""
FailedRegisterDiscordCommand = "Failed to register " + Discord + " command \"%s\": %v"
FailedGetDiscordCommands = "Failed to get " + Discord + " commands: %v"
FailedDeregDiscordCommand = "Failed to deregister " + Discord + " command \"%s\": %v"
FailedReply = "Failed to reply to %s message from \"%s\": %v"
FailedMessage = "Failed to send %s message to \"%s\": %v"
IgnoreOutOfChannelMessage = "Ignoring out-of-channel %s message"
FailedGenerateDiscordInvite = "Failed to generate " + Discord + " invite: %v"
// email.go
FailedInitSMTP = "Failed to initialize SMTP mailer: %v"
FailedGeneratePWRLink = "Failed to generate PWR link: %v"
// housekeeping-d.go
hk = "Housekeeping: "
hkcu = hk + "cleaning up "
HousekeepingEmail = hkcu + Email + " addresses"
HousekeepingDiscord = hkcu + Discord + " IDs"
HousekeepingTelegram = hkcu + Telegram + " IDs"
HousekeepingMatrix = hkcu + Matrix + " IDs"
HousekeepingCaptcha = hkcu + "PWR Captchas"
HousekeepingActivity = hkcu + "Activity log"
HousekeepingInvites = hkcu + "Invites"
ActivityLogTxnTooBig = hk + "Activity log delete transaction was too big, going one-by-one"
// matrix*.go
FailedSyncMatrix = "Failed to sync " + Matrix + " daemon: %v"
FailedCreateMatrixRoom = "Failed to create " + Matrix + " room with user \"%s\": %v"
MatrixOLMLog = "Matrix/OLM: %v"
MatrixOLMTraceLog = "Matrix/OLM [TRACE]:"
FailedDecryptMatrixMessage = "Failed to decrypt " + Matrix + " E2EE'd message: %v"
FailedEnableMatrixEncryption = "Failed to enable encryption in " + Matrix + " room \"%s\": %v"
// NOTE: "migrations.go" is the one file where log messages are not part of logmessages/logmessages.go.
// pwreset.go
PWRExpired = "PWR for user \"%s\" already expired @ %s, check system time!"
// router.go
UseDefaultHTML = "Using default HTML \"%s\""
UseCustomHTML = "Using custom HTML \"%s\""
FailedLoadTemplates = "Failed to load %s templates: %v"
Internal = "internal"
External = "external"
RegisterPprof = "Registered pprof"
SwaggerWarning = "Warning: Swagger should not be used on a public instance."
// storage.go
ConnectDB = "Connected to DB \"%s\""
FailedConnectDB = "Failed to open/connect to database \"%s\": %v"
// updater.go
NoUpdate = "No new updates available"
FoundUpdate = "Found update"
FailedGetUpdateTag = "Failed to get latest tag: %v"
FailedGetUpdate = "Failed to get update: %v"
UpdateTagDetails = "Update/Tag details: %+v"
// user-auth.go
UserPage = "userpage"
UserPageRequiresJellyfinAuth = "Jellyfin login must be enabled for user page access."
// user-d.go
CheckUserExpiries = "Checking for user expiry"
DeleteExpiryForOldUser = "Deleting expiry for old user \"%s\""
DeleteExpiredUser = "Deleting expired user \"%s\""
DisableExpiredUser = "Disabling expired user \"%s\""
FailedDeleteOrDisableExpiredUser = "Failed to delete/disable expired user \"%s\": %v"
// views.go
FailedServerPush = "Failed to use HTTP/2 Server Push: %v"
IgnoreBotPWR = "Ignore PWR magic link visit from bot"
ReCAPTCHA = "ReCAPTCHA"
FailedGenerateCaptcha = "Failed to generate captcha: %v"
CaptchaNotFound = "Captcha \"%s\" not found in invite \"%s\""
FailedVerifyReCAPTCHA = "Failed to verify reCAPTCHA: %v"
InvalidHostname = "invalid hostname (wanted \"%s\", got \"%s\")"
)
const (
FailedGetCookies = "Failed to get cookie(s) \"%s\": %v"
FailedParseJWT = "Failed to parse JWT: %v"
FailedCastJWT = "JWT claims unreadable"
InvalidJWT = "JWT was invalidated, of incorrect type or has expired"
LocallyInvalidatedJWT = "JWT is listed as invalidated"
FailedSignJWT = "Failed to sign JWT: %v"
RequestingToken = "Token requested (%s)"
TokenLoginAttempt = "login attempt"
TokenRefresh = "refresh token"
UserTokenLoginAttempt = UserPage + " " + TokenLoginAttempt
UserTokenRefresh = UserPage + " " + TokenRefresh
GenerateToken = "Token generated for user \"%s\""
FailedGenerateToken = "Failed to generate token: %v"
FailedAuthRequest = "Failed to authorize request: %v"
InvalidAuthHeader = "invalid auth header"
NonAdminToken = "token not for admin use"
NonAdminUser = "user \"%s\" not admin"
InvalidUserOrPass = "invalid user/pass"
EmptyUserOrPass = "invalid user/pass"
UserDisabled = "user is disabled"
)
const (
FailedConstructExpiryAdmin = "Failed to construct expiry notification for \"%s\": %v"
FailedSendExpiryAdmin = "Failed to send expiry notification for \"%s\" to \"%s\": %v"
SentExpiryAdmin = "Sent expiry notification for \"%s\" to \"%s\""
FailedConstructCreationAdmin = "Failed to construct creation notification for \"%s\": %v"
FailedSendCreationAdmin = "Failed to send creation notification for \"%s\" to \"%s\": %v"
SentCreationAdmin = "Sent creation notification for \"%s\" to \"%s\""
FailedConstructInviteMessage = "Failed to construct invite message for \"%s\": %v"
FailedSendInviteMessage = "Failed to send invite message for \"%s\" to \"%s\": %v"
SentInviteMessage = "Sent invite message for \"%s\" to \"%s\""
FailedConstructConfirmationEmail = "Failed to construct confirmation email for \"%s\": %v"
FailedSendConfirmationEmail = "Failed to send confirmation email for \"%s\" to \"%s\": %v"
SentConfirmationEmail = "Sent confirmation email for \"%s\" to \"%s\""
FailedConstructPWRMessage = "Failed to construct PWR message for \"%s\": %v"
FailedSendPWRMessage = "Failed to send PWR message for \"%s\" to \"%s\": %v"
SentPWRMessage = "Sent PWR message for \"%s\" to \"%s\""
FailedConstructWelcomeMessage = "Failed to construct welcome message for \"%s\": %v"
FailedSendWelcomeMessage = "Failed to send welcome message for \"%s\" to \"%s\": %v"
SentWelcomeMessage = "Sent welcome message for \"%s\" to \"%s\""
FailedConstructEnableDisableMessage = "Failed to construct enable/disable message for \"%s\": %v"
FailedSendEnableDisableMessage = "Failed to send enable/disable message for \"%s\" to \"%s\": %v"
SentEnableDisableMessage = "Sent enable/disable message for \"%s\" to \"%s\""
FailedConstructDeletionMessage = "Failed to construct account deletion message for \"%s\": %v"
FailedSendDeletionMessage = "Failed to send account deletion message for \"%s\" to \"%s\": %v"
SentDeletionMessage = "Sent account deletion message for \"%s\" to \"%s\""
FailedConstructExpiryAdjustmentMessage = "Failed to construct expiry adjustment message for \"%s\": %v"
FailedSendExpiryAdjustmentMessage = "Failed to send expiry adjustment message for \"%s\" to \"%s\": %v"
SentExpiryAdjustmentMessage = "Sent expiry adjustment message for \"%s\" to \"%s\""
FailedConstructExpiryMessage = "Failed to construct expiry message for \"%s\": %v"
FailedSendExpiryMessage = "Failed to send expiry message for \"%s\" to \"%s\": %v"
SentExpiryMessage = "Sent expiry message for \"%s\" to \"%s\""
FailedConstructAnnouncementMessage = "Failed to construct announcement message for \"%s\": %v"
FailedSendAnnouncementMessage = "Failed to send announcement message for \"%s\" to \"%s\": %v"
SentAnnouncementMessage = "Sent announcement message for \"%s\" to \"%s\""
)

150
main.go
View File

@ -27,7 +27,6 @@ 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"
@ -102,9 +101,8 @@ type appContext struct {
// Keeping jf name because I can't think of a better one
jf *mediabrowser.MediaBrowser
authJf *mediabrowser.MediaBrowser
ombi *OmbiWrapper
js *JellyseerrWrapper
thirdPartyServices []ThirdPartyService
ombi *ombi.Ombi
js *jellyseerr.Jellyseerr
datePattern string
timePattern string
storage Storage
@ -113,7 +111,6 @@ type appContext struct {
telegram *TelegramDaemon
discord *DiscordDaemon
matrix *MatrixDaemon
contactMethods []ContactMethodLinker
info, debug, err *logger.Logger
host string
port int
@ -216,21 +213,22 @@ func start(asDaemon, firstCall bool) {
firstRun = true
dConfig, err := fs.ReadFile(localFS, "config-default.ini")
if err != nil {
app.err.Fatalf(lm.NoConfig)
app.err.Fatalf("Couldn't find default config file")
}
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.Fatalf(lm.FailedWriting, app.configPath, err)
app.err.Printf("Couldn't open config file for writing: \"%s\"", app.configPath)
app.err.Fatalf("Error: %s", err)
}
defer nConfig.Close()
_, err = nConfig.Write(dConfig)
if err != nil {
app.err.Fatalf(lm.FailedCopyConfig, app.configPath, err)
app.err.Fatalf("Couldn't copy default config.")
}
app.info.Printf(lm.CopyConfig, app.configPath)
app.info.Printf("Copied default configuration to \"%s\"", app.configPath)
tempConfig, _ := ini.Load(app.configPath)
tempConfig.Section("").Key("first_run").SetValue("true")
tempConfig.SaveTo(app.configPath)
@ -239,9 +237,8 @@ func start(asDaemon, firstCall bool) {
var debugMode bool
var address string
if err := app.loadConfig(); err != nil {
app.err.Fatalf(lm.FailedLoadConfig, app.configPath, err)
app.err.Fatalf("Failed to load config file \"%s\": %v", app.configPath, err)
}
app.info.Printf(lm.LoadConfig, app.configPath)
if app.config.Section("").Key("first_run").MustBool(false) {
firstRun = true
@ -273,7 +270,7 @@ func start(asDaemon, firstCall bool) {
os.Remove(SOCK)
listener, err := net.Listen("unix", SOCK)
if err != nil {
app.err.Fatalf(lm.FailedSocketConnect, SOCK, err)
app.err.Fatalf("Couldn't establish socket connection at %s\n", SOCK)
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
@ -289,13 +286,13 @@ func start(asDaemon, firstCall bool) {
for {
con, err := listener.Accept()
if err != nil {
app.err.Printf(lm.FailedSocketRead, SOCK, err)
app.err.Printf("Couldn't read message on %s: %s", SOCK, err)
continue
}
buf := make([]byte, 512)
nr, err := con.Read(buf)
if err != nil {
app.err.Printf(lm.FailedSocketRead, SOCK, err)
app.err.Printf("Couldn't read message on %s: %s", SOCK, err)
continue
}
command := string(buf[0:nr])
@ -320,13 +317,13 @@ func start(asDaemon, firstCall bool) {
err = app.storage.loadLang(langFS, os.DirFS(externalLang))
}
if err != nil {
app.info.Fatalf(lm.FailedLangLoad, err)
app.info.Fatalf("Failed to load language files: %+v\n", err)
}
if !firstRun {
app.host = app.config.Section("ui").Key("host").String()
if app.config.Section("advanced").Key("tls").MustBool(false) {
app.info.Println(lm.UsingTLS)
app.info.Println("Using TLS/HTTP2")
app.port = app.config.Section("advanced").Key("tls_port").MustInt(8057)
} else {
app.port = app.config.Section("ui").Key("port").MustInt(8056)
@ -351,32 +348,29 @@ func start(asDaemon, firstCall bool) {
}
address = fmt.Sprintf("%s:%d", app.host, app.port)
// 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!
app.debug.Printf("Loaded config file \"%s\"", app.configPath)
if app.config.Section("ombi").Key("enabled").MustBool(false) {
app.ombi = &OmbiWrapper{}
app.debug.Printf(lm.UsingOmbi)
app.debug.Printf("Connecting to Ombi")
ombiServer := app.config.Section("ombi").Key("server").String()
app.ombi.Ombi = ombi.NewOmbi(
app.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.js = &JellyseerrWrapper{}
app.debug.Printf(lm.UsingJellyseerr)
app.debug.Printf("Connecting to Jellyseerr")
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
app.js.Jellyseerr = jellyseerr.NewJellyseerr(
app.js = jellyseerr.NewJellyseerr(
jellyseerrServer,
app.config.Section("jellyseerr").Key("api_key").String(),
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
)
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
// app.js.LogRequestBodies = true
app.thirdPartyServices = append(app.thirdPartyServices, app.js)
}
@ -404,9 +398,10 @@ func start(asDaemon, firstCall bool) {
if stringServerType == "emby" {
serverType = mediabrowser.EmbyServer
timeoutHandler = mediabrowser.NewNamedTimeoutHandler("Emby", "\""+server+"\"", true)
app.info.Println(lm.UsingEmby)
app.info.Println("Using Emby server type")
fmt.Println(warning("WARNING: Emby compatibility is experimental, and support is limited.\nPassword resets are not available."))
} else {
app.info.Println(lm.UsingJellyfin)
app.info.Println("Using Jellyfin server type")
}
app.jf, err = mediabrowser.NewServer(
@ -420,7 +415,7 @@ func start(asDaemon, firstCall bool) {
cacheTimeout,
)
if err != nil {
app.err.Fatalf(lm.FailedAuthJellyfin, server, -1, err)
app.err.Fatalf("Failed to authenticate with Jellyfin @ \"%s\": %v", server, err)
}
if debugMode {
app.jf.Verbose = true
@ -438,9 +433,9 @@ func start(asDaemon, firstCall bool) {
}
_, status, err = app.jf.MustAuthenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String(), retryOpts)
if status != 200 || err != nil {
app.err.Fatalf(lm.FailedAuthJellyfin, server, status, err)
app.err.Fatalf("Failed to authenticate with Jellyfin @ \"%s\" (%d): %v", server, status, err)
}
app.info.Printf(lm.AuthJellyfin, server)
app.info.Printf("Authenticated with \"%s\"", server)
runMigrations(app)
@ -453,9 +448,8 @@ 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(lm.UsingJellyfinAuth)
app.debug.Println("Using Jellyfin for authentication")
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
if debugMode {
app.authJf.Verbose = true
@ -518,42 +512,34 @@ func start(asDaemon, firstCall bool) {
defer backupDaemon.Shutdown()
}
// NOTE: The order in which these are placed in app.contactMethods matters.
// Add new ones to the end.
if discordEnabled {
app.discord, err = newDiscordDaemon(app)
if err != nil {
app.err.Printf(lm.FailedInitDiscord, err)
discordEnabled = false
} else {
app.debug.Println(lm.InitDiscord)
go app.discord.run()
defer app.discord.Shutdown()
app.contactMethods = append(app.contactMethods, app.discord)
}
}
if telegramEnabled {
app.telegram, err = newTelegramDaemon(app)
if err != nil {
app.err.Printf(lm.FailedInitTelegram, err)
app.err.Printf("Failed to authenticate with Telegram: %v", 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 discordEnabled {
app.discord, err = newDiscordDaemon(app)
if err != nil {
app.err.Printf("Failed to authenticate with Discord: %v", err)
discordEnabled = false
} else {
go app.discord.run()
defer app.discord.Shutdown()
}
}
if matrixEnabled {
app.matrix, err = newMatrixDaemon(app)
if err != nil {
app.err.Printf(lm.FailedInitMatrix, err)
app.err.Printf("Failed to initialize Matrix daemon: %v", 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 {
@ -572,7 +558,7 @@ func start(asDaemon, firstCall bool) {
app.storage.lang.SetupPath = "setup"
err := app.storage.loadLangSetup(langFS)
if err != nil {
app.info.Fatalf(lm.FailedLangLoad, err)
app.info.Fatalf("Failed to load language files: %+v\n", err)
}
}
@ -580,14 +566,14 @@ func start(asDaemon, firstCall bool) {
// workaround for potentially broken windows mime types
mime.AddExtensionType(".js", "application/javascript")
app.info.Println(lm.InitRouter)
app.info.Println("Initializing router")
router := app.loadRouter(address, debugMode)
app.info.Println(lm.LoadRoutes)
app.info.Println("Loading routes")
if !firstRun {
app.loadRoutes(router)
} else {
app.loadSetup(router)
app.info.Printf(lm.LoadingSetup, address)
app.info.Printf("Loading setup @ %s", address)
}
go func() {
if app.config.Section("advanced").Key("tls").MustBool(false) {
@ -595,45 +581,45 @@ func start(asDaemon, firstCall bool) {
key := app.config.Section("advanced").Key("tls_key").MustString("")
if err := SRV.ListenAndServeTLS(cert, key); err != nil {
filesToCheck := []string{cert, key}
fileNames := []string{lm.InvalidSSLCert, lm.InvalidSSLKey}
fileNames := []string{"Certificate", "Key"}
for i, v := range filesToCheck {
_, err := os.Stat(v)
if err != nil {
app.err.Printf(fileNames[i], v, err)
app.err.Printf("SSL/TLS %s: %v\n", fileNames[i], err)
}
}
if err == http.ErrServerClosed {
app.err.Printf(lm.FailServeSSL, err)
app.err.Printf("Failure serving with SSL/TLS: %s", err)
} else {
app.err.Fatalf(lm.FailServeSSL, err)
app.err.Fatalf("Failure serving with SSL/TLS: %s", err)
}
}
} else {
if err := SRV.ListenAndServe(); err != nil {
if err == http.ErrServerClosed {
app.err.Printf(lm.FailServe, err)
app.err.Printf("Failure serving: %s", err)
} else {
app.err.Fatalf(lm.FailServe, err)
app.err.Fatalf("Failure serving: %s", err)
}
}
}
}()
if firstRun {
app.info.Printf(lm.ServingSetup, address)
app.info.Printf("Loaded, visit %s to start.", address)
} else {
app.info.Printf(lm.Serving, address)
app.info.Printf("Loaded @ %s", address)
}
waitForRestart()
app.info.Printf(lm.QuitReceived)
app.info.Printf("Restart/Quit signal received, give me a second!")
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
if err := SRV.Shutdown(ctx); err != nil {
app.err.Fatalf(lm.FailedQuit, err)
app.err.Fatalf("Server shutdown error: %s", err)
}
app.info.Println(lm.Quit)
app.info.Println("Server shut down.")
return
}
@ -645,7 +631,7 @@ func shutdown() {
}
func (app *appContext) shutdown() {
app.info.Println(lm.Quitting)
app.info.Println("Shutting down...")
shutdown()
}
@ -729,17 +715,15 @@ 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(lm.FailedLogging, err)
fmt.Printf("Failed to start logging: %v\n", err)
}
defer f()
printVersion()
SOCK = filepath.Join(temp, SOCK)
fmt.Printf(lm.SocketPath+"\n", SOCK)
fmt.Println("Socket:", SOCK)
if flagPassed("test") {
TEST = true
}
@ -768,26 +752,24 @@ func main() {
} else if flagPassed("stop") {
con, err := net.Dial("unix", SOCK)
if err != nil {
fmt.Printf(lm.FailedSocketConnect+"\n", SOCK, err)
fmt.Println(lm.SocketCheckRunning)
fmt.Printf("Couldn't dial socket %s, are you sure jfa-go is running?\n", SOCK)
os.Exit(1)
}
_, err = con.Write([]byte("stop"))
if err != nil {
fmt.Printf(lm.FailedSocketWrite+"\n", SOCK, err)
fmt.Println(lm.SocketCheckRunning)
fmt.Printf("Couldn't send command to socket %s, are you sure jfa-go is running?\n", SOCK)
os.Exit(1)
}
fmt.Println(lm.SocketWrite)
fmt.Println("Sent.")
} else if flagPassed("daemon") {
start(true, true)
} else if flagPassed("systemd") {
service, err := fs.ReadFile(localFS, SYSTEMD_SERVICE)
service, err := fs.ReadFile(localFS, "jfa-go.service")
if err != nil {
fmt.Printf(lm.FailedReading+"\n", SYSTEMD_SERVICE, err)
fmt.Printf("Couldn't read jfa-go.service: %v\n", err)
os.Exit(1)
}
absPath, err := os.Executable()
absPath, err := filepath.Abs(os.Args[0])
if err != nil {
absPath = os.Args[0]
}
@ -798,13 +780,13 @@ func main() {
}
}
service = []byte(strings.Replace(string(service), "{executable}", command, 1))
err = os.WriteFile(SYSTEMD_SERVICE, service, 0666)
err = os.WriteFile("jfa-go.service", service, 0666)
if err != nil {
fmt.Printf(lm.FailedWriting+"\n", SYSTEMD_SERVICE, err)
fmt.Printf("Couldn't write jfa-go.service: %v\n", 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 SYSTEMD_SERVICE file to ~/.config/systemd/user (Creating it if necessary).
Move the newly created "jfa-go.service" file to ~/.config/systemd/user (Creating it if necessary).
Then run "systemctl --user daemon-reload".
You can then run:

View File

@ -6,7 +6,6 @@ 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"
@ -32,6 +31,15 @@ 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{
@ -110,13 +118,13 @@ func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string
func (d *MatrixDaemon) run() {
startTime := d.start
d.app.info.Println(lm.StartDaemon, lm.Matrix)
d.app.info.Println("Starting Matrix bot daemon")
syncer := d.bot.Syncer.(*mautrix.DefaultSyncer)
HandleSyncerCrypto(startTime, d, syncer)
syncer.OnEventType(event.EventMessage, d.handleMessage)
if err := d.bot.Sync(); err != nil {
d.app.err.Printf(lm.FailedSyncMatrix, err)
d.app.err.Printf("Matrix sync failed: %v", err)
}
}
@ -162,7 +170,7 @@ func (d *MatrixDaemon) commandLang(evt *event.Event, code, lang string) {
list,
)
if err != nil {
d.app.err.Printf(lm.FailedReply, lm.Matrix, evt.Sender, err)
d.app.err.Printf("Matrix: Failed to send message to \"%s\": %v", evt.Sender, err)
}
return
}
@ -195,7 +203,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(lm.FailedCreateMatrixRoom, userID, err)
d.app.err.Printf("Failed to create room for user \"%s\": %v", userID, err)
return
}
lang := "en-us"
@ -218,7 +226,7 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
roomID,
)
if err != nil {
d.app.err.Printf(lm.FailedMessage, lm.Matrix, userID, err)
d.app.err.Printf("Matrix: Failed to send welcome message to \"%s\": %v", userID, err)
return
}
ok = true
@ -268,57 +276,6 @@ 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)
}

View File

@ -1,13 +1,10 @@
//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"
@ -68,22 +65,22 @@ type olmLogger struct {
}
func (o olmLogger) Error(message string, args ...interface{}) {
o.app.err.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, args))
o.app.err.Printf("OLM: "+message+"\n", args)
}
func (o olmLogger) Warn(message string, args ...interface{}) {
o.app.info.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, args))
o.app.info.Printf("OLM: "+message+"\n", args)
}
func (o olmLogger) Debug(message string, args ...interface{}) {
o.app.debug.Printf(lm.MatrixOlmLog, fmt.Sprintf(message, args))
o.app.debug.Printf("OLM: "+message+"\n", args)
}
func (o olmLogger) Trace(message string, args ...interface{}) {
if strings.HasPrefix(message, "Got membership state event") {
return
}
o.app.debug.Printf(lm.MatrixOlmTracelog, fmt.Sprintf(message, args))
o.app.debug.Printf("OLM [TRACE]: "+message+"\n", args)
}
func InitMatrixCrypto(d *MatrixDaemon) (err error) {
@ -158,7 +155,7 @@ func HandleSyncerCrypto(startTime int64, d *MatrixDaemon, syncer *mautrix.Defaul
// return
// }
if err != nil {
d.app.err.Printf(lm.FailedDecryptMatrixMessage, err)
d.app.err.Printf("Failed to decrypt Matrix message: %v", err)
return
}
d.handleMessage(source, decrypted)
@ -183,7 +180,7 @@ func EncryptRoom(d *MatrixDaemon, room *mautrix.RespCreateRoom, userID id.UserID
if err == nil {
encrypted = true
} else {
d.app.debug.Printf(lm.FailedEnableMatrixEncryption, room.RoomID, err)
d.app.debug.Printf("Matrix: Failed to enable encryption in room: %v", err)
return
}
d.isEncrypted[room.RoomID] = encrypted

View File

@ -9,8 +9,6 @@ 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)

View File

@ -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.
}

View File

@ -8,7 +8,6 @@ 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.
@ -40,16 +39,16 @@ func (app *appContext) GenResetLink(pin string) (string, error) {
}
func (app *appContext) StartPWR() {
app.info.Println(lm.StartDaemon, "PWR")
app.info.Println("Starting password reset daemon")
path := app.config.Section("password_resets").Key("watch_directory").String()
if _, err := os.Stat(path); os.IsNotExist(err) {
app.err.Printf(lm.FailedStartDaemon, "PWR", fmt.Sprintf(lm.PathNotFound, path))
app.err.Printf("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path)
return
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
app.err.Printf("Couldn't initialise password reset daemon")
return
}
defer watcher.Close()
@ -57,7 +56,7 @@ func (app *appContext) StartPWR() {
go pwrMonitor(app, watcher)
err = watcher.Add(path)
if err != nil {
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
app.err.Printf("Failed to start password reset daemon: %s", err)
}
waitForRestart()
@ -85,36 +84,43 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
var pwr PasswordReset
data, err := os.ReadFile(event.Name)
if err != nil {
app.debug.Printf(lm.FailedReading, event.Name, err)
app.debug.Printf("PWR: Failed to read file: %v", err)
return
}
err = json.Unmarshal(data, &pwr)
if len(pwr.Pin) == 0 || err != nil {
app.debug.Printf(lm.FailedReading, event.Name, err)
app.debug.Printf("PWR: Failed to read PIN: %v", err)
continue
}
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 || user.ID == "" {
app.err.Printf(lm.FailedGetUser, pwr.Username, lm.Jellyfin, err)
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)
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(lm.FailedConstructPWRMessage, pwr.Username, err)
app.err.Printf("Failed to construct password reset message for \"%s\"", pwr.Username)
app.debug.Printf("%s: Error: %s", pwr.Username, err)
} else if err := app.sendByID(msg, uid); err != nil {
app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, name, err)
app.err.Printf("Failed to send password reset message to \"%s\"", name)
app.debug.Printf("%s: Error: %s", pwr.Username, err)
} else {
app.err.Printf(lm.SentPWRMessage, pwr.Username, name)
app.info.Printf("Sent password reset message to \"%s\"", name)
}
}
} else {
app.err.Printf(lm.PWRExpired, pwr.Username, pwr.Expiry)
app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry)
}
}
@ -122,7 +128,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
if !ok {
return
}
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
app.err.Printf("Password reset daemon: %s", err)
}
}
}

View File

@ -1,11 +1,7 @@
package main
import (
"fmt"
lm "github.com/hrfee/jfa-go/logmessages"
)
import "fmt"
func (app *appContext) HardRestart() error {
return fmt.Errorf(lm.FailedHardRestartWindows)
return fmt.Errorf("hard restarts not available on windows")
}

View File

@ -11,7 +11,6 @@ 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"
)
@ -22,17 +21,17 @@ func (app *appContext) loadHTML(router *gin.Engine) {
templatePath := "html"
htmlFiles, err := fs.ReadDir(localFS, templatePath)
if err != nil {
app.err.Fatalf(lm.FailedReading, templatePath, err)
app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath)
return
}
loadInternal := []string{}
loadExternal := []string{}
for _, f := range htmlFiles {
if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) {
app.debug.Printf(lm.UseDefaultHTML, f.Name())
app.debug.Printf("Using default \"%s\"", f.Name())
loadInternal = append(loadInternal, FSJoin(templatePath, f.Name()))
} else {
app.info.Printf(lm.UseCustomHTML, f.Name())
app.info.Printf("Using custom \"%s\"", f.Name())
loadExternal = append(loadExternal, filepath.Join(filepath.Join(customPath, f.Name())))
}
}
@ -40,13 +39,13 @@ func (app *appContext) loadHTML(router *gin.Engine) {
if len(loadInternal) != 0 {
tmpl, err = template.ParseFS(localFS, loadInternal...)
if err != nil {
app.err.Fatalf(lm.FailedLoadTemplates, lm.Internal, err)
app.err.Fatalf("Failed to load templates: %v", err)
}
}
if len(loadExternal) != 0 {
tmpl, err = tmpl.ParseFiles(loadExternal...)
if err != nil {
app.err.Fatalf(lm.FailedLoadTemplates, lm.External, err)
app.err.Fatalf("Failed to load external templates: %v", err)
}
}
router.SetHTMLTemplate(tmpl)
@ -97,7 +96,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(lm.RegisterPprof)
app.debug.Println("Loading pprof")
pprof.Register(router)
}
SRV = &http.Server{
@ -135,7 +134,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.GET(p+"/lang/:page/:file", app.ServeLang)
router.GET(p+"/token/login", app.getTokenLogin)
router.GET(p+"/token/refresh", app.getTokenRefresh)
router.POST(p+"/newUser", app.NewUserFromInvite)
router.POST(p+"/newUser", app.NewUser)
router.Use(static.Serve(p+"/invite/", app.webFS))
router.GET(p+"/invite/:invCode", app.InviteProxy)
if app.config.Section("captcha").Key("enabled").MustBool(false) {
@ -166,7 +165,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
}
}
if *SWAGGER {
app.info.Print(warning(lm.SwaggerWarning))
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
for _, p := range routePrefixes {
router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
@ -182,7 +181,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
router.POST(p+"/logout", app.Logout)
api.DELETE(p+"/users", app.DeleteUsers)
api.GET(p+"/users", app.GetUsers)
api.POST(p+"/users", app.NewUserFromAdmin)
api.POST(p+"/users", app.NewUserAdmin)
api.POST(p+"/users/extend", app.ExtendExpiry)
api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
api.POST(p+"/users/enable", app.EnableDisableUsers)

View File

@ -8,7 +8,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/easyproxy"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser"
)
@ -105,7 +104,7 @@ func (app *appContext) TestJF(gc *gin.Context) {
case 404:
msg = "error404"
}
app.err.Printf(lm.FailedAuthJellyfin, req.Server, status, err)
app.info.Printf("Auth failed with code %d (%s)", status, err)
if msg != "" {
respond(status, msg, gc)
} else {
@ -152,20 +151,16 @@ 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)
@ -175,20 +170,16 @@ 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)

283
static/banner.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -2,7 +2,6 @@ package main
import (
"encoding/json"
"fmt"
"io/fs"
"log"
"os"
@ -14,7 +13,6 @@ 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"
@ -177,10 +175,10 @@ func (app *appContext) ConnectDB() {
opts.ValueDir = app.storage.db_path
db, err := badgerhold.Open(opts)
if err != nil {
app.err.Fatalf(lm.FailedConnectDB, app.storage.db_path, err)
app.err.Fatalf("Failed to open db \"%s\": %v", app.storage.db_path, err)
}
app.storage.db = db
app.info.Printf(lm.ConnectDB, app.storage.db_path)
app.info.Printf("Connected to DB \"%s\"", app.storage.db_path)
}
// GetEmails returns a copy of the store.
@ -510,15 +508,6 @@ 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{}
@ -594,35 +583,12 @@ func (st *Storage) DeleteActivityKey(k string) {
st.db.Delete(k, Activity{})
}
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 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 DiscordUser struct {
@ -635,24 +601,6 @@ 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.
@ -737,16 +685,6 @@ 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
@ -780,27 +718,22 @@ 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...)

View File

@ -7,7 +7,6 @@ import (
"time"
tg "github.com/go-telegram-bot-api/telegram-bot-api"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/timshannon/badgerhold/v4"
)
@ -16,27 +15,9 @@ const (
)
type TelegramVerifiedToken struct {
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)
ChatID int64
Username string
JellyfinID string // optional, for ensuring a user-requested change is only accessed by them.
}
// VerifToken stores details about a pending user verification token.
@ -115,12 +96,12 @@ func (t *TelegramDaemon) NewAssignedAuthToken(id string) string {
}
func (t *TelegramDaemon) run() {
t.app.info.Println(lm.StartDaemon, lm.Telegram)
t.app.info.Println("Starting Telegram bot daemon")
u := tg.NewUpdate(0)
u.Timeout = 60
updates, err := t.bot.GetUpdatesChan(u)
if err != nil {
t.app.err.Printf(lm.FailedStartDaemon, lm.Telegram, err)
t.app.err.Printf("Failed to start Telegram daemon: %v", err)
telegramEnabled = false
return
}
@ -218,7 +199,7 @@ func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang strin
content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"})
err := t.Reply(upd, content)
if err != nil {
t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err)
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
}
@ -230,7 +211,7 @@ func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string
}
err := t.Reply(upd, list)
if err != nil {
t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err)
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
return
}
@ -251,14 +232,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(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err)
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
delete(t.tokens, upd.Message.Text)
return
}
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
if err != nil {
t.app.err.Printf(lm.FailedReply, lm.Telegram, upd.Message.From.UserName, err)
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
}
t.verifiedTokens[upd.Message.Text] = TelegramVerifiedToken{
ChatID: upd.Message.Chat.ID,
@ -292,39 +273,7 @@ func (t *TelegramDaemon) UserExists(username string) bool {
return err != nil || c > 0
}
// Exists returns whether or not the given user exists.
func (t *TelegramDaemon) Exists(user ContactMethodUser) bool {
return t.UserExists(user.Name())
}
// DeleteVerifiedToken removes the token with the given PIN.
func (t *TelegramDaemon) DeleteVerifiedToken(PIN string) {
delete(t.verifiedTokens, 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 }

View File

@ -221,16 +221,12 @@ class LangSelect extends Select {
}
}
const replaceLink = (elName: string, sect: string, name: string, url: string, text: string) => html(elName, window.lang.var(sect, name, `<a class="underline" target="_blank" href="${url}">${text}</a>`));
window.lang = new lang(window.langFile as LangFile);
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");
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>`));
const settings = {
"jellyfin": {
@ -322,12 +318,6 @@ 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"),

View File

@ -4,8 +4,7 @@
"target": "es2017",
"lib": ["dom", "es2017"],
"typeRoots": ["./typings", "../node_modules/@types"],
"module": "esnext",
"moduleResolution": "bundler",
"moduleResolution": "nodenext",
"esModuleInterop": true
}
}

View File

@ -16,8 +16,6 @@ import (
"strings"
"time"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/jfa-go/common"
)
@ -562,16 +560,15 @@ 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(lm.FailedGetUpdateTag, err)
app.err.Printf("Failed to get latest tag (%d): %v", status, err)
}
return
}
if tag != app.tag && tag.IsNew() {
app.info.Println(lm.FoundUpdate)
app.debug.Printf(lm.UpdateTagDetails, tag)
app.info.Println("Update found")
update, status, err := app.updater.GetUpdate(tag)
if status != 200 || err != nil {
app.err.Printf(lm.FailedGetUpdate, err)
app.err.Printf("Failed to get update (%d): %v", status, err)
return
}
app.tag = tag

View File

@ -1,11 +1,9 @@
package main
import (
"fmt"
"strings"
"github.com/gin-gonic/gin"
lm "github.com/hrfee/jfa-go/logmessages"
)
func (app *appContext) userAuth() gin.HandlerFunc {
@ -15,7 +13,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.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth)
app.err.Println("Enable Jellyfin Login to use the User Page feature.")
respond(500, "Contact Admin", gc)
return
}
@ -29,6 +27,7 @@ func (app *appContext) userAuthenticate(gc *gin.Context) {
gc.Set("jfId", jfID)
gc.Set("userMode", true)
app.debug.Println("Auth succeeded")
gc.Next()
}
@ -42,11 +41,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.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth)
app.err.Println("Enable Jellyfin Login to use the User Page feature.")
respond(500, "Contact Admin", gc)
return
}
app.logIpInfo(gc, true, fmt.Sprintf(lm.RequestingToken, lm.UserTokenLoginAttempt))
app.logIpInfo(gc, true, "UserToken requested (login attempt)")
username, password, ok := app.decodeValidateLoginHeader(gc, true)
if !ok {
return
@ -59,11 +58,12 @@ func (app *appContext) getUserTokenLogin(gc *gin.Context) {
token, refresh, err := CreateToken(user.ID, user.ID, false)
if err != nil {
app.err.Printf(lm.FailedGenerateToken, err)
app.err.Printf("getUserToken failed: Couldn't generate user token (%s)", err)
respond(500, "Couldn't generate user token", gc)
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.Printf(lm.FailedAuthRequest, lm.UserPageRequiresJellyfinAuth)
app.err.Println("Enable Jellyfin Login to use the User Page feature.")
respond(500, "Contact Admin", gc)
return
}
app.logIpInfo(gc, true, fmt.Sprintf(lm.RequestingToken, lm.UserTokenRefresh))
app.logIpInfo(gc, true, "UserToken request (refresh token)")
claims, ok := app.decodeValidateRefreshCookie(gc, "user-refresh")
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(lm.FailedGenerateToken, err)
app.err.Printf("getUserToken failed: Couldn't generate user token (%s)", err)
respond(500, "Couldn't generate user token", gc)
return
}

View File

@ -3,7 +3,6 @@ package main
import (
"time"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3"
)
@ -22,17 +21,17 @@ func (app *appContext) checkUsers() {
if len(app.storage.GetUserExpiries()) == 0 {
return
}
app.info.Println(lm.CheckUserExpiries)
app.info.Println("Daemon: Checking for user expiry")
users, status, err := app.jf.GetUsers(false)
if err != nil || status != 200 {
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
app.err.Printf("Failed to get users (%d): %s", status, err)
return
}
mode := "disable"
phrase := lm.DisableExpiredUser
term := "Disabling"
if app.config.Section("user_expiry").Key("behaviour").MustString("disable_user") == "delete_user" {
mode = "delete"
phrase = lm.DeleteExpiredUser
term = "Deleting"
}
contact := false
if messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) {
@ -46,7 +45,7 @@ func (app *appContext) checkUsers() {
for _, expiry := range app.storage.GetUserExpiries() {
id := expiry.JellyfinID
if _, ok := userExists[id]; !ok {
app.info.Printf(lm.DeleteExpiryForOldUser, id)
app.info.Printf("Deleting expiry for non-existent user \"%s\"", id)
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
} else if time.Now().After(expiry.Expiry) {
found := false
@ -59,10 +58,11 @@ func (app *appContext) checkUsers() {
}
}
if !found {
app.info.Printf("Expired user already deleted, ignoring.")
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
continue
}
app.info.Printf(phrase, user.Name)
app.info.Printf("%s expired user \"%s\"", term, user.Name)
// Record activity
activity := Activity{
@ -72,24 +72,18 @@ func (app *appContext) checkUsers() {
}
if mode == "delete" {
deleted := false
err, deleted = app.DeleteUser(user)
// Silence unimportant errors
if deleted {
err = nil
}
status, err = app.jf.DeleteUser(id)
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
err, _, _ = app.SetUserDisabled(user, true)
status, err = app.jf.SetPolicy(id, user.Policy)
activity.Type = ActivityDisabled
}
if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedDeleteOrDisableExpiredUser, user.ID, err)
app.err.Printf("Failed to %s \"%s\" (%d): %s", mode, user.Name, status, err)
continue
}
@ -104,11 +98,11 @@ func (app *appContext) checkUsers() {
name := app.getAddressOrName(user.ID)
msg, err := app.email.constructUserExpired(app, false)
if err != nil {
app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err)
app.err.Printf("Failed to construct expiry message for \"%s\": %s", user.Name, err)
} else if err := app.sendByID(msg, user.ID); err != nil {
app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, name, err)
app.err.Printf("Failed to send expiry message to \"%s\": %s", name, err)
} else {
app.err.Printf(lm.SentExpiryMessage, user.ID, name)
app.info.Printf("Sent expiry notification to \"%s\"", name)
}
}
}

223
users.go
View File

@ -1,223 +0,0 @@
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3"
)
// ReponseFunc responds to the user, generally by HTTP response
// The cases when more than this occurs are given below.
type ResponseFunc func(gc *gin.Context)
// LogFunc prints a log line once called.
type LogFunc func()
type ContactMethodConf struct {
Email, Discord, Telegram, Matrix bool
}
type ContactMethodUsers struct {
Email emailStore
Discord DiscordUser
Telegram TelegramVerifiedToken
Matrix MatrixUser
}
type ContactMethodValidation struct {
Verified ContactMethodConf
Users ContactMethodUsers
}
type NewUserParams struct {
Req newUserDTO
SourceType ActivitySource
Source string
ContextForIPLogging *gin.Context
Profile *Profile
}
type NewUserData struct {
Created bool
Success bool
User mediabrowser.User
Message string
Status int
Log func()
}
// Called after a new-user-creating route has done pre-steps (veryfing contact methods for example).
func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData) {
// Some helper functions which will behave as our app.info/error/debug
deferLogInfo := func(s string, args ...any) {
out.Log = func() {
app.info.Printf(s, args)
}
}
/* deferLogDebug := func(s string, args ...any) {
out.Log = func() {
app.debug.Printf(s, args)
}
} */
deferLogError := func(s string, args ...any) {
out.Log = func() {
app.err.Printf(s, args)
}
}
existingUser, _, _ := app.jf.UserByName(p.Req.Username, false)
if existingUser.Name != "" {
out.Message = lm.UserExists
deferLogInfo(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
out.Status = 401
return
}
var status int
var err error
out.User, status, err = app.jf.NewUser(p.Req.Username, p.Req.Password)
if !(status == 200 || status == 204) || err != nil {
out.Message = err.Error()
deferLogError(lm.FailedCreateUser, lm.Jellyfin, p.Req.Username, out.Message)
out.Status = 401
return
}
out.Created = true
// Invalidate Cache to be safe
app.jf.CacheExpiry = time.Now()
app.storage.SetActivityKey(shortuuid.New(), Activity{
Type: ActivityCreation,
UserID: out.User.ID,
SourceType: p.SourceType,
Source: p.Source,
InviteCode: p.Req.Code, // Left blank when an admin does this
Value: out.User.Name,
Time: time.Now(),
}, p.ContextForIPLogging, (p.SourceType != ActivityAdmin))
if p.Profile != nil {
status, err = app.jf.SetPolicy(out.User.ID, p.Profile.Policy)
if !((status == 200 || status == 204) && err == nil) {
app.err.Printf(lm.FailedApplyTemplate, "policy", lm.Jellyfin, out.User.ID, err)
}
status, err = app.jf.SetConfiguration(out.User.ID, p.Profile.Configuration)
if (status == 200 || status == 204) && err == nil {
status, err = app.jf.SetDisplayPreferences(out.User.ID, p.Profile.Displayprefs)
}
if !((status == 200 || status == 204) && err == nil) {
app.err.Printf(lm.FailedApplyTemplate, "configuration", lm.Jellyfin, out.User.ID, err)
}
for _, tps := range app.thirdPartyServices {
if !tps.Enabled(app, p.Profile) {
continue
}
// When ok and err != nil, its a non-fatal failure that we lot without the "FailedImportUser".
err, ok := tps.ImportUser(out.User.ID, p.Req, *p.Profile)
if !ok {
app.err.Printf(lm.FailedImportUser, tps.Name(), p.Req.Username, err)
} else if err != nil {
app.info.Println(err)
}
}
}
// Welcome email is sent by each user of this method separately..
out.Status = 200
out.Success = true
return
}
func (app *appContext) WelcomeNewUser(user mediabrowser.User, expiry time.Time) (failed bool) {
if !app.config.Section("welcome_email").Key("enabled").MustBool(false) {
// we didn't "fail", we "politely declined"
// failed = true
return
}
failed = true
name := app.getAddressOrName(user.ID)
if name == "" {
return
}
msg, err := app.email.constructWelcome(user.Name, expiry, app, false)
if err != nil {
app.err.Printf(lm.FailedConstructWelcomeMessage, user.ID, err)
} else if err := app.sendByID(msg, user.ID); err != nil {
app.err.Printf(lm.FailedSendWelcomeMessage, user.ID, name, err)
} else {
app.info.Printf(lm.SentWelcomeMessage, user.ID, name)
failed = false
}
return
}
func (app *appContext) SetUserDisabled(user mediabrowser.User, disabled bool) (err error, change bool, activityType ActivityType) {
activityType = ActivityEnabled
if disabled {
activityType = ActivityDisabled
}
change = user.Policy.IsDisabled != disabled
user.Policy.IsDisabled = disabled
var status int
status, err = app.jf.SetPolicy(user.ID, user.Policy)
if !(status == 200 || status == 204) && err == nil {
err = fmt.Errorf("failed (code %d)", status)
}
if err != nil {
return
}
if app.discord != nil && app.config.Section("discord").Key("disable_enable_role").MustBool(false) {
cmUser, ok := app.storage.GetDiscordKey(user.ID)
if ok {
if err := app.discord.SetRoleDisabled(cmUser.MethodID().(string), disabled); err != nil {
app.err.Printf(lm.FailedSetDiscordMemberRole, err)
}
}
}
return
}
func (app *appContext) DeleteUser(user mediabrowser.User) (err error, deleted bool) {
var status int
if app.ombi != nil {
var tpUser map[string]any
tpUser, status, err = app.getOmbiUser(user.ID)
if status == 200 && err == nil {
if id, ok := tpUser["id"]; ok {
status, err = app.ombi.DeleteUser(id.(string))
if status != 200 && err == nil {
err = fmt.Errorf("failed (code %d)", status)
}
if err != nil {
app.err.Printf(lm.FailedDeleteUser, lm.Ombi, user.ID, err)
}
}
}
}
if app.discord != nil && app.config.Section("discord").Key("disable_enable_role").MustBool(false) {
cmUser, ok := app.storage.GetDiscordKey(user.ID)
if ok {
if err := app.discord.RemoveRole(cmUser.MethodID().(string)); err != nil {
app.err.Printf(lm.FailedSetDiscordMemberRole, err)
}
}
}
status, err = app.jf.DeleteUser(user.ID)
if status != 200 && status != 204 && err == nil {
err = fmt.Errorf("failed (code %d)", status)
}
if err != nil {
return
}
deleted = true
return
}

243
views.go
View File

@ -4,7 +4,6 @@ import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"html/template"
"io"
"io/fs"
@ -17,7 +16,6 @@ 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"
@ -68,9 +66,10 @@ 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(lm.FailedServerPush, err)
app.debug.Printf("Failed HTTP2 ServerPush of \"%s\": %+v", f, err)
}
}
}
@ -140,13 +139,13 @@ func (app *appContext) AdminPage(gc *gin.Context) {
var license string
l, err := fs.ReadFile(localFS, "LICENSE")
if err != nil {
app.debug.Printf(lm.FailedReading, "LICENSE", err)
app.debug.Printf("Failed to load LICENSE: %s", err)
license = ""
}
license = string(l)
fontLicense, err := fs.ReadFile(localFS, filepath.Join("web", "fonts", "OFL.txt"))
if err != nil {
app.debug.Printf(lm.FailedReading, "fontLicense", err)
app.debug.Printf("Failed to load OFL.txt: %s", err)
}
license += "---Hanken Grotesk---\n\n"
@ -237,7 +236,7 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
"server_channel": app.discord.serverChannelName,
}))
data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
data["discordInviteLink"] = app.discord.inviteChannelName != ""
}
if data["linkResetEnabled"].(bool) {
data["resetPasswordUsername"] = app.config.Section("user_page").Key("allow_pwr_username").MustBool(true)
@ -313,7 +312,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
defer gcHTML(gc, http.StatusOK, "password-reset.html", data)
// If it's a bot, pretend to be a success so the preview is nice.
if isBot {
app.debug.Println(lm.IgnoreBotPWR)
app.debug.Println("PWR: Ignoring magic link visit from bot")
data["success"] = true
data["pin"] = "NO-BO-TS"
return
@ -339,13 +338,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(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, pin))
app.debug.Printf("Ignoring PWR request due to expired internal PIN: %s", pin)
app.NoRouteHandler(gc)
return
} else {
status, err = app.jf.ResetPasswordAdmin(pwr.ID)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", err)
app.err.Printf("Password Reset failed (%d): %v", status, err)
} else {
status, err = app.jf.SetPassword(pwr.ID, "", pin)
}
@ -359,7 +358,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
username = resp.UsersReset[0]
}
} else {
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", err)
app.err.Printf("Password Reset failed (%d): %v", status, err)
}
// Only log PWRs we know the user for.
@ -379,21 +378,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(lm.FailedGetUser, username, lm.Jellyfin, err)
app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err)
return
}
ombiUser, status, err := app.getOmbiUser(jfUser.ID)
if status != 200 || err != nil {
app.err.Printf(lm.FailedGetUser, username, lm.Ombi, err)
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", username, status, err)
return
}
ombiUser["password"] = pin
status, err = app.ombi.ModifyUser(ombiUser)
if status != 200 || err != nil {
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
return
}
app.debug.Printf(lm.ChangePassword, lm.Ombi, ombiUser["userName"])
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
}
}
@ -461,7 +460,7 @@ func (app *appContext) GenCaptcha(gc *gin.Context) {
}
capt, err := captcha.New(300, 100)
if err != nil {
app.err.Printf(lm.FailedGenerateCaptcha, err)
app.err.Printf("Failed to generate captcha: %v", err)
respondBool(500, false, gc)
return
}
@ -471,7 +470,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(lm.FailedGenerateCaptcha, err)
app.err.Printf("Failed to render captcha: %v", err)
respondBool(500, false, gc)
return
}
@ -504,12 +503,8 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
ok := true
if !isPWR {
inv, ok := app.storage.GetInvitesKey(code)
if !ok {
app.debug.Printf(lm.InvalidInviteCode, code)
return false
}
if !isPWR && inv.Captchas == nil {
app.debug.Printf(lm.CaptchaNotFound, id, code)
if !ok || (!isPWR && inv.Captchas == nil) {
app.debug.Printf("Couldn't find invite \"%s\"", code)
return false
}
c, ok = inv.Captchas[id]
@ -517,7 +512,7 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
c, ok = app.pwrCaptchas[code]
}
if !ok {
app.debug.Printf(lm.CaptchaNotFound, id, code)
app.debug.Printf("Couldn't find Captcha \"%s\"", id)
return false
}
return strings.ToLower(c.Answer) == strings.ToLower(text)
@ -539,11 +534,8 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err == nil && resp.StatusCode != 200 {
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
}
if err != nil {
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
if err != nil || resp.StatusCode != 200 {
app.err.Printf("Failed to read reCAPTCHA status (%d): %+v\n", resp.Status, err)
return false
}
defer resp.Body.Close()
@ -551,19 +543,18 @@ func (app *appContext) verifyCaptcha(code, id, text string, isPWR bool) bool {
body, err := io.ReadAll(resp.Body)
err = json.Unmarshal(body, &data)
if err != nil {
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
app.err.Printf("Failed to unmarshal reCAPTCHA response: %+v\n", err)
return false
}
hostname := app.config.Section("captcha").Key("recaptcha_hostname").MustString("")
if strings.ToLower(data.Hostname) != strings.ToLower(hostname) && data.Hostname != "" {
err = fmt.Errorf(lm.InvalidHostname, hostname, data.Hostname)
app.err.Printf(lm.FailedVerifyReCAPTCHA, err)
app.debug.Printf("Invalidating reCAPTCHA request: Hostnames didn't match (Wanted \"%s\", got \"%s\"\n", hostname, data.Hostname)
return false
}
if len(data.ErrorCodes) > 0 {
app.err.Printf(lm.AdditionalErrors, lm.ReCAPTCHA, data.ErrorCodes)
app.err.Printf("reCAPTCHA returned errors: %+v\n", data.ErrorCodes)
return false
}
@ -616,108 +607,13 @@ func (app *appContext) VerifyCaptcha(gc *gin.Context) {
return
}
func (app *appContext) NewUserFromConfirmationKey(invite Invite, key string, lang string, gc *gin.Context) {
fail := func() {
gcHTML(gc, 404, "404.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
var req newUserDTO
if app.ConfirmationKeys == nil {
fail()
return
}
invKeys, ok := app.ConfirmationKeys[invite.Code]
if !ok {
fail()
return
}
req, ok = invKeys[key]
if !ok {
fail()
return
}
token, err := jwt.Parse(key, checkToken)
if err != nil {
fail()
app.debug.Printf(lm.FailedParseJWT, err)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
if !(ok && token.Valid && claims["invite"].(string) == invite.Code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
fail()
app.debug.Printf(lm.InvalidJWT)
return
}
sourceType, source := invite.Source()
var profile *Profile = nil
if invite.Profile != "" {
p, ok := app.storage.GetProfileKey(invite.Profile)
if !ok {
app.debug.Printf(lm.FailedGetProfile+lm.FallbackToDefault, invite.Profile)
p = app.storage.GetDefaultProfile()
}
profile = &p
}
nu := app.NewUserPostVerification(NewUserParams{
Req: req,
SourceType: sourceType,
Source: source,
ContextForIPLogging: gc,
Profile: profile,
})
if !nu.Success {
nu.Log()
}
if !nu.Created {
respond(nu.Status, nu.Message, gc)
fail()
return
}
app.checkInvite(req.Code, true, req.Username)
jfLink := app.config.Section("ui").Key("redirect_url").String()
if app.config.Section("ui").Key("auto_redirect").MustBool(false) {
gc.Redirect(301, jfLink)
} else {
gcHTML(gc, http.StatusOK, "create-success.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"strings": app.storage.lang.User[lang].Strings,
"successMessage": app.config.Section("ui").Key("success_message").String(),
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"jfLink": jfLink,
})
}
app.confirmationKeysLock.Lock()
// Re-fetch invKeys just incase an update occurred
invKeys, ok = app.ConfirmationKeys[invite.Code]
if !ok {
fail()
return
}
delete(invKeys, key)
app.ConfirmationKeys[invite.Code] = invKeys
app.confirmationKeysLock.Unlock()
return
}
func (app *appContext) InviteProxy(gc *gin.Context) {
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, "") {
invite, ok := app.storage.GetInvitesKey(gc.Param("invCode"))
inv, ok := app.storage.GetInvitesKey(code)
if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{
"urlBase": app.getURLBase(gc),
@ -727,13 +623,76 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
})
return
}
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
app.NewUserFromConfirmationKey(invite, key, lang, gc)
fail := func() {
gcHTML(gc, 404, "404.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
var req newUserDTO
if app.ConfirmationKeys == nil {
fail()
return
}
invKeys, ok := app.ConfirmationKeys[code]
if !ok {
fail()
return
}
req, ok = invKeys[key]
if !ok {
fail()
return
}
token, err := jwt.Parse(key, checkToken)
if err != nil {
fail()
app.err.Printf("Failed to parse key: %s", err)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
if !(ok && token.Valid && claims["invite"].(string) == code && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
fail()
app.debug.Printf("Invalid key")
return
}
f, success := app.newUser(req, true, gc)
if !success {
app.err.Printf("Failed to create new user")
// Not meant for us. Calling this will be a mess, but at least it might give us some information.
f(gc)
fail()
return
}
jfLink := app.config.Section("ui").Key("redirect_url").String()
if app.config.Section("ui").Key("auto_redirect").MustBool(false) {
gc.Redirect(301, jfLink)
} else {
gcHTML(gc, http.StatusOK, "create-success.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"strings": app.storage.lang.User[lang].Strings,
"successMessage": app.config.Section("ui").Key("success_message").String(),
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"jfLink": jfLink,
})
}
delete(invKeys, key)
app.confirmationKeysLock.Lock()
app.ConfirmationKeys[code] = invKeys
app.confirmationKeysLock.Unlock()
return
}
email := invite.SendTo
email := ""
if invite, ok := app.storage.GetInvitesKey(code); ok {
email = invite.SendTo
}
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
email = ""
}
@ -748,8 +707,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
userPageAddress += "/my/account"
fromUser := ""
if invite.ReferrerJellyfinID != "" {
sender, status, err := app.jf.UserByID(invite.ReferrerJellyfinID, false)
if inv.ReferrerJellyfinID != "" {
sender, status, err := app.jf.UserByID(inv.ReferrerJellyfinID, false)
if status == 200 && err == nil {
fromUser = sender.Name
}
@ -771,13 +730,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": invite.Code,
"code": code,
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
"userExpiry": invite.UserExpiry,
"userExpiryMonths": invite.UserMonths,
"userExpiryDays": invite.UserDays,
"userExpiryHours": invite.UserHours,
"userExpiryMinutes": invite.UserMinutes,
"userExpiry": inv.UserExpiry,
"userExpiryMonths": inv.UserMonths,
"userExpiryDays": inv.UserDays,
"userExpiryHours": inv.UserHours,
"userExpiryMinutes": inv.UserMinutes,
"userExpiryMessage": app.storage.lang.User[lang].Strings.get("yourAccountIsValidUntil"),
"langName": lang,
"passwordReset": false,
@ -812,7 +771,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"server_channel": app.discord.serverChannelName,
}))
data["discordServerName"] = app.discord.serverName
data["discordInviteLink"] = app.discord.InviteChannel.Name != ""
data["discordInviteLink"] = app.discord.inviteChannelName != ""
}
if msg, ok := app.storage.GetCustomContentKey("PostSignupCard"); ok && msg.Enabled {
data["customSuccessCard"] = true