1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-06-25 19:07:47 +02:00

Compare commits

...

35 Commits

Author SHA1 Message Date
dependabot[bot]
1b1dba2baa
Merge 97506c0bcd into 3143d32b45 2023-10-12 14:35:13 -07:00
3143d32b45
log: include caller in debug storage logging
includes the location where Set*Key/Delete*Key was called.
2023-10-12 18:21:47 +01:00
742f5c095a
log: add basic database write debug logging
A series of settings can be found in Settings > Advanced for logging
writes to the database, for each main storage object. "All" logs all
writes, "Deletion" logs Delete* Calls and Write* calls where the
principal data in the object (e.g. address in an EmailAddress object) is
set to "".
2023-10-12 18:12:18 +01:00
7b2a6cdf74
discord: merge /inv from @VioletLeporid
Adds the /inv command to send an invite directly to a Discord user.
2023-10-12 09:25:33 +01:00
2f3d5e4e3a
discord: update profile list when changes occur 2023-10-11 12:00:38 +01:00
2fb2f3ee74
discord: send error message when inv construction fails 2023-10-11 11:38:55 +01:00
7813c8c68b
discord: Use GenerateInviteCode in /inv 2023-10-11 11:35:08 +01:00
e528f7c348
Merge latest changes 2023-10-11 11:33:51 +01:00
77f6b1042e
invites: move code gen to function
code to generate an invite code w/ a non-integer first character was
reused a bunch, so it's now function GenerateInviteCode().
2023-10-11 11:30:28 +01:00
7db94dcebf
Merge /inv command additions
Merge branch 'main' of github.com:VioletLeporid/jfa-go
2023-10-11 11:25:00 +01:00
Violet Scheen
70afc21217
Merge branch 'hrfee:main' into main 2023-10-10 13:51:51 -04:00
Violet Scheen
525c13ff6a
Update discord.go 2023-10-10 12:55:56 -04:00
Violet Scheen
0366e5116d
Update discord.go
Cleaning up a bit
2023-10-10 11:14:57 -04:00
62923d5e45
discord: register available profiles for /inv
profiles are registered as options for /inv as startup. Note in
description added to restart jfa-go to reload them.
2023-10-10 15:15:25 +01:00
10a32ad1ae
discord: re-add optional args 2023-10-10 14:52:54 +01:00
e52e21a54b
discord: fix up /inv basic functionality
sending now succeeds, and a reponse of "Invite sent." is given to the
requester. Also some formatting changes.
2023-10-10 13:45:29 +01:00
7c861e5763
lang: fix the usual mistakes
someone directly translating "English (US)", and lowercasing lang files.
2023-10-10 10:36:57 +01:00
9c771e193e
lang: fix typo in french
`{n]` instead of `{n}` meant expiry times on the user page weren't being
rendered.
2023-10-10 10:22:22 +01:00
Killianbe
f37451021f translation from Weblate (French)
Currently translated at 100.0% (188 of 188 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/fr/
2023-10-10 11:18:21 +02:00
Killianbe
4aa095d466 Translated using Weblate (French)
Currently translated at 92.5% (111 of 120 strings)

Translation: jfa-go/Setup
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/setup/fr/
2023-10-10 11:18:21 +02:00
Killianbe
638be18ea8 Translated using Weblate (French)
Currently translated at 100.0% (51 of 51 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/common-strings/fr/
2023-10-10 11:18:21 +02:00
Killianbe
42264f0547 translation from Weblate (French)
Currently translated at 100.0% (62 of 62 strings)

Translation: jfa-go/Account Creation Form
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/form/fr/
2023-10-10 11:18:21 +02:00
Killianbe
07d738006f translation from Weblate (French)
Currently translated at 96.8% (182 of 188 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/fr/
2023-10-10 11:18:21 +02:00
Anton B
4bc51570c2 Translated using Weblate (Swedish)
Currently translated at 82.3% (42 of 51 strings)

Translation: jfa-go/Common Strings
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/common-strings/sv/
2023-10-10 11:18:21 +02:00
cf94fdb2f0
ombi: fix password reset on default route
the ombi password wasn't being changed w/ password resets initiated
through the admin page (and probably by other routes, too), as the code
was considering a HTTP 204 from Jellyfin as a failure, causing it to
skip anything with Ombi. Also added a little check for Ombi-imported
accounts that probably won't help with anything, but whatever.
2023-10-09 10:40:40 +01:00
4864c6c53c
ombi: implement getOmbiImportedUser 2023-10-06 14:41:33 +01:00
Violet Scheen
dd93758b0e
Update discord.go 2023-10-03 23:59:42 -04:00
Violet Scheen
b595d3ea03
Update discord.go 2023-10-03 23:37:24 -04:00
Violet Scheen
49dfac514d
Update discord.go 2023-10-03 23:22:20 -04:00
Violet Scheen
729548334d
Update discord.go 2023-09-30 12:16:06 -04:00
Violet Scheen
27f85f866e
Update discord.go
Hopefully functional, any errors are coming from elsewhere
2023-09-30 12:10:38 -04:00
Violet Scheen
c43d5cf1b0
Update discord.go 2023-09-30 11:25:36 -04:00
HekeHokkus
3538935d3b
Update discord.go
Adding /invite command and Discord status message to the bot
2023-09-28 19:37:35 -04:00
HekeHokkus
edf6c13f03
Update discord.go 2023-09-28 17:55:47 -04:00
HekeHokkus
b30d6c3ee1
Update discord.go 2023-09-28 15:54:48 -04:00
30 changed files with 600 additions and 126 deletions

View File

@ -17,6 +17,18 @@ const (
CAPTCHA_VALIDITY = 20 * 60 // Seconds CAPTCHA_VALIDITY = 20 * 60 // Seconds
) )
// GenerateInviteCode generates an invite code in the correct format.
func GenerateInviteCode() string {
// make sure code doesn't begin with number
inviteCode := shortuuid.New()
_, err := strconv.Atoi(string(inviteCode[0]))
for err == nil {
inviteCode = shortuuid.New()
_, err = strconv.Atoi(string(inviteCode[0]))
}
return inviteCode
}
func (app *appContext) checkInvites() { func (app *appContext) checkInvites() {
currentTime := time.Now() currentTime := time.Now()
for _, data := range app.storage.GetInvites() { for _, data := range app.storage.GetInvites() {
@ -150,14 +162,8 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
currentTime := time.Now() currentTime := time.Now()
validTill := currentTime.AddDate(0, req.Months, req.Days) validTill := currentTime.AddDate(0, req.Months, req.Days)
validTill = validTill.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes)) validTill = validTill.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes))
// make sure code doesn't begin with number
inviteCode := shortuuid.New()
_, err := strconv.Atoi(string(inviteCode[0]))
for err == nil {
inviteCode = shortuuid.New()
_, err = strconv.Atoi(string(inviteCode[0]))
}
var invite Invite var invite Invite
invite.Code = GenerateInviteCode()
if req.Label != "" { if req.Label != "" {
invite.Label = req.Label invite.Label = req.Label
} }
@ -185,7 +191,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
addressValid := false addressValid := false
discord := "" discord := ""
app.debug.Printf("%s: Sending invite message", inviteCode) app.debug.Printf("%s: Sending invite message", invite.Code)
if discordEnabled && !strings.Contains(req.SendTo, "@") { if discordEnabled && !strings.Contains(req.SendTo, "@") {
users := app.discord.GetUsers(req.SendTo) users := app.discord.GetUsers(req.SendTo)
if len(users) == 0 { if len(users) == 0 {
@ -202,10 +208,10 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
invite.SendTo = req.SendTo invite.SendTo = req.SendTo
} }
if addressValid { if addressValid {
msg, err := app.email.constructInvite(inviteCode, invite, app, false) msg, err := app.email.constructInvite(invite.Code, invite, app, false)
if err != nil { if err != nil {
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo) invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: Failed to construct invite message: %v", inviteCode, err) app.err.Printf("%s: Failed to construct invite message: %v", invite.Code, err)
} else { } else {
var err error var err error
if discord != "" { if discord != "" {
@ -215,9 +221,9 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
} }
if err != nil { if err != nil {
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo) invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
app.err.Printf("%s: %s: %v", inviteCode, invite.SendTo, err) app.err.Printf("%s: %s: %v", invite.Code, invite.SendTo, err)
} else { } else {
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.SendTo) app.info.Printf("%s: Sent invite email to \"%s\"", invite.Code, req.SendTo)
} }
} }
} }
@ -229,7 +235,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
invite.Profile = "Default" invite.Profile = "Default"
} }
} }
app.storage.SetInvitesKey(inviteCode, invite) app.storage.SetInvitesKey(invite.Code, invite)
respondBool(200, true, gc) respondBool(200, true, gc)
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hrfee/mediabrowser"
) )
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) { func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
@ -29,7 +30,31 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
return ombiUser, code, err return ombiUser, code, err
} }
} }
return nil, 400, fmt.Errorf("Couldn't find user") return nil, 400, fmt.Errorf("couldn't find user")
}
// Returns a user with the given name who has been imported from Jellyfin/Emby by Ombi
func (app *appContext) getOmbiImportedUser(name string) (map[string]interface{}, int, error) {
// Ombi User Types: 3/4 = Emby, 5 = Jellyfin
ombiUsers, code, err := app.ombi.GetUsers()
if err != nil || code != 200 {
return nil, code, err
}
for _, ombiUser := range ombiUsers {
if ombiUser["userName"].(string) == name {
uType, ok := ombiUser["userType"].(int)
if !ok { // Don't know if Ombi somehow allows duplicate usernames
continue
}
if serverType == mediabrowser.JellyfinServer && uType != 5 { // Jellyfin
continue
} else if uType != 3 && uType != 4 { // Emby
continue
}
return ombiUser, code, err
}
}
return nil, 400, fmt.Errorf("couldn't find user")
} }
// @Summary Get a list of Ombi users. // @Summary Get a list of Ombi users.
@ -107,3 +132,18 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
app.storage.SetProfileKey(profileName, profile) app.storage.SetProfileKey(profileName, profile)
respondBool(204, true, gc) respondBool(204, true, gc)
} }
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{}:
user[k] = v
default:
if v != user[k] {
user[k] = v
}
}
}
status, err = app.ombi.ModifyUser(user)
return
}

View File

@ -1,11 +1,9 @@
package main package main
import ( import (
"strconv"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
) )
@ -106,6 +104,10 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
} }
} }
app.storage.SetProfileKey(req.Name, profile) app.storage.SetProfileKey(req.Name, profile)
// Refresh discord bots, profile list
if discordEnabled {
app.discord.UpdateCommands()
}
respondBool(200, true, gc) respondBool(200, true, gc)
} }
@ -151,13 +153,7 @@ func (app *appContext) EnableReferralForProfile(gc *gin.Context) {
} }
// Generate new code for referral template // Generate new code for referral template
inv.Code = shortuuid.New() inv.Code = GenerateInviteCode()
// make sure code doesn't begin with number
_, err := strconv.Atoi(string(inv.Code[0]))
for err == nil {
inv.Code = shortuuid.New()
_, err = strconv.Atoi(string(inv.Code[0]))
}
inv.Created = time.Now() inv.Created = time.Now()
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour) inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
inv.IsReferral = true inv.IsReferral = true

View File

@ -3,13 +3,11 @@ package main
import ( import (
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
) )
@ -673,13 +671,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
respondBool(400, false, gc) respondBool(400, false, gc)
return return
} }
inv.Code = shortuuid.New() inv.Code = GenerateInviteCode()
// make sure code doesn't begin with number
_, err := strconv.Atoi(string(inv.Code[0]))
for err == nil {
inv.Code = shortuuid.New()
_, err = strconv.Atoi(string(inv.Code[0]))
}
inv.Created = time.Now() inv.Created = time.Now()
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour) inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
inv.IsReferral = true inv.IsReferral = true
@ -689,13 +681,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
// 3. We found an invite for us, but it's expired. // 3. We found an invite for us, but it's expired.
// We delete it from storage, and put it back with a fresh code and expiry. // We delete it from storage, and put it back with a fresh code and expiry.
app.storage.DeleteInvitesKey(inv.Code) app.storage.DeleteInvitesKey(inv.Code)
inv.Code = shortuuid.New() inv.Code = GenerateInviteCode()
// make sure code doesn't begin with number
_, err := strconv.Atoi(string(inv.Code[0]))
for err == nil {
inv.Code = shortuuid.New()
_, err = strconv.Atoi(string(inv.Code[0]))
}
inv.Created = time.Now() inv.Created = time.Now()
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour) inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
app.storage.SetInvitesKey(inv.Code, inv) app.storage.SetInvitesKey(inv.Code, inv)

View File

@ -3,14 +3,12 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
) )
@ -377,30 +375,47 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
if profile.Ombi != nil && len(profile.Ombi) != 0 { if profile.Ombi != nil && len(profile.Ombi) != 0 {
template := profile.Ombi template := profile.Ombi
errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, template) errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, template)
accountExists := false
var ombiUser map[string]interface{}
if err != nil || code != 200 { if err != nil || code != 200 {
app.info.Printf("Failed to create Ombi user (%d): %s", code, err) // Check if on the off chance, Ombi's user importer has already added the account.
app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", ")) ombiUser, status, err = app.getOmbiImportedUser(req.Username)
} else { if status == 200 && err == nil {
app.info.Println("Created Ombi user") app.info.Println("Found existing Ombi user, applying changes")
if discordVerified || telegramVerified { accountExists = true
ombiUser, status, err := app.getOmbiUser(id) template["password"] = req.Password
status, err = app.applyOmbiProfile(ombiUser, template)
if status != 200 || err != nil { if status != 200 || err != nil {
app.err.Printf("Failed to get Ombi user (%d): %v", status, err) app.err.Printf("Failed to modify existing Ombi user (%d): %v\n", status, err)
} else { }
dID := "" } else {
tUser := "" app.info.Printf("Failed to create Ombi user (%d): %s", code, err)
if discordVerified { app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", "))
dID = discordUser.ID }
} } else {
if telegramVerified { ombiUser, status, err = app.getOmbiUser(id)
u, _ := app.storage.GetTelegramKey(user.ID) if status != 200 || err != nil {
tUser = u.Username app.err.Printf("Failed to get Ombi user (%d): %v", status, err)
} } else {
resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser) app.info.Println("Created Ombi user")
if !(status == 200 || status == 204) || err != nil { accountExists = true
app.err.Printf("Failed to link Telegram/Discord to Ombi (%d): %v", status, err) }
app.debug.Printf("Response: %v", resp) }
} if accountExists {
if discordVerified || telegramVerified {
dID := ""
tUser := ""
if discordVerified {
dID = discordUser.ID
}
if telegramVerified {
u, _ := app.storage.GetTelegramKey(user.ID)
tUser = u.Username
}
resp, status, err := app.ombi.SetNotificationPrefs(ombiUser, dID, tUser)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Failed to link Telegram/Discord to Ombi (%d): %v", status, err)
app.debug.Printf("Response: %v", resp)
} }
} }
} }
@ -690,13 +705,7 @@ func (app *appContext) EnableReferralForUsers(gc *gin.Context) {
// 2. Generate referral invite. // 2. Generate referral invite.
inv := baseInv inv := baseInv
inv.Code = shortuuid.New() inv.Code = GenerateInviteCode()
// make sure code doesn't begin with number
_, err := strconv.Atoi(string(inv.Code[0]))
for err == nil {
inv.Code = shortuuid.New()
_, err = strconv.Atoi(string(inv.Code[0]))
}
inv.Created = time.Now() inv.Created = time.Now()
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour) inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
inv.IsReferral = true inv.IsReferral = true
@ -1214,17 +1223,7 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
// newUser["userName"] = user["userName"] // newUser["userName"] = user["userName"]
// newUser["alias"] = user["alias"] // newUser["alias"] = user["alias"]
// newUser["emailAddress"] = user["emailAddress"] // newUser["emailAddress"] = user["emailAddress"]
for k, v := range ombi { status, err = app.applyOmbiProfile(user, ombi)
switch v.(type) {
case map[string]interface{}, []interface{}:
user[k] = v
default:
if v != user[k] {
user[k] = v
}
}
}
status, err = app.ombi.ModifyUser(user)
if status != 200 || err != nil { if status != 200 || err != nil {
errorString += fmt.Sprintf("Apply %d: %v ", status, err) errorString += fmt.Sprintf("Apply %d: %v ", status, err)
} }

2
api.go
View File

@ -182,7 +182,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
} }
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
// Silently fail for changing ombi passwords // Silently fail for changing ombi passwords
if status != 200 || err != nil { if (status != 200 && status != 204) || err != nil {
app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err) app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err)
respondBool(200, true, gc) respondBool(200, true, gc)
return return

View File

@ -395,6 +395,123 @@
"type": "password", "type": "password",
"value": "", "value": "",
"description": "Leave blank for no Authentication." "description": "Leave blank for no Authentication."
},
"debug_log_emails": {
"name": "Debug Storage Logging: Emails",
"required": false,
"requires_restart": true,
"type": "select",
"options": [
["none", "None"],
["all", "All Writes"],
["deletion", "Deletion Only*"]
],
"value": "none",
"description": "Extra debug logging for writes to the database. *: Deletion also includes blanking out major fields, e.g. an email address."
},
"debug_log_discord": {
"name": "Debug Storage Logging: Discord",
"required": false,
"requires_restart": true,
"type": "select",
"options": [
["none", "None"],
["all", "All Writes"],
["deletion", "Deletion Only*"]
],
"value": "none",
"description": "Extra debug logging for writes to the database. *: Deletion also includes blanking out major fields, e.g. an email address."
},
"debug_log_telegram": {
"name": "Debug Storage Logging: Telegram",
"required": false,
"requires_restart": true,
"type": "select",
"options": [
["none", "None"],
["all", "All Writes"],
["deletion", "Deletion Only*"]
],
"value": "none",
"description": "Extra debug logging for writes to the database. *: Deletion also includes blanking out major fields, e.g. an email address."
},
"debug_log_matrix": {
"name": "Debug Storage Logging: Matrix",
"required": false,
"requires_restart": true,
"type": "select",
"options": [
["none", "None"],
["all", "All Writes"],
["deletion", "Deletion Only*"]
],
"value": "none",
"description": "Extra debug logging for writes to the database. *: Deletion also includes blanking out major fields, e.g. an email address."
},
"debug_log_invites": {
"name": "Debug Storage Logging: Invites",
"required": false,
"requires_restart": true,
"type": "select",
"options": [
["none", "None"],
["all", "All Writes"],
["deletion", "Deletion Only*"]
],
"value": "none",
"description": "Extra debug logging for writes to the database. *: Deletion also includes blanking out major fields, e.g. an email address."
},
"debug_log_announcements": {
"name": "Debug Storage Logging: Announcements",
"required": false,
"requires_restart": true,
"type": "select",
"options": [
["none", "None"],
["all", "All Writes"],
["deletion", "Deletion Only*"]
],
"value": "none",
"description": "Extra debug logging for writes to the database. *: Deletion also includes blanking out major fields, e.g. an email address."
},
"debug_log_expiries": {
"name": "Debug Storage Logging: User Expiries",
"required": false,
"requires_restart": true,
"type": "select",
"options": [
["none", "None"],
["all", "All Writes"],
["deletion", "Deletion Only*"]
],
"value": "none",
"description": "Extra debug logging for writes to the database. *: Deletion also includes blanking out major fields, e.g. an email address."
},
"debug_log_profiles": {
"name": "Debug Storage Logging: Profiles",
"required": false,
"requires_restart": true,
"type": "select",
"options": [
["none", "None"],
["all", "All Writes"],
["deletion", "Deletion Only*"]
],
"value": "none",
"description": "Extra debug logging for writes to the database. *: Deletion also includes blanking out major fields, e.g. an email address."
},
"debug_log_custom_content": {
"name": "Debug Storage Logging: Custom Message Content",
"required": false,
"requires_restart": true,
"type": "select",
"options": [
["none", "None"],
["all", "All Writes"],
["deletion", "Deletion Only*"]
],
"value": "none",
"description": "Extra debug logging for writes to the database. *: Deletion also includes blanking out major fields, e.g. an email address."
} }
} }
}, },

View File

@ -24,6 +24,7 @@ type DiscordDaemon struct {
app *appContext app *appContext
commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string) commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string)
commandIDs []string commandIDs []string
commandDescriptions []*dg.ApplicationCommand
} }
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) { func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
@ -50,6 +51,7 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
dd.commandHandlers[app.config.Section("discord").Key("start_command").MustString("start")] = dd.cmdStart dd.commandHandlers[app.config.Section("discord").Key("start_command").MustString("start")] = dd.cmdStart
dd.commandHandlers["lang"] = dd.cmdLang dd.commandHandlers["lang"] = dd.cmdLang
dd.commandHandlers["pin"] = dd.cmdPIN dd.commandHandlers["pin"] = dd.cmdPIN
dd.commandHandlers["inv"] = dd.cmdInvite
for _, user := range app.storage.GetDiscord() { for _, user := range app.storage.GetDiscord() {
dd.users[user.ID] = user dd.users[user.ID] = user
} }
@ -127,6 +129,7 @@ func (d *DiscordDaemon) run() {
d.inviteChannelName = invChannel d.inviteChannelName = invChannel
} }
} }
err = d.bot.UpdateGameStatus(0, "/"+d.app.config.Section("discord").Key("start_command").MustString("start"))
defer d.deregisterCommands() defer d.deregisterCommands()
defer d.bot.Close() defer d.bot.Close()
@ -220,7 +223,6 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
d.app.err.Printf("Discord: Failed to get guild: %v", err) d.app.err.Printf("Discord: Failed to get guild: %v", err)
return return
} }
// FIXME: Fix CSS, and handle no icon
iconURL = guild.IconURL("256") iconURL = guild.IconURL("256")
return return
} }
@ -308,7 +310,7 @@ func (d *DiscordDaemon) Shutdown() {
} }
func (d *DiscordDaemon) registerCommands() { func (d *DiscordDaemon) registerCommands() {
commands := []*dg.ApplicationCommand{ d.commandDescriptions = []*dg.ApplicationCommand{
{ {
Name: d.app.config.Section("discord").Key("start_command").MustString("start"), Name: d.app.config.Section("discord").Key("start_command").MustString("start"),
Description: "Start the Discord linking process. The bot will send further instructions.", Description: "Start the Discord linking process. The bot will send further instructions.",
@ -338,26 +340,73 @@ func (d *DiscordDaemon) registerCommands() {
}, },
}, },
}, },
{
Name: "inv",
Description: "Send an invite to a discord user (admin only).",
Options: []*dg.ApplicationCommandOption{
{
Type: dg.ApplicationCommandOptionUser,
Name: "user",
Description: "User to Invite.",
Required: true,
},
{
Type: dg.ApplicationCommandOptionInteger,
Name: "expiry",
Description: "Time in minutes before expiration.",
Required: false,
},
/* Label should be automatically set to something like "Discord invite for @username"
{
Type: dg.ApplicationCommandOptionString,
Name: "label",
Description: "Label given to this invite (shown on the Admin page)",
Required: false,
}, */
{
Type: dg.ApplicationCommandOptionString,
Name: "user_label",
Description: "Label given to users created with this invite.",
Required: false,
},
{
Type: dg.ApplicationCommandOptionString,
Name: "profile",
Description: "Profile to apply to the created user.",
Required: false,
},
},
},
} }
commands[1].Options[0].Choices = make([]*dg.ApplicationCommandOptionChoice, len(d.app.storage.lang.Telegram)) d.commandDescriptions[1].Options[0].Choices = make([]*dg.ApplicationCommandOptionChoice, len(d.app.storage.lang.Telegram))
i := 0 i := 0
for code := range d.app.storage.lang.Telegram { for code := range d.app.storage.lang.Telegram {
d.app.debug.Printf("Registering choice \"%s\":\"%s\"\n", 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)
commands[1].Options[0].Choices[i] = &dg.ApplicationCommandOptionChoice{ d.commandDescriptions[1].Options[0].Choices[i] = &dg.ApplicationCommandOptionChoice{
Name: d.app.storage.lang.Telegram[code].Meta.Name, Name: d.app.storage.lang.Telegram[code].Meta.Name,
Value: code, Value: code,
} }
i++ i++
} }
profiles := d.app.storage.GetProfiles()
d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(profiles))
for i, profile := range profiles {
d.app.debug.Printf("Discord: registering profile choice \"%s\"", profile.Name)
d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{
Name: profile.Name,
Value: profile.Name,
}
}
// d.deregisterCommands() // d.deregisterCommands()
d.commandIDs = make([]string, len(commands)) d.commandIDs = make([]string, len(d.commandDescriptions))
// cCommands, err := d.bot.ApplicationCommandBulkOverwrite(d.bot.State.User.ID, d.guildID, commands) // cCommands, err := d.bot.ApplicationCommandBulkOverwrite(d.bot.State.User.ID, d.guildID, commands)
// if err != nil { // if err != nil {
// d.app.err.Printf("Discord: Cannot create commands: %v", err) // d.app.err.Printf("Discord: Cannot create commands: %v", err)
// } // }
for i, cmd := range commands { for i, cmd := range d.commandDescriptions {
command, err := d.bot.ApplicationCommandCreate(d.bot.State.User.ID, d.guildID, cmd) command, err := d.bot.ApplicationCommandCreate(d.bot.State.User.ID, d.guildID, cmd)
if err != nil { if err != nil {
d.app.err.Printf("Discord: Cannot create command \"%s\": %v", cmd.Name, err) d.app.err.Printf("Discord: Cannot create command \"%s\": %v", cmd.Name, err)
@ -375,12 +424,32 @@ func (d *DiscordDaemon) deregisterCommands() {
return return
} }
for _, cmd := range existingCommands { for _, cmd := range existingCommands {
if err := d.bot.ApplicationCommandDelete(d.bot.State.User.ID, "", cmd.ID); err != nil { if err := d.bot.ApplicationCommandDelete(d.bot.State.User.ID, d.guildID, cmd.ID); err != nil {
d.app.err.Printf("Failed to deregister command: %v", err) d.app.err.Printf("Discord: Failed to deregister command: %v", err)
} }
} }
} }
// UpdateCommands updates commands which have defined lists of options, to be used when changes occur.
func (d *DiscordDaemon) UpdateCommands() {
// Reload Profile List
profiles := d.app.storage.GetProfiles()
d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(profiles))
for i, profile := range profiles {
d.app.debug.Printf("Discord: registering profile choice \"%s\"", profile.Name)
d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{
Name: profile.Name,
Value: profile.Name,
}
}
cmd, err := d.bot.ApplicationCommandEdit(d.bot.State.User.ID, d.guildID, d.commandIDs[3], d.commandDescriptions[3])
if err != nil {
d.app.err.Printf("Discord: Failed to update profile list: %v\n", err)
} else {
d.commandIDs[3] = cmd.ID
}
}
func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) { func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok { if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok {
if i.GuildID != "" && d.channelName != "" { if i.GuildID != "" && d.channelName != "" {
@ -503,6 +572,124 @@ func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang str
} }
} }
func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang string) {
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
if err != nil {
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", i.Interaction.Member.User.Username, err)
return
}
requester := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username)
d.users[i.Interaction.Member.User.ID] = requester
recipient := i.ApplicationCommandData().Options[0].UserValue(s)
// d.app.debug.Println(invuser)
//label := i.ApplicationCommandData().Options[2].StringValue()
//profile := i.ApplicationCommandData().Options[3].StringValue()
//mins, err := strconv.Atoi(i.ApplicationCommandData().Options[1].StringValue())
//if mins > 0 {
// expmin = mins
//}
// Check whether requestor is linked to the admin account
requesterEmail, ok := d.app.storage.GetEmailsKey(requester.JellyfinID)
if !ok {
d.app.err.Printf("Failed to verify admin")
}
if !requesterEmail.Admin {
d.app.err.Printf("User is not admin")
//add response message
return
}
var expiryMinutes int64 = 30
userLabel := ""
profileName := ""
for i, opt := range i.ApplicationCommandData().Options {
if i == 0 {
continue
}
switch opt.Name {
case "expiry":
expiryMinutes = opt.IntValue()
case "user_label":
userLabel = opt.StringValue()
case "profile":
profileName = opt.StringValue()
}
}
currentTime := time.Now()
validTill := currentTime.Add(time.Minute * time.Duration(expiryMinutes))
invite := Invite{
Code: GenerateInviteCode(),
Created: currentTime,
RemainingUses: 1,
UserExpiry: false,
ValidTill: validTill,
UserLabel: userLabel,
Profile: "Default",
Label: fmt.Sprintf("Discord: %s", RenderDiscordUsername(recipient)),
}
if profileName != "" {
if _, ok := d.app.storage.GetProfileKey(profileName); ok {
invite.Profile = profileName
}
}
if recipient != nil && d.app.config.Section("invite_emails").Key("enabled").MustBool(false) {
d.app.debug.Printf("%s: Sending invite message", invite.Code)
invname, err := d.bot.GuildMember(d.guildID, recipient.ID)
invite.SendTo = invname.User.Username
msg, err := d.app.email.constructInvite(invite.Code, invite, d.app, false)
if err != nil {
invite.SendTo = fmt.Sprintf("Failed to send to %s", RenderDiscordUsername(recipient))
d.app.err.Printf("%s: Failed to construct invite message: %v", invite.Code, err)
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
Type: dg.InteractionResponseChannelMessageWithSource,
Data: &dg.InteractionResponseData{
Content: d.app.storage.lang.Telegram[lang].Strings.get("sentInviteFailure"),
Flags: 64, // Ephemeral
},
})
if err != nil {
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("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{
Content: d.app.storage.lang.Telegram[lang].Strings.get("sentInviteFailure"),
Flags: 64, // Ephemeral
},
})
if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
}
} else {
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{
Content: d.app.storage.lang.Telegram[lang].Strings.get("sentInvite"),
Flags: 64, // Ephemeral
},
})
if err != nil {
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
}
}
}
}
//if profile != "" {
d.app.storage.SetInvitesKey(invite.Code, invite)
}
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) { func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
if m.GuildID != "" && d.channelName != "" { if m.GuildID != "" && d.channelName != "" {
if d.channelID == "" { if d.channelID == "" {

View File

@ -118,7 +118,14 @@
"userPagePage": "Page utilisateur : Page", "userPagePage": "Page utilisateur : Page",
"after": "Après", "after": "Après",
"before": "Avant", "before": "Avant",
"unlink": "Délier le compte" "unlink": "Délier le compte",
"enableReferrals": "Activer Parrainage",
"enableReferralsDescription": "Offrez aux utilisateurs un lien de parrainage personnel semblable à une invitation, à envoyer à vos amis/famille. Peut provenir modèle de profil ou dune invitation existante.",
"invite": "Inviter",
"userLabel": "Étiquette",
"userLabelDescription": "Étiquette à appliquer aux utilisateurs créés avec cette invitation.",
"disableReferrals": "Désactiver Parrainage",
"enableReferralsProfileDescription": "Donnez aux utilisateurs créés avec ce profil un lien de parrainage personnel semblable à une invitation, à envoyer à vos amis/famille. Créez une invitation avec les paramètres souhaités, puis sélectionnez-la ici. Chaque référence sera alors basée sur cette invitation. Vous pouvez supprimer l'invitation une fois terminée."
}, },
"notifications": { "notifications": {
"changedEmailAddress": "Adresse e-mail modifiée de {n}.", "changedEmailAddress": "Adresse e-mail modifiée de {n}.",
@ -156,7 +163,9 @@
"accountConnected": "Compte connecté.", "accountConnected": "Compte connecté.",
"savedAnnouncement": "Annonce enregistrée.", "savedAnnouncement": "Annonce enregistrée.",
"setOmbiProfile": "Profil ombi enregistré.", "setOmbiProfile": "Profil ombi enregistré.",
"errorSetOmbiProfile": "Echec de la sauvegarde du profil ombi." "errorSetOmbiProfile": "Echec de la sauvegarde du profil ombi.",
"errorNoReferralTemplate": "Le profil ne contient pas de modèle de référence, ajoutez-en un dans les paramètres.",
"referralsEnabled": "Parrainage activer."
}, },
"quantityStrings": { "quantityStrings": {
"modifySettingsFor": { "modifySettingsFor": {
@ -214,6 +223,10 @@
"setExpiry": { "setExpiry": {
"singular": "Définir l'expiration pour {n} utilisateur", "singular": "Définir l'expiration pour {n} utilisateur",
"plural": "Définir l'expiration pour {n} utilisateurs" "plural": "Définir l'expiration pour {n} utilisateurs"
},
"enableReferralsFor": {
"singular": "Activer les parrainages pour {n} utilisateur",
"plural": "Activer les parrainages pour {n} utilisateur"
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"name": "Angol (US)" "name": "Magyar (HU)"
}, },
"strings": { "strings": {
"invites": "Meghívások", "invites": "Meghívások",

View File

@ -5,7 +5,7 @@
"strings": { "strings": {
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"password": "Mot de passe", "password": "Mot de passe",
"emailAddress": "Adresse courriel", "emailAddress": "Adresse Email",
"name": "Nom", "name": "Nom",
"submit": "Soumettre", "submit": "Soumettre",
"send": "Envoyer", "send": "Envoyer",
@ -29,7 +29,7 @@
"logout": "Se déconnecter", "logout": "Se déconnecter",
"admin": "Administrateur", "admin": "Administrateur",
"enabled": "Activé", "enabled": "Activé",
"disabled": "Désactivé", "disabled": "Désactiver",
"reEnable": "Ré-activé", "reEnable": "Ré-activé",
"disable": "Désactivé", "disable": "Désactivé",
"expiry": "Expiration", "expiry": "Expiration",
@ -40,7 +40,8 @@
"accountStatus": "Statut du compte", "accountStatus": "Statut du compte",
"notSet": "Non défini", "notSet": "Non défini",
"myAccount": "Mon compte", "myAccount": "Mon compte",
"contactMethods": "Moyens de contact" "contactMethods": "Moyens de contact",
"referrals": "Programme de parrainage"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.", "errorLoginBlank": "Le nom d'utilisateur et/ou le mot de passe sont vides.",
@ -51,16 +52,16 @@
}, },
"quantityStrings": { "quantityStrings": {
"year": { "year": {
"plural": "{n] années", "plural": "{n} années",
"singular": "{n] année" "singular": "{n} année"
}, },
"day": { "day": {
"singular": "{n] jour", "singular": "{n} jour",
"plural": "{n] jours" "plural": "{n} jours"
}, },
"month": { "month": {
"singular": "{n] mois", "singular": "{n} mois",
"plural": "{n] mois" "plural": "{n} mois"
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"name": "Angol (US)" "name": "Magyar (HU)"
}, },
"strings": { "strings": {
"login": "Belépés", "login": "Belépés",

View File

@ -23,7 +23,22 @@
"expiry": "Löper ut", "expiry": "Löper ut",
"edit": "Redigera", "edit": "Redigera",
"delete": "Radera", "delete": "Radera",
"inviteRemainingUses": "Återstående användningar" "inviteRemainingUses": "Återstående användningar",
"send": "Skicka",
"linkDiscord": "Länka Discord",
"copied": "Kopierat",
"linkTelegram": "Länka Telegram",
"contactEmail": "Kontakta via e-post",
"contactTelegram": "Kontakta via Telegram",
"refresh": "Uppdatera",
"required": "Obligatoriskt",
"contactDiscord": "Kontakt via Discord",
"linkMatrix": "Länka Matrix",
"reEnable": "Återaktivera",
"disable": "Inaktivera",
"contactMethods": "Kontaktmetoder",
"accountStatus": "Kontostatus",
"notSet": "Inte inställt"
}, },
"notifications": { "notifications": {
"errorLoginBlank": "Användarnamnet och/eller lösenordet lämnades tomt.", "errorLoginBlank": "Användarnamnet och/eller lösenordet lämnades tomt.",
@ -31,6 +46,5 @@
"errorUnknown": "Okänt fel.", "errorUnknown": "Okänt fel.",
"error401Unauthorized": "Obehörig. Prova att uppdatera sidan.", "error401Unauthorized": "Obehörig. Prova att uppdatera sidan.",
"errorSaveSettings": "Det gick inte att spara inställningarna." "errorSaveSettings": "Det gick inte att spara inställningarna."
}, }
"quantityStrings": {}
} }

View File

@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"name": "Angol (US)" "name": "Magyar (HU)"
}, },
"strings": { "strings": {
"ifItWasNotYou": "", "ifItWasNotYou": "",

View File

@ -7,11 +7,11 @@
"pageTitle": "Créer un compte Jellyfin", "pageTitle": "Créer un compte Jellyfin",
"createAccountHeader": "Création du compte", "createAccountHeader": "Création du compte",
"accountDetails": "Détails", "accountDetails": "Détails",
"emailAddress": "Courriel", "emailAddress": "Email",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"password": "Mot de passe", "password": "Mot de passe",
"reEnterPassword": "Confirmez mot de passe", "reEnterPassword": "Confirmez mot de passe",
"reEnterPasswordInvalid": "Les mots de passe ne correspondent pas.", "reEnterPasswordInvalid": "Les mots de passe ne sont pas pareils.",
"createAccountButton": "Créer le compte", "createAccountButton": "Créer le compte",
"passwordRequirementsHeader": "Mot de passe requis", "passwordRequirementsHeader": "Mot de passe requis",
"successHeader": "Succès !", "successHeader": "Succès !",
@ -35,7 +35,10 @@
"editContactMethod": "Modifier le moyen de contact", "editContactMethod": "Modifier le moyen de contact",
"joinTheServer": "Rejoindre le serveur :", "joinTheServer": "Rejoindre le serveur :",
"customMessagePlaceholderHeader": "Personnaliser cette carte", "customMessagePlaceholderHeader": "Personnaliser cette carte",
"resetPassword": "Réinitialisation de mot de passe" "resetPassword": "Réinitialisation mot de passe",
"referralsDescription": "Invitez vos amis et votre famille à Jellyfin avec ce lien. Revenez ici pour en obtenir un nouveau s'il expire.",
"copyReferral": "Copier le lien",
"invitedBy": "Vous avez été invité par l'utilisateur {user}."
}, },
"validationStrings": { "validationStrings": {
"length": { "length": {

View File

@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"name": "Angol (US)" "name": "Magyar (HU)"
}, },
"strings": { "strings": {
"pageTitle": "Jellyfin fiók létrehozása", "pageTitle": "Jellyfin fiók létrehozása",

View File

@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"name": "Angol (US)" "name": "Magyar (HU)"
}, },
"strings": { "strings": {
"passwordReset": "Jelszó visszaállítás", "passwordReset": "Jelszó visszaállítás",

View File

@ -8,7 +8,7 @@
"back": "Retour", "back": "Retour",
"optional": "Optionnel", "optional": "Optionnel",
"serverType": "Type de serveur", "serverType": "Type de serveur",
"disabled": "Désactivé", "disabled": "Désactiver",
"enabled": "Activé", "enabled": "Activé",
"port": "Port", "port": "Port",
"message": "Message", "message": "Message",

View File

@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"name": "Angol (US)" "name": "Magyar (HU)"
}, },
"strings": { "strings": {
"pageTitle": "Telepítés - jfa-go", "pageTitle": "Telepítés - jfa-go",

View File

@ -11,6 +11,8 @@
"languageMessage": "Note: See available languages with {command}, and set language with {command} <language code>.", "languageMessage": "Note: See available languages with {command}, and set language with {command} <language code>.",
"languageMessageDiscord": "Note: set your language with /lang <language name>.", "languageMessageDiscord": "Note: set your language with /lang <language name>.",
"languageSet": "Language set to {language}.", "languageSet": "Language set to {language}.",
"discordDMs": "Please check your DMs for a response." "discordDMs": "Please check your DMs for a response.",
"sentInvite": "Sent invite.",
"sentInviteFailure": "Failed to send invite, check logs."
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"meta": { "meta": {
"name": "Angol (US)" "name": "Magyar (HU)"
}, },
"strings": { "strings": {
"startMessage": "Helló!\nAdd meg a Jellyfin PIN kódodat itt, hogy megerősítsd a fiókodat.", "startMessage": "Helló!\nAdd meg a Jellyfin PIN kódodat itt, hogy megerősítsd a fiókodat.",

View File

@ -28,9 +28,10 @@ type Logger struct {
fatalFunc func(err interface{}) fatalFunc func(err interface{})
} }
func Lshortfile() string { // Lshortfile is a re-implemented log.Lshortfile with a modifiable call level.
func Lshortfile(level int) string {
// 0 = This function, 1 = Print/Printf/Println, 2 = Caller of Print/Printf/Println // 0 = This function, 1 = Print/Printf/Println, 2 = Caller of Print/Printf/Println
_, file, line, ok := runtime.Caller(2) _, file, line, ok := runtime.Caller(level)
lineString := strconv.Itoa(line) lineString := strconv.Itoa(line)
if !ok { if !ok {
return "" return ""
@ -47,6 +48,10 @@ func Lshortfile() string {
return file + ":" + lineString + ":" return file + ":" + lineString + ":"
} }
func lshortfile() string {
return Lshortfile(2)
}
func NewLogger(out io.Writer, prefix string, flag int, color c.Attribute) (l *Logger) { func NewLogger(out io.Writer, prefix string, flag int, color c.Attribute) (l *Logger) {
l = &Logger{} l = &Logger{}
// Use reimplemented Lshortfile since wrapping the log functions messes them up // Use reimplemented Lshortfile since wrapping the log functions messes them up
@ -73,7 +78,7 @@ func (l *Logger) Printf(format string, v ...interface{}) {
} }
var out string var out string
if l.shortfile { if l.shortfile {
out = Lshortfile() out = lshortfile()
} }
out += " " + l.printer.Sprintf(format, v...) out += " " + l.printer.Sprintf(format, v...)
l.logger.Print(out) l.logger.Print(out)
@ -85,7 +90,7 @@ func (l *Logger) Print(v ...interface{}) {
} }
var out string var out string
if l.shortfile { if l.shortfile {
out = Lshortfile() out = lshortfile()
} }
out += " " + l.printer.Sprint(v...) out += " " + l.printer.Sprint(v...)
l.logger.Print(out) l.logger.Print(out)
@ -97,7 +102,7 @@ func (l *Logger) Println(v ...interface{}) {
} }
var out string var out string
if l.shortfile { if l.shortfile {
out = Lshortfile() out = lshortfile()
} }
out += " " + l.printer.Sprintln(v...) out += " " + l.printer.Sprintln(v...)
l.logger.Print(out) l.logger.Print(out)
@ -109,7 +114,7 @@ func (l *Logger) Fatal(v ...interface{}) {
} }
var out string var out string
if l.shortfile { if l.shortfile {
out = Lshortfile() out = lshortfile()
} }
out += " " + l.printer.Sprint(v...) out += " " + l.printer.Sprint(v...)
l.logger.Fatal(out) l.logger.Fatal(out)
@ -121,7 +126,7 @@ func (l *Logger) Fatalf(format string, v ...interface{}) {
} }
var out string var out string
if l.shortfile { if l.shortfile {
out = Lshortfile() out = lshortfile()
} }
out += " " + l.printer.Sprintf(format, v...) out += " " + l.printer.Sprintf(format, v...)
if l.fatalFunc != nil { if l.fatalFunc != nil {

View File

@ -246,8 +246,12 @@ func start(asDaemon, firstCall bool) {
} }
if debugMode { if debugMode {
app.debug = logger.NewLogger(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile, color.FgYellow) app.debug = logger.NewLogger(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile, color.FgYellow)
// Bind debug log
app.storage.debug = app.debug
app.storage.logActions = generateLogActions(app.config)
} else { } else {
app.debug = logger.NewEmptyLogger() app.debug = logger.NewEmptyLogger()
app.storage.debug = nil
} }
if *PPROF { if *PPROF {
app.info.Print(warning("\n\nWARNING: Don't use pprof in production.\n\n")) app.info.Print(warning("\n\nWARNING: Don't use pprof in production.\n\n"))

View File

@ -10,8 +10,10 @@ import (
"strings" "strings"
"time" "time"
"github.com/hrfee/jfa-go/logger"
"github.com/hrfee/mediabrowser" "github.com/hrfee/mediabrowser"
"github.com/timshannon/badgerhold/v4" "github.com/timshannon/badgerhold/v4"
"gopkg.in/ini.v1"
) )
type discordStore map[string]DiscordUser type discordStore map[string]DiscordUser
@ -24,7 +26,18 @@ type UserExpiry struct {
Expiry time.Time Expiry time.Time
} }
type DebugLogAction int
const (
NoLog DebugLogAction = iota
LogAll
LogDeletion // Logs deletion, and wiping of main field in new data, e.g. setting email.addr to "".
)
type Storage struct { type Storage struct {
debug *logger.Logger
logActions map[string]DebugLogAction
timePattern string timePattern string
db_path string db_path string
@ -47,6 +60,76 @@ type Storage struct {
lang Lang lang Lang
} }
type StoreType int
// Used for debug logging of storage.
const (
StoredEmails StoreType = iota
StoredDiscord
StoredTelegram
StoredMatrix
StoredInvites
StoredAnnouncements
StoredExpiries
StoredProfiles
StoredCustomContent
)
// DebugWatch logs database writes according on the advanced debugging settings in the Advanced section
func (st *Storage) DebugWatch(storeType StoreType, key, mainData string) {
if st.debug == nil {
return
}
actionKey := ""
switch storeType {
case StoredEmails:
actionKey = "emails"
case StoredDiscord:
actionKey = "discord"
case StoredTelegram:
actionKey = "telegram"
case StoredMatrix:
actionKey = "matrix"
case StoredInvites:
actionKey = "invites"
case StoredAnnouncements:
actionKey = "announcements"
case StoredExpiries:
actionKey = "expiries"
case StoredProfiles:
actionKey = "profiles"
case StoredCustomContent:
actionKey = "custom_content"
}
logAction := st.logActions[actionKey]
if logAction == NoLog {
return
}
actionString := "WRITE"
if mainData == "" {
actionString = "DELETE"
}
if logAction == LogAll || mainData == "" {
st.debug.Printf("%s @ %s %s[%s] = \"%s\"\n", actionString, logger.Lshortfile(3), actionKey, key, mainData)
}
}
func generateLogActions(c *ini.File) map[string]DebugLogAction {
m := map[string]DebugLogAction{}
for _, v := range []string{"emails", "discord", "telegram", "matrix", "invites", "announcements", "expirires", "profiles", "custom_content"} {
switch c.Section("advanced").Key("debug_log_" + v).MustString("none") {
case "none":
m[v] = NoLog
case "all":
m[v] = LogAll
case "deletion":
m[v] = LogDeletion
}
}
return m
}
func (app *appContext) ConnectDB() { func (app *appContext) ConnectDB() {
opts := badgerhold.DefaultOptions opts := badgerhold.DefaultOptions
opts.Dir = app.storage.db_path opts.Dir = app.storage.db_path
@ -83,6 +166,7 @@ func (st *Storage) GetEmailsKey(k string) (EmailAddress, bool) {
// SetEmailsKey stores value v in key k. // SetEmailsKey stores value v in key k.
func (st *Storage) SetEmailsKey(k string, v EmailAddress) { func (st *Storage) SetEmailsKey(k string, v EmailAddress) {
st.DebugWatch(StoredEmails, k, v.Addr)
v.JellyfinID = k v.JellyfinID = k
err := st.db.Upsert(k, v) err := st.db.Upsert(k, v)
if err != nil { if err != nil {
@ -92,6 +176,7 @@ func (st *Storage) SetEmailsKey(k string, v EmailAddress) {
// DeleteEmailKey deletes value at key k. // DeleteEmailKey deletes value at key k.
func (st *Storage) DeleteEmailsKey(k string) { func (st *Storage) DeleteEmailsKey(k string) {
st.DebugWatch(StoredEmails, k, "")
st.db.Delete(k, EmailAddress{}) st.db.Delete(k, EmailAddress{})
} }
@ -119,6 +204,7 @@ func (st *Storage) GetDiscordKey(k string) (DiscordUser, bool) {
// SetDiscordKey stores value v in key k. // SetDiscordKey stores value v in key k.
func (st *Storage) SetDiscordKey(k string, v DiscordUser) { func (st *Storage) SetDiscordKey(k string, v DiscordUser) {
st.DebugWatch(StoredDiscord, k, v.Username)
v.JellyfinID = k v.JellyfinID = k
err := st.db.Upsert(k, v) err := st.db.Upsert(k, v)
if err != nil { if err != nil {
@ -128,6 +214,7 @@ func (st *Storage) SetDiscordKey(k string, v DiscordUser) {
// DeleteDiscordKey deletes value at key k. // DeleteDiscordKey deletes value at key k.
func (st *Storage) DeleteDiscordKey(k string) { func (st *Storage) DeleteDiscordKey(k string) {
st.DebugWatch(StoredDiscord, k, "")
st.db.Delete(k, DiscordUser{}) st.db.Delete(k, DiscordUser{})
} }
@ -155,6 +242,7 @@ func (st *Storage) GetTelegramKey(k string) (TelegramUser, bool) {
// SetTelegramKey stores value v in key k. // SetTelegramKey stores value v in key k.
func (st *Storage) SetTelegramKey(k string, v TelegramUser) { func (st *Storage) SetTelegramKey(k string, v TelegramUser) {
st.DebugWatch(StoredTelegram, k, v.Username)
v.JellyfinID = k v.JellyfinID = k
err := st.db.Upsert(k, v) err := st.db.Upsert(k, v)
if err != nil { if err != nil {
@ -164,6 +252,7 @@ func (st *Storage) SetTelegramKey(k string, v TelegramUser) {
// DeleteTelegramKey deletes value at key k. // DeleteTelegramKey deletes value at key k.
func (st *Storage) DeleteTelegramKey(k string) { func (st *Storage) DeleteTelegramKey(k string) {
st.DebugWatch(StoredTelegram, k, "")
st.db.Delete(k, TelegramUser{}) st.db.Delete(k, TelegramUser{})
} }
@ -191,6 +280,7 @@ func (st *Storage) GetMatrixKey(k string) (MatrixUser, bool) {
// SetMatrixKey stores value v in key k. // SetMatrixKey stores value v in key k.
func (st *Storage) SetMatrixKey(k string, v MatrixUser) { func (st *Storage) SetMatrixKey(k string, v MatrixUser) {
st.DebugWatch(StoredMatrix, k, v.UserID)
v.JellyfinID = k v.JellyfinID = k
err := st.db.Upsert(k, v) err := st.db.Upsert(k, v)
if err != nil { if err != nil {
@ -200,6 +290,7 @@ func (st *Storage) SetMatrixKey(k string, v MatrixUser) {
// DeleteMatrixKey deletes value at key k. // DeleteMatrixKey deletes value at key k.
func (st *Storage) DeleteMatrixKey(k string) { func (st *Storage) DeleteMatrixKey(k string) {
st.DebugWatch(StoredMatrix, k, "")
st.db.Delete(k, MatrixUser{}) st.db.Delete(k, MatrixUser{})
} }
@ -227,6 +318,7 @@ func (st *Storage) GetInvitesKey(k string) (Invite, bool) {
// SetInvitesKey stores value v in key k. // SetInvitesKey stores value v in key k.
func (st *Storage) SetInvitesKey(k string, v Invite) { func (st *Storage) SetInvitesKey(k string, v Invite) {
st.DebugWatch(StoredInvites, k, "changed") // Not sure what the main data from this would be
v.Code = k v.Code = k
err := st.db.Upsert(k, v) err := st.db.Upsert(k, v)
if err != nil { if err != nil {
@ -236,6 +328,7 @@ func (st *Storage) SetInvitesKey(k string, v Invite) {
// DeleteInvitesKey deletes value at key k. // DeleteInvitesKey deletes value at key k.
func (st *Storage) DeleteInvitesKey(k string) { func (st *Storage) DeleteInvitesKey(k string) {
st.DebugWatch(StoredInvites, k, "")
st.db.Delete(k, Invite{}) st.db.Delete(k, Invite{})
} }
@ -263,6 +356,7 @@ func (st *Storage) GetAnnouncementsKey(k string) (announcementTemplate, bool) {
// SetAnnouncementsKey stores value v in key k. // SetAnnouncementsKey stores value v in key k.
func (st *Storage) SetAnnouncementsKey(k string, v announcementTemplate) { func (st *Storage) SetAnnouncementsKey(k string, v announcementTemplate) {
st.DebugWatch(StoredAnnouncements, k, v.Subject)
err := st.db.Upsert(k, v) err := st.db.Upsert(k, v)
if err != nil { if err != nil {
// fmt.Printf("Failed to set announcement: %v\n", err) // fmt.Printf("Failed to set announcement: %v\n", err)
@ -271,6 +365,7 @@ func (st *Storage) SetAnnouncementsKey(k string, v announcementTemplate) {
// DeleteAnnouncementsKey deletes value at key k. // DeleteAnnouncementsKey deletes value at key k.
func (st *Storage) DeleteAnnouncementsKey(k string) { func (st *Storage) DeleteAnnouncementsKey(k string) {
st.DebugWatch(StoredAnnouncements, k, "")
st.db.Delete(k, announcementTemplate{}) st.db.Delete(k, announcementTemplate{})
} }
@ -298,6 +393,7 @@ func (st *Storage) GetUserExpiryKey(k string) (UserExpiry, bool) {
// SetUserExpiryKey stores value v in key k. // SetUserExpiryKey stores value v in key k.
func (st *Storage) SetUserExpiryKey(k string, v UserExpiry) { func (st *Storage) SetUserExpiryKey(k string, v UserExpiry) {
st.DebugWatch(StoredExpiries, k, v.Expiry.String())
v.JellyfinID = k v.JellyfinID = k
err := st.db.Upsert(k, v) err := st.db.Upsert(k, v)
if err != nil { if err != nil {
@ -307,6 +403,7 @@ func (st *Storage) SetUserExpiryKey(k string, v UserExpiry) {
// DeleteUserExpiryKey deletes value at key k. // DeleteUserExpiryKey deletes value at key k.
func (st *Storage) DeleteUserExpiryKey(k string) { func (st *Storage) DeleteUserExpiryKey(k string) {
st.DebugWatch(StoredExpiries, k, "")
st.db.Delete(k, UserExpiry{}) st.db.Delete(k, UserExpiry{})
} }
@ -337,6 +434,7 @@ func (st *Storage) GetProfileKey(k string) (Profile, bool) {
// SetProfileKey stores value v in key k. // SetProfileKey stores value v in key k.
func (st *Storage) SetProfileKey(k string, v Profile) { func (st *Storage) SetProfileKey(k string, v Profile) {
st.DebugWatch(StoredProfiles, k, "changed")
v.Name = k v.Name = k
v.Admin = v.Policy.IsAdministrator v.Admin = v.Policy.IsAdministrator
if v.Policy.EnabledFolders != nil { if v.Policy.EnabledFolders != nil {
@ -357,6 +455,7 @@ func (st *Storage) SetProfileKey(k string, v Profile) {
// DeleteProfileKey deletes value at key k. // DeleteProfileKey deletes value at key k.
func (st *Storage) DeleteProfileKey(k string) { func (st *Storage) DeleteProfileKey(k string) {
st.DebugWatch(StoredProfiles, k, "")
st.db.Delete(k, Profile{}) st.db.Delete(k, Profile{})
} }
@ -401,6 +500,7 @@ func (st *Storage) MustGetCustomContentKey(k string) CustomContent {
// SetCustomContentKey stores value v in key k. // SetCustomContentKey stores value v in key k.
func (st *Storage) SetCustomContentKey(k string, v CustomContent) { func (st *Storage) SetCustomContentKey(k string, v CustomContent) {
st.DebugWatch(StoredCustomContent, k, "changed")
v.Name = k v.Name = k
err := st.db.Upsert(k, v) err := st.db.Upsert(k, v)
if err != nil { if err != nil {
@ -410,6 +510,7 @@ func (st *Storage) SetCustomContentKey(k string, v CustomContent) {
// DeleteCustomContentKey deletes value at key k. // DeleteCustomContentKey deletes value at key k.
func (st *Storage) DeleteCustomContentKey(k string) { func (st *Storage) DeleteCustomContentKey(k string) {
st.DebugWatch(StoredCustomContent, k, "")
st.db.Delete(k, CustomContent{}) st.db.Delete(k, CustomContent{})
} }