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

Compare commits

...

12 Commits

Author SHA1 Message Date
0946b3a1da
Merge Database Migration
Database Migration
2023-06-25 20:32:33 +01:00
e1c215b72e
db: remove remaining storage.loadX calls 2023-06-25 20:18:40 +01:00
ea0598e507
db: move legacy data loading out of main/config
put it in loadLegacyData in migrations, which is only called by
migrateToBadger.
2023-06-25 20:17:10 +01:00
28c3d9d2e4
db: use db key to store migration status
the planned config key "migrated_to_db" is not used, instead it is
stored in the database since that's a bit cleaner.
2023-06-25 19:59:11 +01:00
e9f9d9dc98
db: mark migration as completed when it's done
migrated_to_db config key is used. Might also add an extra check to see
if anything is in the DB.
2023-06-25 19:47:31 +01:00
bb75bfd15d
db: deprecate customEmails/userPage 2023-06-25 19:40:54 +01:00
9c84fb5887
profiles: fully deprecate old system
ombi_template, configuration, displayprefs, and policy still stuck
around for the admin new user feature. They are now sourced from the
default profile, and eventually a feature to select the source (or no
source) will be added.

this was still used when creating a new user as admin for some reason.
template is now sourced from the default profile.
2023-06-25 18:59:55 +01:00
3bb9272f06
db: mark profile store as deprecated 2023-06-24 21:32:25 +01:00
a735e4ff29
db: migrate user profiles 2023-06-24 21:29:54 +01:00
63948a6de0
db: migrate invites, user expiry
some fixes to stuff in there too, probably
2023-06-24 19:13:05 +01:00
a470d77938
db: fix contact method cleaning daemons
don't think there's a way to negate a query with badgerhold, so i can't
do "delete(not (where JellyfinID in <ExistingUsers>))", and the old
    method of rebuilding the store is no longer possible.
2023-06-24 18:38:52 +01:00
833be688ac
storage: start db migration (badger(hold))
migrating to badger, with the badgerhold frontend. So far, done:
* Announcements (small, for a quick test)
* Discord/Telegram/Matrix/Email

most interaction with badgerhold is done through the standard
Get<x>/Get<x>Key/Set<x>Key/Delete<x>Key. UserExists functions have been
added for email and matrix, and those and the original ones now use a
query against the database rather than sifting through every record.
I've tagged these searched fields as "index" for badgerhold, although this
definitely isn't used yet, and i'm not entirely sure if it'll be useful.

migrateToBadger is now in migrations.go, and a temporary config key
"migrated_to_badger" has been added, although it isn't being used yet,
migration is just running every time during development.
2023-06-24 17:05:04 +01:00
21 changed files with 1018 additions and 709 deletions

View File

@ -10,21 +10,20 @@ import (
"github.com/gin-gonic/gin"
"github.com/itchyny/timefmt-go"
"github.com/lithammer/shortuuid/v3"
"github.com/timshannon/badgerhold/v4"
)
func (app *appContext) checkInvites() {
currentTime := time.Now()
app.storage.loadInvites()
changed := false
for code, data := range app.storage.GetInvites() {
for _, data := range app.storage.GetInvites() {
expiry := data.ValidTill
if !currentTime.After(expiry) {
continue
}
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
app.debug.Printf("Housekeeping: Deleting old invite %s", data.Code)
notify := data.Notify
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
app.debug.Printf("%s: Expiry notification", code)
app.debug.Printf("%s: Expiry notification", data.Code)
var wait sync.WaitGroup
for address, settings := range notify {
if !settings["notify-expiry"] {
@ -33,9 +32,9 @@ func (app *appContext) checkInvites() {
wait.Add(1)
go func(addr string) {
defer wait.Done()
msg, err := app.email.constructExpiry(code, data, app, false)
msg, err := app.email.constructExpiry(data.Code, data, app, false)
if err != nil {
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
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, "@") {
@ -44,7 +43,7 @@ func (app *appContext) checkInvites() {
err = app.sendByID(msg, addr)
}
if err != nil {
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
app.err.Printf("%s: Failed to send expiry notification: %v", data.Code, err)
} else {
app.info.Printf("Sent expiry notification to %s", addr)
}
@ -53,18 +52,12 @@ func (app *appContext) checkInvites() {
}
wait.Wait()
}
changed = true
app.storage.DeleteInvitesKey(code)
}
if changed {
app.storage.storeInvites()
app.storage.DeleteInvitesKey(data.Code)
}
}
func (app *appContext) checkInvite(code string, used bool, username string) bool {
currentTime := time.Now()
app.storage.loadInvites()
changed := false
inv, match := app.storage.GetInvitesKey(code)
if !match {
return false
@ -103,11 +96,9 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
}
wait.Wait()
}
changed = true
match = false
app.storage.DeleteInvitesKey(code)
} else if used {
changed = true
del := false
newInv := inv
if newInv.RemainingUses == 1 {
@ -122,9 +113,6 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
app.storage.SetInvitesKey(code, newInv)
}
}
if changed {
app.storage.storeInvites()
}
return match
}
@ -138,7 +126,6 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
func (app *appContext) GenerateInvite(gc *gin.Context) {
var req generateInviteDTO
app.debug.Println("Generating new invite")
app.storage.loadInvites()
gc.BindJSON(&req)
currentTime := time.Now()
validTill := currentTime.AddDate(0, req.Months, req.Days)
@ -213,14 +200,13 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
}
}
if req.Profile != "" {
if _, ok := app.storage.profiles[req.Profile]; ok {
if _, ok := app.storage.GetProfileKey(req.Profile); ok {
invite.Profile = req.Profile
} else {
invite.Profile = "Default"
}
}
app.storage.SetInvitesKey(inviteCode, invite)
app.storage.storeInvites()
respondBool(200, true, gc)
}
@ -233,13 +219,12 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
func (app *appContext) GetInvites(gc *gin.Context) {
app.debug.Println("Invites requested")
currentTime := time.Now()
app.storage.loadInvites()
app.checkInvites()
var invites []inviteDTO
for code, inv := range app.storage.GetInvites() {
for _, inv := range app.storage.GetInvites() {
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
invite := inviteDTO{
Code: code,
Code: inv.Code,
Months: months,
Days: days,
Hours: hours,
@ -277,37 +262,36 @@ func (app *appContext) GetInvites(gc *gin.Context) {
invite.SendTo = inv.SendTo
}
if len(inv.Notify) != 0 {
var address string
// 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) {
app.storage.loadEmails()
if addr, ok := app.storage.GetEmailsKey(gc.GetString("jfId")); ok && addr.Addr != "" {
address = addr.Addr
}
addressOrID = gc.GetString("jfId")
} else {
address = app.config.Section("ui").Key("email").String()
addressOrID = app.config.Section("ui").Key("email").String()
}
if _, ok := inv.Notify[address]; ok {
if _, ok = inv.Notify[address]["notify-expiry"]; ok {
invite.NotifyExpiry = inv.Notify[address]["notify-expiry"]
if _, ok := inv.Notify[addressOrID]; ok {
if _, ok = inv.Notify[addressOrID]["notify-expiry"]; ok {
invite.NotifyExpiry = inv.Notify[addressOrID]["notify-expiry"]
}
if _, ok = inv.Notify[address]["notify-creation"]; ok {
invite.NotifyCreation = inv.Notify[address]["notify-creation"]
if _, ok = inv.Notify[addressOrID]["notify-creation"]; ok {
invite.NotifyCreation = inv.Notify[addressOrID]["notify-creation"]
}
}
}
invites = append(invites, invite)
}
profiles := make([]string, len(app.storage.profiles))
if len(app.storage.profiles) != 0 {
profiles[0] = app.storage.defaultProfile
fullProfileList := app.storage.GetProfiles()
profiles := make([]string, len(fullProfileList))
if len(profiles) != 0 {
defaultProfile := app.storage.GetDefaultProfile()
profiles[0] = defaultProfile.Name
i := 1
if len(app.storage.profiles) > 1 {
for p := range app.storage.profiles {
if p != app.storage.defaultProfile {
profiles[i] = p
if len(fullProfileList) > 1 {
app.storage.db.ForEach(badgerhold.Where("Name").Ne(profiles[0]), func(p *Profile) error {
profiles[i] = p.Name
i++
}
}
return nil
})
}
}
resp := getInvitesDTO{
@ -330,7 +314,7 @@ func (app *appContext) SetProfile(gc *gin.Context) {
gc.BindJSON(&req)
app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile)
// "" means "Don't apply profile"
if _, ok := app.storage.profiles[req.Profile]; !ok && req.Profile != "" {
if _, ok := app.storage.GetProfileKey(req.Profile); !ok && req.Profile != "" {
app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile)
respond(500, "Profile not found", gc)
return
@ -338,7 +322,6 @@ func (app *appContext) SetProfile(gc *gin.Context) {
inv, _ := app.storage.GetInvitesKey(req.Invite)
inv.Profile = req.Profile
app.storage.SetInvitesKey(req.Invite, inv)
app.storage.storeInvites()
respondBool(200, true, gc)
}
@ -357,8 +340,6 @@ func (app *appContext) SetNotify(gc *gin.Context) {
changed := false
for code, settings := range req {
app.debug.Printf("%s: Notification settings change requested", code)
app.storage.loadInvites()
app.storage.loadEmails()
invite, ok := app.storage.GetInvitesKey(code)
if !ok {
app.err.Printf("%s Notification setting change failed: Invalid code", code)
@ -401,9 +382,6 @@ func (app *appContext) SetNotify(gc *gin.Context) {
app.storage.SetInvitesKey(code, invite)
}
}
if changed {
app.storage.storeInvites()
}
}
// @Summary Delete an invite.
@ -422,7 +400,6 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
_, ok = app.storage.GetInvitesKey(req.Code)
if ok {
app.storage.DeleteInvitesKey(req.Code)
app.storage.storeInvites()
app.info.Printf("%s: Invite deleted", req.Code)
respondBool(200, true, gc)
return

View File

@ -25,18 +25,18 @@ func (app *appContext) GetCustomContent(gc *gin.Context) {
adminLang = app.storage.lang.chosenAdminLang
}
list := emailListDTO{
"UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled},
"InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled},
"PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled},
"UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.customEmails.UserDeleted.Enabled},
"UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.customEmails.UserDisabled.Enabled},
"UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.customEmails.UserEnabled.Enabled},
"InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.customEmails.InviteEmail.Enabled},
"WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled},
"EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled},
"UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.customEmails.UserExpired.Enabled},
"UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.userPage.Login.Enabled},
"UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.userPage.Page.Enabled},
"UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.MustGetCustomContentKey("UserCreated").Enabled},
"InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.MustGetCustomContentKey("InviteExpiry").Enabled},
"PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.MustGetCustomContentKey("PasswordReset").Enabled},
"UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.MustGetCustomContentKey("UserDeleted").Enabled},
"UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserDisabled").Enabled},
"UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserEnabled").Enabled},
"InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.MustGetCustomContentKey("InviteEmail").Enabled},
"WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.MustGetCustomContentKey("WelcomeEmail").Enabled},
"EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.MustGetCustomContentKey("EmailConfirmation").Enabled},
"UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpired").Enabled},
"UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.MustGetCustomContentKey("Login").Enabled},
"UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.MustGetCustomContentKey("Page").Enabled},
}
filter := gc.Query("filter")
@ -50,10 +50,11 @@ func (app *appContext) GetCustomContent(gc *gin.Context) {
gc.JSON(200, list)
}
func (app *appContext) getCustomMessage(id string) *customContent {
// No longer needed, these are stored by string keys in the database now.
/* func (app *appContext) getCustomMessage(id string) *CustomContent {
switch id {
case "Announcement":
return &customContent{}
return &CustomContent{}
case "UserCreated":
return &app.storage.customEmails.UserCreated
case "InviteExpiry":
@ -80,45 +81,38 @@ func (app *appContext) getCustomMessage(id string) *customContent {
return &app.storage.userPage.Page
}
return nil
}
} */
// @Summary Sets the corresponding custom email.
// @Summary Sets the corresponding custom content.
// @Produce json
// @Param customEmails body customEmails true "Content = email (in markdown)."
// @Param CustomContent body CustomContent true "Content = email (in markdown)."
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Param id path string true "ID of email"
// @Param id path string true "ID of content"
// @Router /config/emails/{id} [post]
// @Security Bearer
// @tags Configuration
func (app *appContext) SetCustomMessage(gc *gin.Context) {
var req customContent
var req CustomContent
gc.BindJSON(&req)
id := gc.Param("id")
if req.Content == "" {
respondBool(400, false, gc)
return
}
message := app.getCustomMessage(id)
if message == nil {
message, ok := app.storage.GetCustomContentKey(id)
if !ok {
respondBool(400, false, gc)
return
}
message.Content = req.Content
message.Enabled = true
if app.storage.storeCustomEmails() != nil {
respondBool(500, false, gc)
return
}
if app.storage.storeUserPageContent() != nil {
respondBool(500, false, gc)
return
}
app.storage.SetCustomContentKey(id, message)
respondBool(200, true, gc)
}
// @Summary Enable/Disable custom email.
// @Summary Enable/Disable custom content.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
@ -137,24 +131,17 @@ func (app *appContext) SetCustomMessageState(gc *gin.Context) {
} else if s != "disable" {
respondBool(400, false, gc)
}
message := app.getCustomMessage(id)
if message == nil {
message, ok := app.storage.GetCustomContentKey(id)
if !ok {
respondBool(400, false, gc)
return
}
message.Enabled = enabled
if app.storage.storeCustomEmails() != nil {
respondBool(500, false, gc)
return
}
if app.storage.storeUserPageContent() != nil {
respondBool(500, false, gc)
return
}
app.storage.SetCustomContentKey(id, message)
respondBool(200, true, gc)
}
// @Summary Returns the custom email/message (generating it if not set) and list of used variables in it.
// @Summary Returns the custom content/message (generating it if not set) and list of used variables in it.
// @Produce json
// @Success 200 {object} customEmailDTO
// @Failure 400 {object} boolResponse
@ -174,8 +161,8 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
var values map[string]interface{}
username := app.storage.lang.Email[lang].Strings.get("username")
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
customMessage := app.getCustomMessage(id)
if customMessage == nil {
customMessage, ok := app.storage.GetCustomContentKey(id)
if !ok {
app.err.Printf("Failed to get custom message with ID \"%s\"", id)
respondBool(400, false, gc)
return
@ -280,13 +267,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
if variables == nil {
variables = []string{}
}
if app.storage.storeCustomEmails() != nil {
respondBool(500, false, gc)
return
}
if app.storage.storeUserPageContent() != nil {
respondBool(500, false, gc)
}
app.storage.SetCustomContentKey(id, customMessage)
var mail *Message
if id != "UserLogin" && id != "UserPage" {
mail, err = app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
@ -387,11 +368,6 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
change := dcUser.Contact != req.Discord
dcUser.Contact = req.Discord
app.storage.SetDiscordKey(req.ID, dcUser)
if err := app.storage.storeDiscordUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Discord: Failed to store users: %v", err)
return
}
if change {
msg := ""
if !req.Discord {
@ -404,11 +380,6 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
change := mxUser.Contact != req.Matrix
mxUser.Contact = req.Matrix
app.storage.SetMatrixKey(req.ID, mxUser)
if err := app.storage.storeMatrixUsers(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Matrix: Failed to store users: %v", err)
return
}
if change {
msg := ""
if !req.Matrix {
@ -421,11 +392,6 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
change := email.Contact != req.Email
email.Contact = req.Email
app.storage.SetEmailsKey(req.ID, email)
if err := app.storage.storeEmails(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Failed to store emails: %v", err)
return
}
if change {
msg := ""
if !req.Email {
@ -646,7 +612,7 @@ func (app *appContext) MatrixConnect(gc *gin.Context) {
var req MatrixConnectUserDTO
gc.BindJSON(&req)
if app.storage.GetMatrix() == nil {
app.storage.matrix = matrixStore{}
app.storage.deprecatedMatrix = matrixStore{}
}
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
if err != nil {
@ -662,11 +628,6 @@ func (app *appContext) MatrixConnect(gc *gin.Context) {
Encrypted: encrypted,
})
app.matrix.isEncrypted[roomID] = encrypted
if err := app.storage.storeMatrixUsers(); err != nil {
app.err.Printf("Failed to store Matrix users: %v", err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
@ -717,11 +678,6 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
return
}
app.storage.SetDiscordKey(req.JellyfinID, user)
if err := app.storage.storeDiscordUsers(); err != nil {
app.err.Printf("Failed to store Discord users: %v", err)
respondBool(500, false, gc)
return
}
linkExistingOmbiDiscordTelegram(app)
respondBool(200, true, gc)
}

View File

@ -71,7 +71,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
var req ombiUser
gc.BindJSON(&req)
profileName := gc.Param("profile")
profile, ok := app.storage.profiles[profileName]
profile, ok := app.storage.GetProfileKey(profileName)
if !ok {
respondBool(400, false, gc)
return
@ -83,12 +83,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
return
}
profile.Ombi = template
app.storage.profiles[profileName] = profile
if err := app.storage.storeProfiles(); err != nil {
respond(500, "Failed to store profile", gc)
app.err.Printf("Failed to store profiles: %v", err)
return
}
app.storage.SetProfileKey(profileName, profile)
respondBool(204, true, gc)
}
@ -103,17 +98,12 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
// @tags Ombi
func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
profileName := gc.Param("profile")
profile, ok := app.storage.profiles[profileName]
profile, ok := app.storage.GetProfileKey(profileName)
if !ok {
respondBool(400, false, gc)
return
}
profile.Ombi = nil
app.storage.profiles[profileName] = profile
if err := app.storage.storeProfiles(); err != nil {
respond(500, "Failed to store profile", gc)
app.err.Printf("Failed to store profiles: %v", err)
return
}
app.storage.SetProfileKey(profileName, profile)
respondBool(204, true, gc)
}

View File

@ -4,6 +4,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/timshannon/badgerhold/v4"
)
// @Summary Get a list of profiles
@ -13,14 +14,13 @@ import (
// @Security Bearer
// @tags Profiles & Settings
func (app *appContext) GetProfiles(gc *gin.Context) {
app.storage.loadProfiles()
app.debug.Println("Profiles requested")
out := getProfilesDTO{
DefaultProfile: app.storage.defaultProfile,
DefaultProfile: app.storage.GetDefaultProfile().Name,
Profiles: map[string]profileDTO{},
}
for name, p := range app.storage.profiles {
out.Profiles[name] = profileDTO{
for _, p := range app.storage.GetProfiles() {
out.Profiles[p.Name] = profileDTO{
Admin: p.Admin,
LibraryAccess: p.LibraryAccess,
FromUser: p.FromUser,
@ -42,20 +42,20 @@ func (app *appContext) SetDefaultProfile(gc *gin.Context) {
req := profileChangeDTO{}
gc.BindJSON(&req)
app.info.Printf("Setting default profile to \"%s\"", req.Name)
if _, ok := app.storage.profiles[req.Name]; !ok {
if _, ok := app.storage.GetProfileKey(req.Name); !ok {
app.err.Printf("Profile not found: \"%s\"", req.Name)
respond(500, "Profile not found", gc)
return
}
for name, profile := range app.storage.profiles {
if name == req.Name {
profile.Admin = true
app.storage.profiles[name] = profile
app.storage.db.ForEach(&badgerhold.Query{}, func(profile *Profile) error {
if profile.Name == req.Name {
profile.Default = true
} else {
profile.Admin = false
profile.Default = false
}
}
app.storage.defaultProfile = req.Name
app.storage.SetProfileKey(profile.Name, *profile)
return nil
})
respondBool(200, true, gc)
}
@ -92,10 +92,7 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
return
}
}
app.storage.loadProfiles()
app.storage.profiles[req.Name] = profile
app.storage.storeProfiles()
app.storage.loadProfiles()
app.storage.SetProfileKey(req.Name, profile)
respondBool(200, true, gc)
}
@ -110,12 +107,6 @@ func (app *appContext) DeleteProfile(gc *gin.Context) {
req := profileChangeDTO{}
gc.BindJSON(&req)
name := req.Name
if _, ok := app.storage.profiles[name]; ok {
if app.storage.defaultProfile == name {
app.storage.defaultProfile = ""
}
delete(app.storage.profiles, name)
}
app.storage.storeProfiles()
app.storage.DeleteProfileKey(name)
respondBool(200, true, gc)
}

View File

@ -38,15 +38,10 @@ func (app *appContext) MyDetails(gc *gin.Context) {
}
resp.Disabled = user.Policy.IsDisabled
if exp, ok := app.storage.users[user.ID]; ok {
resp.Expiry = exp.Unix()
if exp, ok := app.storage.GetUserExpiryKey(user.ID); ok {
resp.Expiry = exp.Expiry.Unix()
}
app.storage.loadEmails()
app.storage.loadDiscordUsers()
app.storage.loadMatrixUsers()
app.storage.loadTelegramUsers()
if emailEnabled {
resp.Email = &MyDetailsContactMethodsDTO{}
if email, ok := app.storage.GetEmailsKey(user.ID); ok && email.Addr != "" {
@ -199,7 +194,6 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
}
}
app.storage.storeEmails()
app.info.Println("Email list modified")
gc.Redirect(http.StatusSeeOther, "/my/account")
return
@ -511,8 +505,8 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
var pwr InternalPWR
var err error
jfID := app.ReverseUserSearch(address)
if jfID == "" {
jfUser, ok := app.ReverseUserSearch(address)
if !ok {
app.debug.Printf("Ignoring PWR request: User not found")
for range timerWait {
@ -521,7 +515,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
}
return
}
pwr, err = app.GenInternalReset(jfID)
pwr, err = app.GenInternalReset(jfUser.ID)
if err != nil {
app.err.Printf("Failed to get user from Jellyfin: %v", err)
for range timerWait {
@ -550,7 +544,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
return
}
return
} else if err := app.sendByID(msg, jfID); err != nil {
} else if err := app.sendByID(msg, jfUser.ID); err != nil {
app.err.Printf("Failed to send password reset message to \"%s\": %v", address, err)
} else {
app.info.Printf("Sent password reset message to \"%s\"", address)

View File

@ -44,16 +44,16 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
return
}
id := user.ID
if app.storage.policy.BlockedTags != nil {
status, err = app.jf.SetPolicy(id, app.storage.policy)
profile := app.storage.GetDefaultProfile()
// Check profile isn't empty
if profile.Policy.BlockedTags != nil {
status, err = app.jf.SetPolicy(id, profile.Policy)
if !(status == 200 || status == 204 || err == nil) {
app.err.Printf("%s: Failed to set user policy (%d): %v", req.Username, status, err)
}
}
if app.storage.configuration.GroupedFolders != nil && len(app.storage.displayprefs) != 0 {
status, err = app.jf.SetConfiguration(id, app.storage.configuration)
status, err = app.jf.SetConfiguration(id, profile.Configuration)
if (status == 200 || status == 204) && err == nil {
status, err = app.jf.SetDisplayPreferences(id, app.storage.displayprefs)
status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs)
}
if !((status == 200 || status == 204) && err == nil) {
app.err.Printf("%s: Failed to set configuration template (%d): %v", req.Username, status, err)
@ -62,12 +62,13 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
app.jf.CacheExpiry = time.Now()
if emailEnabled {
app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true})
app.storage.storeEmails()
}
if app.config.Section("ombi").Key("enabled").MustBool(false) {
app.storage.loadOmbiTemplate()
if len(app.storage.ombi_template) != 0 {
errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, app.storage.ombi_template)
profile := app.storage.GetDefaultProfile()
if profile.Ombi == nil {
profile.Ombi = map[string]interface{}{}
}
errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi)
if err != nil || code != 200 {
app.err.Printf("Failed to create Ombi user (%d): %v", code, err)
app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", "))
@ -75,7 +76,6 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
app.info.Println("Created Ombi user")
}
}
}
if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" {
app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email)
msg, err := app.email.constructWelcome(req.Username, time.Time{}, app, false)
@ -130,9 +130,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false
return
}
if app.config.Section("discord").Key("require_unique").MustBool(false) {
for _, u := range app.storage.GetDiscord() {
if discordUser.ID == u.ID {
if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(discordUser.ID) {
f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Discord user already linked", req.Code)
respond(400, "errorAccountLinked", gc)
@ -140,8 +138,6 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false
return
}
}
}
err := app.discord.ApplyRole(discordUser.ID)
if err != nil {
f = func(gc *gin.Context) {
@ -176,9 +172,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false
return
}
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
for _, u := range app.storage.GetMatrix() {
if user.User.UserID == u.UserID {
if app.config.Section("matrix").Key("require_unique").MustBool(false) && app.matrix.UserExists(user.User.UserID) {
f = func(gc *gin.Context) {
app.debug.Printf("%s: New user failed: Matrix user already linked", req.Code)
respond(400, "errorAccountLinked", gc)
@ -186,8 +180,6 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false
return
}
}
}
matrixVerified = user.Verified
matrixUser = *user.User
@ -278,7 +270,6 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
success = false
return
}
app.storage.loadProfiles()
invite, _ := app.storage.GetInvitesKey(req.Code)
app.checkInvite(req.Code, true, req.Username)
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) {
@ -310,9 +301,9 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
if invite.Profile != "" {
app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile)
var ok bool
profile, ok = app.storage.profiles[invite.Profile]
profile, ok = app.storage.GetProfileKey(invite.Profile)
if !ok {
profile = app.storage.profiles["Default"]
profile = app.storage.GetDefaultProfile()
}
if profile.Policy.BlockedTags != nil {
app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile)
@ -335,30 +326,20 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
if req.Email != "" {
app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true})
app.storage.storeEmails()
}
expiry := time.Time{}
if invite.UserExpiry {
app.storage.usersLock.Lock()
defer app.storage.usersLock.Unlock()
expiry = time.Now().AddDate(0, invite.UserMonths, invite.UserDays).Add(time.Duration((60*invite.UserHours)+invite.UserMinutes) * time.Minute)
app.storage.users[id] = expiry
if err := app.storage.storeUsers(); err != nil {
app.err.Printf("Failed to store user duration: %v", err)
}
app.storage.SetUserExpiryKey(id, UserExpiry{Expiry: expiry})
}
if discordVerified {
discordUser.Contact = req.DiscordContact
if app.storage.discord == nil {
app.storage.discord = discordStore{}
if app.storage.deprecatedDiscord == nil {
app.storage.deprecatedDiscord = discordStore{}
}
app.storage.SetDiscordKey(user.ID, discordUser)
if err := app.storage.storeDiscordUsers(); err != nil {
app.err.Printf("Failed to store Discord users: %v", err)
} else {
delete(app.discord.verifiedTokens, req.DiscordPIN)
}
}
if telegramVerified {
tgUser := TelegramUser{
ChatID: tgToken.ChatID,
@ -368,8 +349,8 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
tgUser.Lang = lang
}
if app.storage.telegram == nil {
app.storage.telegram = telegramStore{}
if app.storage.deprecatedTelegram == nil {
app.storage.deprecatedTelegram = telegramStore{}
}
app.telegram.DeleteVerifiedToken(req.TelegramPIN)
app.storage.SetTelegramKey(user.ID, tgUser)
@ -412,13 +393,10 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
if matrixVerified {
matrixUser.Contact = req.MatrixContact
delete(app.matrix.tokens, req.MatrixPIN)
if app.storage.matrix == nil {
app.storage.matrix = matrixStore{}
if app.storage.deprecatedMatrix == nil {
app.storage.deprecatedMatrix = matrixStore{}
}
app.storage.SetMatrixKey(user.ID, matrixUser)
if err := app.storage.storeMatrixUsers(); err != nil {
app.err.Printf("Failed to store Matrix users: %v", err)
}
}
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramVerified || discordVerified || matrixVerified {
name := app.getAddressOrName(user.ID)
@ -478,16 +456,12 @@ func (app *appContext) NewUser(gc *gin.Context) {
respond(400, "errorNoEmail", gc)
return
}
if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" {
for _, email := range app.storage.GetEmails() {
if req.Email == email.Addr {
if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" && app.EmailAddressExists(req.Email) {
app.info.Printf("%s: New user failed: Email already in use", req.Code)
respond(400, "errorEmailLinked", gc)
return
}
}
}
}
f, success := app.newUser(req, false)
if !success {
f(gc)
@ -641,21 +615,16 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) {
respondBool(400, false, gc)
return
}
app.storage.usersLock.Lock()
defer app.storage.usersLock.Unlock()
for _, id := range req.Users {
if expiry, ok := app.storage.users[id]; ok {
app.storage.users[id] = expiry.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute)
base := time.Now()
if expiry, ok := app.storage.GetUserExpiryKey(id); ok {
base = expiry.Expiry
app.debug.Printf("Expiry extended for \"%s\"", id)
} else {
app.storage.users[id] = time.Now().AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute)
app.debug.Printf("Created expiry for \"%s\"", id)
}
}
if err := app.storage.storeUsers(); err != nil {
app.err.Printf("Failed to store user duration: %v", err)
respondBool(500, false, gc)
return
expiry := UserExpiry{Expiry: base.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute)}
app.storage.SetUserExpiryKey(id, expiry)
}
respondBool(204, true, gc)
}
@ -727,27 +696,21 @@ func (app *appContext) SaveAnnounceTemplate(gc *gin.Context) {
respondBool(400, false, gc)
return
}
app.storage.announcements[req.Name] = req
if err := app.storage.storeAnnouncements(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Failed to store announcement templates: %v", err)
return
}
app.storage.SetAnnouncementsKey(req.Name, req)
respondBool(200, true, gc)
}
// @Summary Save an announcement as a template for use or editing later.
// @Produce json
// @Success 200 {object} getAnnouncementsDTO
// @Router /users/announce/template [get]
// @Router /users/announce [get]
// @Security Bearer
// @tags Users
func (app *appContext) GetAnnounceTemplates(gc *gin.Context) {
resp := &getAnnouncementsDTO{make([]string, len(app.storage.announcements))}
i := 0
for name := range app.storage.announcements {
resp.Announcements[i] = name
i++
resp := &getAnnouncementsDTO{make([]string, len(app.storage.GetAnnouncements()))}
for i, a := range app.storage.GetAnnouncements() {
resp.Announcements[i] = a.Name
}
gc.JSON(200, resp)
}
@ -762,7 +725,7 @@ func (app *appContext) GetAnnounceTemplates(gc *gin.Context) {
// @tags Users
func (app *appContext) GetAnnounceTemplate(gc *gin.Context) {
name := gc.Param("name")
if announcement, ok := app.storage.announcements[name]; ok {
if announcement, ok := app.storage.GetAnnouncementsKey(name); ok {
gc.JSON(200, announcement)
return
}
@ -779,12 +742,7 @@ func (app *appContext) GetAnnounceTemplate(gc *gin.Context) {
// @tags Users
func (app *appContext) DeleteAnnounceTemplate(gc *gin.Context) {
name := gc.Param("name")
delete(app.storage.announcements, name)
if err := app.storage.storeAnnouncements(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Failed to store announcement templates: %v", err)
return
}
app.storage.DeleteAnnouncementsKey(name)
respondBool(200, false, gc)
}
@ -876,8 +834,6 @@ func (app *appContext) GetUsers(gc *gin.Context) {
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
allowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
i := 0
app.storage.usersLock.Lock()
defer app.storage.usersLock.Unlock()
for _, jfUser := range users {
user := respUser{
ID: jfUser.ID,
@ -894,9 +850,9 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.Label = email.Label
user.AccountsAdmin = (app.jellyfinLogin) && (email.Admin || (adminOnly && jfUser.Policy.IsAdministrator) || allowAll)
}
expiry, ok := app.storage.users[jfUser.ID]
expiry, ok := app.storage.GetUserExpiryKey(jfUser.ID)
if ok {
user.Expiry = expiry.Unix()
user.Expiry = expiry.Expiry.Unix()
}
if tgUser, ok := app.storage.GetTelegramKey(jfUser.ID); ok {
user.Telegram = tgUser.Username
@ -947,10 +903,6 @@ func (app *appContext) SetAccountsAdmin(gc *gin.Context) {
app.storage.SetEmailsKey(id, emailStore)
}
}
if err := app.storage.storeEmails(); err != nil {
app.err.Printf("Failed to store email list: %v", err)
respondBool(500, false, gc)
}
app.info.Println("Email list modified")
respondBool(204, true, gc)
}
@ -984,10 +936,6 @@ func (app *appContext) ModifyLabels(gc *gin.Context) {
app.storage.SetEmailsKey(id, emailStore)
}
}
if err := app.storage.storeEmails(); err != nil {
app.err.Printf("Failed to store email list: %v", err)
respondBool(500, false, gc)
}
app.info.Println("Email list modified")
respondBool(204, true, gc)
}
@ -1022,7 +970,6 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
// Auto enable contact by email for newly added addresses
if !ok || oldEmail.Addr == "" {
emailStore.Contact = true
app.storage.storeEmails()
}
emailStore.Addr = address
@ -1039,7 +986,6 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
}
}
}
app.storage.storeEmails()
app.info.Println("Email list modified")
respondBool(200, true, gc)
}
@ -1062,25 +1008,24 @@ func (app *appContext) ApplySettings(gc *gin.Context) {
var displayprefs map[string]interface{}
var ombi map[string]interface{}
if req.From == "profile" {
app.storage.loadProfiles()
// Check profile exists & isn't empty
if _, ok := app.storage.profiles[req.Profile]; !ok || app.storage.profiles[req.Profile].Policy.BlockedTags == nil {
profile, ok := app.storage.GetProfileKey(req.Profile)
if !ok || profile.Policy.BlockedTags == nil {
app.err.Printf("Couldn't find profile \"%s\" or profile was empty", req.Profile)
respond(500, "Couldn't find profile", gc)
return
}
if req.Homescreen {
if app.storage.profiles[req.Profile].Configuration.GroupedFolders == nil || len(app.storage.profiles[req.Profile].Displayprefs) == 0 {
if profile.Configuration.GroupedFolders == nil || len(profile.Displayprefs) == 0 {
app.err.Printf("No homescreen saved in profile \"%s\"", req.Profile)
respond(500, "No homescreen template available", gc)
return
}
configuration = app.storage.profiles[req.Profile].Configuration
displayprefs = app.storage.profiles[req.Profile].Displayprefs
configuration = profile.Configuration
displayprefs = profile.Displayprefs
}
policy = app.storage.profiles[req.Profile].Policy
policy = profile.Policy
if app.config.Section("ombi").Key("enabled").MustBool(false) {
profile := app.storage.profiles[req.Profile]
if profile.Ombi != nil && len(profile.Ombi) != 0 {
ombi = profile.Ombi
}

View File

@ -157,15 +157,6 @@ func (app *appContext) loadConfig() error {
app.MustSetValue("updates", "channel", releaseChannel)
}
app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String()
app.storage.loadCustomEmails()
app.MustSetValue("user_page", "enabled", "true")
if app.config.Section("user_page").Key("enabled").MustBool(false) {
app.storage.userPage_path = app.config.Section("files").Key("custom_user_page_content").String()
app.storage.loadUserPageContent()
}
substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
if substituteStrings != "" {

View File

@ -7,86 +7,54 @@ import "time"
// the user cache is fresh.
func (app *appContext) clearEmails() {
app.debug.Println("Housekeeping: removing unused email addresses")
users, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil || len(users) == 0 {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
return
emails := app.storage.GetEmails()
for _, email := range emails {
_, status, err := app.jf.UserByID(email.JellyfinID, false)
if status == 200 && err != nil {
continue
}
// Rebuild email storage to from existing users to reduce time complexity
emails := emailStore{}
app.storage.emailsLock.Lock()
for _, user := range users {
if email, ok := app.storage.GetEmailsKey(user.ID); ok {
emails[user.ID] = email
app.storage.DeleteEmailsKey(email.JellyfinID)
}
}
app.storage.emails = emails
app.storage.storeEmails()
app.storage.emailsLock.Unlock()
}
// clearDiscord does the same as clearEmails, but for Discord Users.
func (app *appContext) clearDiscord() {
app.debug.Println("Housekeeping: removing unused Discord IDs")
users, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil || len(users) == 0 {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
return
discordUsers := app.storage.GetDiscord()
for _, discordUser := range discordUsers {
_, status, err := app.jf.UserByID(discordUser.JellyfinID, false)
if status == 200 && err != nil {
continue
}
// Rebuild discord storage to from existing users to reduce time complexity
dcUsers := discordStore{}
app.storage.discordLock.Lock()
for _, user := range users {
if dcUser, ok := app.storage.GetDiscordKey(user.ID); ok {
dcUsers[user.ID] = dcUser
app.storage.DeleteDiscordKey(discordUser.JellyfinID)
}
}
app.storage.discord = dcUsers
app.storage.storeDiscordUsers()
app.storage.discordLock.Unlock()
}
// clearMatrix does the same as clearEmails, but for Matrix Users.
func (app *appContext) clearMatrix() {
app.debug.Println("Housekeeping: removing unused Matrix IDs")
users, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil || len(users) == 0 {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
return
matrixUsers := app.storage.GetMatrix()
for _, matrixUser := range matrixUsers {
_, status, err := app.jf.UserByID(matrixUser.JellyfinID, false)
if status == 200 && err != nil {
continue
}
// Rebuild matrix storage to from existing users to reduce time complexity
mxUsers := matrixStore{}
app.storage.matrixLock.Lock()
for _, user := range users {
if mxUser, ok := app.storage.GetMatrixKey(user.ID); ok {
mxUsers[user.ID] = mxUser
app.storage.DeleteMatrixKey(matrixUser.JellyfinID)
}
}
app.storage.matrix = mxUsers
app.storage.storeMatrixUsers()
app.storage.matrixLock.Unlock()
}
// clearTelegram does the same as clearEmails, but for Telegram Users.
func (app *appContext) clearTelegram() {
app.debug.Println("Housekeeping: removing unused Telegram IDs")
users, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil || len(users) == 0 {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
return
telegramUsers := app.storage.GetTelegram()
for _, telegramUser := range telegramUsers {
_, status, err := app.jf.UserByID(telegramUser.JellyfinID, false)
if status == 200 && err != nil {
continue
}
// Rebuild telegram storage to from existing users to reduce time complexity
tgUsers := telegramStore{}
app.storage.telegramLock.Lock()
for _, user := range users {
if tgUser, ok := app.storage.GetTelegramKey(user.ID); ok {
tgUsers[user.ID] = tgUser
app.storage.DeleteTelegramKey(telegramUser.JellyfinID)
}
}
app.storage.telegram = tgUsers
app.storage.storeTelegramUsers()
app.storage.telegramLock.Unlock()
}
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
@ -148,7 +116,6 @@ func (rt *housekeepingDaemon) run() {
break
}
started := time.Now()
rt.app.storage.loadInvites()
for _, job := range rt.jobs {
job(rt.app)

View File

@ -6,6 +6,7 @@ import (
"time"
dg "github.com/bwmarrin/discordgo"
"github.com/timshannon/badgerhold/v4"
)
type DiscordDaemon struct {
@ -478,11 +479,11 @@ func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang str
code := i.ApplicationCommandData().Options[0].StringValue()
if _, ok := d.app.storage.lang.Telegram[code]; ok {
var user DiscordUser
for jfID, u := range d.app.storage.GetDiscord() {
for _, u := range d.app.storage.GetDiscord() {
if u.ID == i.Interaction.Member.User.ID {
u.Lang = code
lang = code
d.app.storage.SetDiscordKey(jfID, u)
d.app.storage.SetDiscordKey(u.JellyfinID, u)
user = u
break
}
@ -585,10 +586,10 @@ func (d *DiscordDaemon) msgLang(s *dg.Session, m *dg.MessageCreate, sects []stri
}
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
var user DiscordUser
for jfID, u := range d.app.storage.GetDiscord() {
for _, u := range d.app.storage.GetDiscord() {
if u.ID == m.Author.ID {
u.Lang = sects[1]
d.app.storage.SetDiscordKey(jfID, u)
d.app.storage.SetDiscordKey(u.JellyfinID, u)
user = u
break
}
@ -708,15 +709,9 @@ func (d *DiscordDaemon) AssignedUserVerified(pin string, jfID string) (user Disc
}
// UserExists returns whether or not a user with the given ID exists.
func (d *DiscordDaemon) UserExists(id string) (ok bool) {
ok = false
for _, u := range d.app.storage.GetDiscord() {
if u.ID == id {
ok = true
break
}
}
return
func (d *DiscordDaemon) UserExists(id string) bool {
c, err := d.app.storage.db.Count(&DiscordUser{}, badgerhold.Where("ID").Eq(id))
return err != nil || c > 0
}
// DeleteVerifiedUser removes the token with the given PIN.

136
email.go
View File

@ -19,8 +19,10 @@ import (
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/hrfee/mediabrowser"
"github.com/itchyny/timefmt-go"
"github.com/mailgun/mailgun-go/v4"
"github.com/timshannon/badgerhold/v4"
sMail "github.com/xhit/go-simple-mail/v2"
)
@ -329,10 +331,11 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
}
var err error
template := emailer.confirmationValues(code, username, key, app, noSub)
if app.storage.customEmails.EmailConfirmation.Enabled {
message := app.storage.MustGetCustomContentKey("EmailConfirmation")
if message.Enabled {
content := templateEmail(
app.storage.customEmails.EmailConfirmation.Content,
app.storage.customEmails.EmailConfirmation.Variables,
message.Content,
message.Variables,
nil,
template,
)
@ -412,10 +415,11 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
}
template := emailer.inviteValues(code, invite, app, noSub)
var err error
if app.storage.customEmails.InviteEmail.Enabled {
message := app.storage.MustGetCustomContentKey("InviteEmail")
if message.Enabled {
content := templateEmail(
app.storage.customEmails.InviteEmail.Content,
app.storage.customEmails.InviteEmail.Variables,
message.Content,
message.Variables,
nil,
template,
)
@ -451,10 +455,11 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
}
var err error
template := emailer.expiryValues(code, invite, app, noSub)
if app.storage.customEmails.InviteExpiry.Enabled {
message := app.storage.MustGetCustomContentKey("InviteExpiry")
if message.Enabled {
content := templateEmail(
app.storage.customEmails.InviteExpiry.Content,
app.storage.customEmails.InviteExpiry.Variables,
message.Content,
message.Variables,
nil,
template,
)
@ -505,10 +510,11 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
}
template := emailer.createdValues(code, username, address, invite, app, noSub)
var err error
if app.storage.customEmails.UserCreated.Enabled {
message := app.storage.MustGetCustomContentKey("UserCreated")
if message.Enabled {
content := templateEmail(
app.storage.customEmails.UserCreated.Content,
app.storage.customEmails.UserCreated.Variables,
message.Content,
message.Variables,
nil,
template,
)
@ -578,10 +584,11 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
}
template := emailer.resetValues(pwr, app, noSub)
var err error
if app.storage.customEmails.PasswordReset.Enabled {
message := app.storage.MustGetCustomContentKey("PasswordReset")
if message.Enabled {
content := templateEmail(
app.storage.customEmails.PasswordReset.Content,
app.storage.customEmails.PasswordReset.Variables,
message.Content,
message.Variables,
nil,
template,
)
@ -619,10 +626,11 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
}
var err error
template := emailer.deletedValues(reason, app, noSub)
if app.storage.customEmails.UserDeleted.Enabled {
message := app.storage.MustGetCustomContentKey("UserDeleted")
if message.Enabled {
content := templateEmail(
app.storage.customEmails.UserDeleted.Content,
app.storage.customEmails.UserDeleted.Variables,
message.Content,
message.Variables,
nil,
template,
)
@ -660,10 +668,11 @@ func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub
}
var err error
template := emailer.disabledValues(reason, app, noSub)
if app.storage.customEmails.UserDisabled.Enabled {
message := app.storage.MustGetCustomContentKey("UserDisabled")
if message.Enabled {
content := templateEmail(
app.storage.customEmails.UserDisabled.Content,
app.storage.customEmails.UserDisabled.Variables,
message.Content,
message.Variables,
nil,
template,
)
@ -701,10 +710,11 @@ func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub b
}
var err error
template := emailer.enabledValues(reason, app, noSub)
if app.storage.customEmails.UserEnabled.Enabled {
message := app.storage.MustGetCustomContentKey("UserEnabled")
if message.Enabled {
content := templateEmail(
app.storage.customEmails.UserEnabled.Content,
app.storage.customEmails.UserEnabled.Variables,
message.Content,
message.Variables,
nil,
template,
)
@ -756,7 +766,8 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app
}
var err error
var template map[string]interface{}
if app.storage.customEmails.WelcomeEmail.Enabled {
message := app.storage.MustGetCustomContentKey("WelcomeEmail")
if message.Enabled {
template = emailer.welcomeValues(username, expiry, app, noSub, true)
} else {
template = emailer.welcomeValues(username, expiry, app, noSub, false)
@ -766,11 +777,11 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app
"date": "{yourAccountWillExpire}",
})
}
if app.storage.customEmails.WelcomeEmail.Enabled {
if message.Enabled {
content := templateEmail(
app.storage.customEmails.WelcomeEmail.Content,
app.storage.customEmails.WelcomeEmail.Variables,
app.storage.customEmails.WelcomeEmail.Conditionals,
message.Content,
message.Variables,
message.Conditionals,
template,
)
email, err = emailer.constructTemplate(email.Subject, content, app)
@ -801,10 +812,11 @@ func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Mess
}
var err error
template := emailer.userExpiredValues(app, noSub)
if app.storage.customEmails.UserExpired.Enabled {
message := app.storage.MustGetCustomContentKey("UserExpired")
if message.Enabled {
content := templateEmail(
app.storage.customEmails.UserExpired.Content,
app.storage.customEmails.UserExpired.Variables,
message.Content,
message.Variables,
nil,
template,
)
@ -874,31 +886,63 @@ func (app *appContext) getAddressOrName(jfID string) string {
// ReverseUserSearch returns the jellyfin ID of the user with the given username, email, or contact method username.
// returns "" if none found. returns only the first match, might be an issue if there are users with the same contact method usernames.
func (app *appContext) ReverseUserSearch(address string) string {
func (app *appContext) ReverseUserSearch(address string) (user mediabrowser.User, ok bool) {
ok = false
user, status, err := app.jf.UserByName(address, false)
if status == 200 && err == nil {
return user.ID
ok = true
return
}
for id, email := range app.storage.GetEmails() {
if strings.ToLower(address) == strings.ToLower(email.Addr) {
return id
emailAddresses := []EmailAddress{}
err = app.storage.db.Find(&emailAddresses, badgerhold.Where("Addr").Eq(address))
if err == nil && len(emailAddresses) > 0 {
for _, emailUser := range emailAddresses {
user, status, err = app.jf.UserByID(emailUser.JellyfinID, false)
if status == 200 && err == nil {
ok = true
return
}
}
for id, dcUser := range app.storage.GetDiscord() {
}
// Dont know how we'd use badgerhold when we need to render each username,
// Apart from storing the rendered name in the db.
for _, dcUser := range app.storage.GetDiscord() {
if RenderDiscordUsername(dcUser) == strings.ToLower(address) {
return id
user, status, err = app.jf.UserByID(dcUser.JellyfinID, false)
if status == 200 && err == nil {
ok = true
return
}
}
}
tgUsername := strings.TrimPrefix(address, "@")
for id, tgUser := range app.storage.GetTelegram() {
if tgUsername == tgUser.Username {
return id
telegramUsers := []TelegramUser{}
err = app.storage.db.Find(&telegramUsers, badgerhold.Where("Username").Eq(tgUsername))
if err == nil && len(telegramUsers) > 0 {
for _, telegramUser := range telegramUsers {
user, status, err = app.jf.UserByID(telegramUser.JellyfinID, false)
if status == 200 && err == nil {
ok = true
return
}
}
for id, mxUser := range app.storage.GetMatrix() {
if address == mxUser.UserID {
return id
}
matrixUsers := []MatrixUser{}
err = app.storage.db.Find(&matrixUsers, badgerhold.Where("UserID").Eq(address))
if err == nil && len(matrixUsers) > 0 {
for _, matrixUser := range matrixUsers {
user, status, err = app.jf.UserByID(matrixUser.JellyfinID, false)
if status == 200 && err == nil {
ok = true
return
}
}
return ""
}
return
}
// EmailAddressExists returns whether or not a user with the given email address exists.
func (app *appContext) EmailAddressExists(address string) bool {
c, err := app.storage.db.Count(&EmailAddress{}, badgerhold.Where("Addr").Eq(address))
return err != nil || c > 0
}

14
go.mod
View File

@ -48,7 +48,12 @@ require (
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgraph-io/badger/v3 v3.2103.1 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
github.com/getlantern/errors v1.0.3 // indirect
@ -69,12 +74,19 @@ require (
github.com/go-stack/stack v1.8.1 // indirect
github.com/go-test/deep v1.1.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v2.0.0+incompatible // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
@ -92,9 +104,11 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/timshannon/badgerhold/v4 v4.0.2 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect

149
go.sum
View File

@ -1,11 +1,14 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
@ -13,17 +16,39 @@ github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v3 v3.2103.1 h1:zaX53IRg7ycxVlkd5pYdCeFp1FynD6qBGQoQql3R3Hk=
github.com/dgraph-io/badger/v3 v3.2103.1/go.mod h1:dULbq6ehJ5K0cGW/1TQ9iSfUk0gbSiToDWmWmTsJ53E=
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:M88ob4TyDnEqNuL3PgsE/p3bDujfspnulR+0dQWNYZs=
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:buzQsO8HHkZX2Q45fdfGH1xejPjuDQaXH8btcYMFzPM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
@ -33,6 +58,7 @@ github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+ne
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
@ -131,20 +157,54 @@ github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB1/j8wa6xdKn85B5NzgVL/pTU=
github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a h1:AWZzzFrqyjYlRloN6edwTLTUbKxf5flLXNuTBDm3Ews=
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI=
github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -153,8 +213,10 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hrfee/mediabrowser v0.3.8 h1:y0iBCb6jE3QKcsiCJSYva2fFPHRn4UA+sGRzoPuJ/Dk=
github.com/hrfee/mediabrowser v0.3.8/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@ -165,6 +227,11 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@ -185,6 +252,7 @@ github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJV
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailgun/mailgun-go/v4 v4.9.0 h1:wRbxvVQ5QObFewLxc1uVvipA16D8gxeiO+cBOca51Iw=
github.com/mailgun/mailgun-go/v4 v4.9.0/go.mod h1:FJlF9rI5cQT+mrwujtJjPMbIVy3Ebor9bKTVsJ0QU40=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -206,6 +274,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -216,6 +286,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
@ -225,6 +296,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc=
github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
@ -233,10 +305,19 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/steambap/captcha v1.4.1 h1:OmMdxLCWCqJvsFaFYwRpvMckIuvI6s8s1LsBrBw97P0=
github.com/steambap/captcha v1.4.1/go.mod h1:oC9T7IfEgnrhzjDz5Djf1H7GPffCzRMbsQfFkJmhlnk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -276,6 +357,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/timshannon/badgerhold/v3 v3.0.0-20210909134927-2b6764d68c1e/go.mod h1:/Seq5xGNo8jLhSbDX3jdbeZrp4yFIpQ6/7n4TjziEWs=
github.com/timshannon/badgerhold/v4 v4.0.2 h1:83OLY/NFnEaMnHEPd84bYtkLipVkjTsMbzQRYbk47g4=
github.com/timshannon/badgerhold/v4 v4.0.2/go.mod h1:rh6RyXLQFsvrvcKondPQQFZnNovpRzu+gS0FlLxYuHY=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
@ -285,6 +369,7 @@ github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/o
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
@ -296,8 +381,14 @@ github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6Fk
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
github.com/xhit/go-simple-mail/v2 v2.13.0 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI=
github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
@ -320,26 +411,37 @@ go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -347,29 +449,45 @@ golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -396,11 +514,16 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
@ -410,13 +533,37 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -435,6 +582,8 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
maunium.net/go/mautrix v0.15.2 h1:fUiVajeoOR92uJoSShHbCvh7uG6lDY4ZO4Mvt90LbjU=

View File

@ -79,7 +79,7 @@
"ombiProfile": "Ombi user profile",
"ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go when this profile is selected.",
"userProfiles": "User Profiles",
"userProfilesDescription": "Profiles are applied to users when they create an account. A profile include library access rights and homescreen layout.",
"userProfilesDescription": "Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.",
"userProfilesIsDefault": "Default",
"userProfilesLibraries": "Libraries",
"addProfile": "Add Profile",

56
main.go
View File

@ -335,59 +335,8 @@ func start(asDaemon, firstCall bool) {
app.debug.Printf("Loaded config file \"%s\"", app.configPath)
app.debug.Println("Loading storage")
app.storage.invite_path = app.config.Section("files").Key("invites").String()
if err := app.storage.loadInvites(); err != nil {
app.err.Printf("Failed to load Invites: %v", err)
}
app.storage.emails_path = app.config.Section("files").Key("emails").String()
if err := app.storage.loadEmails(); err != nil {
app.err.Printf("Failed to load Emails: %v", err)
err := migrateEmailStorage(app)
if err != nil {
app.err.Printf("Failed to migrate Email storage: %v", err)
}
}
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
if err := app.storage.loadPolicy(); err != nil {
app.err.Printf("Failed to load Policy: %v", err)
}
app.storage.configuration_path = app.config.Section("files").Key("user_configuration").String()
if err := app.storage.loadConfiguration(); err != nil {
app.err.Printf("Failed to load Configuration: %v", err)
}
app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String()
if err := app.storage.loadDisplayprefs(); err != nil {
app.err.Printf("Failed to load Displayprefs: %v", err)
}
app.storage.users_path = app.config.Section("files").Key("users").String()
if err := app.storage.loadUsers(); err != nil {
app.err.Printf("Failed to load Users: %v", err)
}
app.storage.telegram_path = app.config.Section("files").Key("telegram_users").String()
if err := app.storage.loadTelegramUsers(); err != nil {
app.err.Printf("Failed to load Telegram users: %v", err)
}
app.storage.discord_path = app.config.Section("files").Key("discord_users").String()
if err := app.storage.loadDiscordUsers(); err != nil {
app.err.Printf("Failed to load Discord users: %v", err)
}
app.storage.matrix_path = app.config.Section("files").Key("matrix_users").String()
if err := app.storage.loadMatrixUsers(); err != nil {
app.err.Printf("Failed to load Matrix users: %v", err)
}
app.storage.announcements_path = app.config.Section("files").Key("announcements").String()
if err := app.storage.loadAnnouncements(); err != nil {
app.err.Printf("Failed to load announcement templates: %v", err)
}
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
app.storage.loadProfiles()
if app.config.Section("ombi").Key("enabled").MustBool(false) {
app.storage.ombi_path = app.config.Section("files").Key("ombi_template").String()
app.storage.loadOmbiTemplate()
app.debug.Printf("Connecting to Ombi")
ombiServer := app.config.Section("ombi").Key("server").String()
app.ombi = ombi.NewOmbi(
ombiServer,
@ -397,6 +346,9 @@ func start(asDaemon, firstCall bool) {
}
app.storage.db_path = filepath.Join(app.dataPath, "db")
app.ConnectDB()
defer app.storage.db.Close()
// Read config-base for settings on web.
app.configBasePath = "config-base.json"
configBase, _ := fs.ReadFile(localFS, app.configBasePath)

View File

@ -6,6 +6,7 @@ import (
"time"
"github.com/gomarkdown/markdown"
"github.com/timshannon/badgerhold/v4"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
@ -36,6 +37,7 @@ type MatrixUser struct {
UserID string
Lang string
Contact bool
JellyfinID string `badgerhold:"key"`
}
var matrixFilter = mautrix.Filter{
@ -268,6 +270,12 @@ func (d *MatrixDaemon) Send(message *Message, users ...MatrixUser) (err error) {
return
}
// UserExists returns whether or not a user with the given User ID exists.
func (d *MatrixDaemon) UserExists(userID string) bool {
c, err := d.app.storage.db.Count(&MatrixUser{}, badgerhold.Where("UserID").Eq(userID))
return err != nil || c > 0
}
// 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

View File

@ -16,11 +16,14 @@ func runMigrations(app *appContext) {
migrateNotificationMethods(app)
linkExistingOmbiDiscordTelegram(app)
// migrateHyphens(app)
migrateToBadger(app)
}
// Migrate pre-0.2.0 user templates to profiles
func migrateProfiles(app *appContext) {
if !(app.storage.policy.BlockedTags == nil && app.storage.configuration.GroupedFolders == nil && len(app.storage.displayprefs) == 0) {
if app.storage.deprecatedPolicy.BlockedTags == nil && app.storage.deprecatedConfiguration.GroupedFolders == nil && len(app.storage.deprecatedDisplayprefs) == 0 {
return
}
app.info.Println("Migrating user template files to new profile format")
app.storage.migrateToProfile()
for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} {
@ -36,7 +39,6 @@ func migrateProfiles(app *appContext) {
app.info.Println("In case of a problem, your original files have been renamed to <file>.old.json")
app.storage.storeProfiles()
}
}
// Migrate pre-0.2.5 bootstrap theme choice to a17t version.
func migrateBootstrap(app *appContext) {
@ -131,7 +133,7 @@ func migrateNotificationMethods(app *appContext) error {
return nil
}
changes := false
for code, invite := range app.storage.invites {
for code, invite := range app.storage.deprecatedInvites {
if invite.Notify == nil {
continue
}
@ -139,9 +141,9 @@ func migrateNotificationMethods(app *appContext) error {
if !strings.Contains(address, "@") {
continue
}
for id, email := range app.storage.GetEmails() {
for _, email := range app.storage.GetEmails() {
if email.Addr == address {
invite.Notify[id] = notifyPrefs
invite.Notify[email.JellyfinID] = notifyPrefs
delete(invite.Notify, address)
changes = true
break
@ -149,7 +151,7 @@ func migrateNotificationMethods(app *appContext) error {
}
}
if changes {
app.storage.invites[code] = invite
app.storage.deprecatedInvites[code] = invite
}
}
if changes {
@ -168,16 +170,16 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
return nil
}
idList := map[string][2]string{}
for jfID, user := range app.storage.GetDiscord() {
idList[jfID] = [2]string{user.ID, ""}
for _, user := range app.storage.GetDiscord() {
idList[user.JellyfinID] = [2]string{user.ID, ""}
}
for jfID, user := range app.storage.GetTelegram() {
vals, ok := idList[jfID]
for _, user := range app.storage.GetTelegram() {
vals, ok := idList[user.JellyfinID]
if !ok {
vals = [2]string{"", ""}
}
vals[1] = user.Username
idList[jfID] = vals
idList[user.JellyfinID] = vals
}
for jfID, ids := range idList {
ombiUser, status, err := app.getOmbiUser(jfID)
@ -194,6 +196,143 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
return nil
}
// MigrationStatus is just used to store whether data from JSON files has been migrated to the DB.
type MigrationStatus struct {
Done bool
}
func loadLegacyData(app *appContext) {
app.storage.invite_path = app.config.Section("files").Key("invites").String()
if err := app.storage.loadInvites(); err != nil {
app.err.Printf("LegacyData: Failed to load Invites: %v", err)
}
app.storage.emails_path = app.config.Section("files").Key("emails").String()
if err := app.storage.loadEmails(); err != nil {
app.err.Printf("LegacyData: Failed to load Emails: %v", err)
err := migrateEmailStorage(app)
if err != nil {
app.err.Printf("LegacyData: Failed to migrate Email storage: %v", err)
}
}
app.storage.users_path = app.config.Section("files").Key("users").String()
if err := app.storage.loadUserExpiries(); err != nil {
app.err.Printf("LegacyData: Failed to load Users: %v", err)
}
app.storage.telegram_path = app.config.Section("files").Key("telegram_users").String()
if err := app.storage.loadTelegramUsers(); err != nil {
app.err.Printf("LegacyData: Failed to load Telegram users: %v", err)
}
app.storage.discord_path = app.config.Section("files").Key("discord_users").String()
if err := app.storage.loadDiscordUsers(); err != nil {
app.err.Printf("LegacyData: Failed to load Discord users: %v", err)
}
app.storage.matrix_path = app.config.Section("files").Key("matrix_users").String()
if err := app.storage.loadMatrixUsers(); err != nil {
app.err.Printf("LegacyData: Failed to load Matrix users: %v", err)
}
app.storage.announcements_path = app.config.Section("files").Key("announcements").String()
if err := app.storage.loadAnnouncements(); err != nil {
app.err.Printf("LegacyData: Failed to load announcement templates: %v", err)
}
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
app.storage.loadProfiles()
app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String()
app.storage.loadCustomEmails()
app.MustSetValue("user_page", "enabled", "true")
if app.config.Section("user_page").Key("enabled").MustBool(false) {
app.storage.userPage_path = app.config.Section("files").Key("custom_user_page_content").String()
app.storage.loadUserPageContent()
}
}
func migrateToBadger(app *appContext) {
// Check the DB to see if we've already migrated
migrated := MigrationStatus{}
app.storage.db.Get("migrated_to_db", &migrated)
if migrated.Done {
return
}
app.info.Println("Migrating to Badger(hold)")
loadLegacyData(app)
for k, v := range app.storage.deprecatedAnnouncements {
app.storage.SetAnnouncementsKey(k, v)
}
for jfID, v := range app.storage.deprecatedDiscord {
app.storage.SetDiscordKey(jfID, v)
}
for jfID, v := range app.storage.deprecatedTelegram {
app.storage.SetTelegramKey(jfID, v)
}
for jfID, v := range app.storage.deprecatedMatrix {
app.storage.SetMatrixKey(jfID, v)
}
for jfID, v := range app.storage.deprecatedEmails {
app.storage.SetEmailsKey(jfID, v)
}
for k, v := range app.storage.deprecatedInvites {
app.storage.SetInvitesKey(k, v)
}
for k, v := range app.storage.deprecatedUserExpiries {
app.storage.SetUserExpiryKey(k, UserExpiry{Expiry: v})
}
for k, v := range app.storage.deprecatedProfiles {
app.storage.SetProfileKey(k, v)
}
if _, ok := app.storage.GetCustomContentKey("UserCreated"); !ok {
app.storage.SetCustomContentKey("UserCreated", app.storage.deprecatedCustomEmails.UserCreated)
}
if _, ok := app.storage.GetCustomContentKey("InviteExpiry"); !ok {
app.storage.SetCustomContentKey("InviteExpiry", app.storage.deprecatedCustomEmails.InviteExpiry)
}
if _, ok := app.storage.GetCustomContentKey("PasswordReset"); !ok {
app.storage.SetCustomContentKey("PasswordReset", app.storage.deprecatedCustomEmails.PasswordReset)
}
if _, ok := app.storage.GetCustomContentKey("UserDeleted"); !ok {
app.storage.SetCustomContentKey("UserDeleted", app.storage.deprecatedCustomEmails.UserDeleted)
}
if _, ok := app.storage.GetCustomContentKey("UserDisabled"); !ok {
app.storage.SetCustomContentKey("UserDisabled", app.storage.deprecatedCustomEmails.UserDisabled)
}
if _, ok := app.storage.GetCustomContentKey("UserEnabled"); !ok {
app.storage.SetCustomContentKey("UserEnabled", app.storage.deprecatedCustomEmails.UserEnabled)
}
if _, ok := app.storage.GetCustomContentKey("InviteEmail"); !ok {
app.storage.SetCustomContentKey("InviteEmail", app.storage.deprecatedCustomEmails.InviteEmail)
}
if _, ok := app.storage.GetCustomContentKey("WelcomeEmail"); !ok {
app.storage.SetCustomContentKey("WelcomeEmail", app.storage.deprecatedCustomEmails.WelcomeEmail)
}
if _, ok := app.storage.GetCustomContentKey("EmailConfirmation"); !ok {
app.storage.SetCustomContentKey("EmailConfirmation", app.storage.deprecatedCustomEmails.EmailConfirmation)
}
if _, ok := app.storage.GetCustomContentKey("UserExpired"); !ok {
app.storage.SetCustomContentKey("UserExpired", app.storage.deprecatedCustomEmails.UserExpired)
}
if _, ok := app.storage.GetCustomContentKey("UserLogin"); !ok {
app.storage.SetCustomContentKey("UserLogin", app.storage.deprecatedUserPageContent.Login)
}
if _, ok := app.storage.GetCustomContentKey("UserPage"); !ok {
app.storage.SetCustomContentKey("UserPage", app.storage.deprecatedUserPageContent.Page)
}
err := app.storage.db.Upsert("migrated_to_db", MigrationStatus{true})
if err != nil {
app.err.Fatalf("Failed to migrate to DB: %v\n", err)
}
app.info.Println("All data migrated to database. JSON files in the config folder can be deleted if you are sure all data is correct in the app. Create an issue if you have problems.")
}
// Migrate between hyphenated & non-hyphenated user IDs. Doesn't seem to happen anymore, so disabled.
// func migrateHyphens(app *appContext) {
// checkVersion := func(version string) int {
@ -212,8 +351,8 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
// app.jf.GetUsers(false)
//
// noHyphens := true
// for id := range app.storage.GetEmails() {
// if strings.Contains(id, "-") {
// for _, e := range app.storage.GetEmails() {
// if strings.Contains(e.JellyfinID, "-") {
// noHyphens = false
// break
// }
@ -255,7 +394,7 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
// app.storage.emails = newEmails
// app.storage.users = newUsers
// err = app.storage.storeEmails()
// err2 = app.storage.storeUsers()
// err2 = app.storage.storeUserExpiries()
// if err != nil {
// app.err.Fatalf("couldn't store emails.json: %v", err)
// }

View File

@ -100,7 +100,6 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
app.debug.Printf("Error: %s", err)
return
}
app.storage.loadEmails()
uid := user.ID
if uid == "" {
app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username)

View File

@ -8,11 +8,11 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/hrfee/mediabrowser"
"github.com/steambap/captcha"
"github.com/timshannon/badgerhold/v4"
)
type discordStore map[string]DiscordUser
@ -20,224 +20,439 @@ type telegramStore map[string]TelegramUser
type matrixStore map[string]MatrixUser
type emailStore map[string]EmailAddress
type UserExpiry struct {
JellyfinID string `badgerhold:"key"`
Expiry time.Time
}
type Storage struct {
timePattern string
db_path string
db *badgerhold.Store
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path, userPage_path string
users map[string]time.Time // Map of Jellyfin User IDs to their expiry times.
invites Invites
profiles map[string]Profile
defaultProfile string
displayprefs, ombi_template map[string]interface{}
emails emailStore
telegram telegramStore // Map of Jellyfin User IDs to telegram users.
discord discordStore // Map of Jellyfin user IDs to discord users.
matrix matrixStore // Map of Jellyfin user IDs to Matrix users.
customEmails customEmails
userPage userPageContent
policy mediabrowser.Policy
configuration mediabrowser.Configuration
deprecatedUserExpiries map[string]time.Time // Map of Jellyfin User IDs to their expiry times.
deprecatedInvites Invites
deprecatedProfiles map[string]Profile
deprecatedDisplayprefs, deprecatedOmbiTemplate map[string]interface{}
deprecatedEmails emailStore // Map of Jellyfin User IDs to Email addresses.
deprecatedTelegram telegramStore // Map of Jellyfin User IDs to telegram users.
deprecatedDiscord discordStore // Map of Jellyfin user IDs to discord users.
deprecatedMatrix matrixStore // Map of Jellyfin user IDs to Matrix users.
deprecatedPolicy mediabrowser.Policy
deprecatedConfiguration mediabrowser.Configuration
deprecatedAnnouncements map[string]announcementTemplate
deprecatedCustomEmails customEmails
deprecatedUserPageContent userPageContent
lang Lang
announcements map[string]announcementTemplate
invitesLock, usersLock, discordLock, telegramLock, matrixLock, emailsLock sync.Mutex
}
func (app *appContext) ConnectDB() {
opts := badgerhold.DefaultOptions
opts.Dir = app.storage.db_path
opts.ValueDir = app.storage.db_path
db, err := badgerhold.Open(opts)
if err != nil {
app.err.Fatalf("Failed to open db \"%s\": %v", app.storage.db_path, err)
}
app.storage.db = db
app.info.Printf("Connected to DB \"%s\"", app.storage.db_path)
}
// GetEmails returns a copy of the store.
func (st *Storage) GetEmails() emailStore {
return st.emails
func (st *Storage) GetEmails() []EmailAddress {
result := []EmailAddress{}
err := st.db.Find(&result, &badgerhold.Query{})
if err != nil {
// fmt.Printf("Failed to find emails: %v\n", err)
}
return result
}
// GetEmailsKey returns the value stored in the store's key.
func (st *Storage) GetEmailsKey(k string) (EmailAddress, bool) {
v, ok := st.emails[k]
return v, ok
result := EmailAddress{}
err := st.db.Get(k, &result)
ok := true
if err != nil {
// fmt.Printf("Failed to find email: %v\n", err)
ok = false
}
return result, ok
}
// SetEmailsKey stores value v in key k.
func (st *Storage) SetEmailsKey(k string, v EmailAddress) {
st.emailsLock.Lock()
st.emails[k] = v
st.storeEmails()
st.emailsLock.Unlock()
v.JellyfinID = k
err := st.db.Upsert(k, v)
if err != nil {
// fmt.Printf("Failed to set email: %v\n", err)
}
}
// DeleteEmailKey deletes value at key k.
func (st *Storage) DeleteEmailsKey(k string) {
st.emailsLock.Lock()
delete(st.emails, k)
st.storeEmails()
st.emailsLock.Unlock()
st.db.Delete(k, EmailAddress{})
}
// GetDiscord returns a copy of the store.
func (st *Storage) GetDiscord() discordStore {
if st.discord == nil {
st.discord = discordStore{}
func (st *Storage) GetDiscord() []DiscordUser {
result := []DiscordUser{}
err := st.db.Find(&result, &badgerhold.Query{})
if err != nil {
// fmt.Printf("Failed to find users: %v\n", err)
}
return st.discord
return result
}
// GetDiscordKey returns the value stored in the store's key.
func (st *Storage) GetDiscordKey(k string) (DiscordUser, bool) {
v, ok := st.discord[k]
return v, ok
result := DiscordUser{}
err := st.db.Get(k, &result)
ok := true
if err != nil {
// fmt.Printf("Failed to find user: %v\n", err)
ok = false
}
return result, ok
}
// SetDiscordKey stores value v in key k.
func (st *Storage) SetDiscordKey(k string, v DiscordUser) {
st.discordLock.Lock()
if st.discord == nil {
st.discord = discordStore{}
v.JellyfinID = k
err := st.db.Upsert(k, v)
if err != nil {
// fmt.Printf("Failed to set user: %v\n", err)
}
st.discord[k] = v
st.storeDiscordUsers()
st.discordLock.Unlock()
}
// DeleteDiscordKey deletes value at key k.
func (st *Storage) DeleteDiscordKey(k string) {
st.discordLock.Lock()
delete(st.discord, k)
st.storeDiscordUsers()
st.discordLock.Unlock()
st.db.Delete(k, DiscordUser{})
}
// GetTelegram returns a copy of the store.
func (st *Storage) GetTelegram() telegramStore {
if st.telegram == nil {
st.telegram = telegramStore{}
func (st *Storage) GetTelegram() []TelegramUser {
result := []TelegramUser{}
err := st.db.Find(&result, &badgerhold.Query{})
if err != nil {
// fmt.Printf("Failed to find users: %v\n", err)
}
return st.telegram
return result
}
// GetTelegramKey returns the value stored in the store's key.
func (st *Storage) GetTelegramKey(k string) (TelegramUser, bool) {
v, ok := st.telegram[k]
return v, ok
result := TelegramUser{}
err := st.db.Get(k, &result)
ok := true
if err != nil {
// fmt.Printf("Failed to find user: %v\n", err)
ok = false
}
return result, ok
}
// SetTelegramKey stores value v in key k.
func (st *Storage) SetTelegramKey(k string, v TelegramUser) {
st.telegramLock.Lock()
if st.telegram == nil {
st.telegram = telegramStore{}
v.JellyfinID = k
err := st.db.Upsert(k, v)
if err != nil {
// fmt.Printf("Failed to set user: %v\n", err)
}
st.telegram[k] = v
st.storeTelegramUsers()
st.telegramLock.Unlock()
}
// DeleteTelegramKey deletes value at key k.
func (st *Storage) DeleteTelegramKey(k string) {
st.telegramLock.Lock()
delete(st.telegram, k)
st.storeTelegramUsers()
st.telegramLock.Unlock()
st.db.Delete(k, TelegramUser{})
}
// GetMatrix returns a copy of the store.
func (st *Storage) GetMatrix() matrixStore {
if st.matrix == nil {
st.matrix = matrixStore{}
func (st *Storage) GetMatrix() []MatrixUser {
result := []MatrixUser{}
err := st.db.Find(&result, &badgerhold.Query{})
if err != nil {
// fmt.Printf("Failed to find users: %v\n", err)
}
return st.matrix
return result
}
// GetMatrixKey returns the value stored in the store's key.
func (st *Storage) GetMatrixKey(k string) (MatrixUser, bool) {
v, ok := st.matrix[k]
return v, ok
result := MatrixUser{}
err := st.db.Get(k, &result)
ok := true
if err != nil {
// fmt.Printf("Failed to find user: %v\n", err)
ok = false
}
return result, ok
}
// SetMatrixKey stores value v in key k.
func (st *Storage) SetMatrixKey(k string, v MatrixUser) {
st.matrixLock.Lock()
if st.matrix == nil {
st.matrix = matrixStore{}
v.JellyfinID = k
err := st.db.Upsert(k, v)
if err != nil {
// fmt.Printf("Failed to set user: %v\n", err)
}
st.matrix[k] = v
st.storeMatrixUsers()
st.matrixLock.Unlock()
}
// DeleteMatrixKey deletes value at key k.
func (st *Storage) DeleteMatrixKey(k string) {
st.matrixLock.Lock()
delete(st.matrix, k)
st.storeMatrixUsers()
st.matrixLock.Unlock()
st.db.Delete(k, MatrixUser{})
}
// GetInvites returns a copy of the store.
func (st *Storage) GetInvites() Invites {
if st.invites == nil {
st.invites = Invites{}
func (st *Storage) GetInvites() []Invite {
result := []Invite{}
err := st.db.Find(&result, &badgerhold.Query{})
if err != nil {
// fmt.Printf("Failed to find invites: %v\n", err)
}
return st.invites
return result
}
// GetInvitesKey returns the value stored in the store's key.
func (st *Storage) GetInvitesKey(k string) (Invite, bool) {
v, ok := st.invites[k]
return v, ok
result := Invite{}
err := st.db.Get(k, &result)
ok := true
if err != nil {
// fmt.Printf("Failed to find invite: %v\n", err)
ok = false
}
return result, ok
}
// SetInvitesKey stores value v in key k.
func (st *Storage) SetInvitesKey(k string, v Invite) {
st.invitesLock.Lock()
if st.invites == nil {
st.invites = Invites{}
v.Code = k
err := st.db.Upsert(k, v)
if err != nil {
// fmt.Printf("Failed to set invite: %v\n", err)
}
st.invites[k] = v
st.storeInvites()
st.invitesLock.Unlock()
}
// DeleteInvitesKey deletes value at key k.
func (st *Storage) DeleteInvitesKey(k string) {
st.invitesLock.Lock()
delete(st.invites, k)
st.storeInvites()
st.invitesLock.Unlock()
st.db.Delete(k, Invite{})
}
// GetAnnouncements returns a copy of the store.
func (st *Storage) GetAnnouncements() []announcementTemplate {
result := []announcementTemplate{}
err := st.db.Find(&result, &badgerhold.Query{})
if err != nil {
// fmt.Printf("Failed to find announcements: %v\n", err)
}
return result
}
// GetAnnouncementsKey returns the value stored in the store's key.
func (st *Storage) GetAnnouncementsKey(k string) (announcementTemplate, bool) {
result := announcementTemplate{}
err := st.db.Get(k, &result)
ok := true
if err != nil {
// fmt.Printf("Failed to find announcement: %v\n", err)
ok = false
}
return result, ok
}
// SetAnnouncementsKey stores value v in key k.
func (st *Storage) SetAnnouncementsKey(k string, v announcementTemplate) {
err := st.db.Upsert(k, v)
if err != nil {
// fmt.Printf("Failed to set announcement: %v\n", err)
}
}
// DeleteAnnouncementsKey deletes value at key k.
func (st *Storage) DeleteAnnouncementsKey(k string) {
st.db.Delete(k, announcementTemplate{})
}
// GetUserExpiries returns a copy of the store.
func (st *Storage) GetUserExpiries() []UserExpiry {
result := []UserExpiry{}
err := st.db.Find(&result, &badgerhold.Query{})
if err != nil {
// fmt.Printf("Failed to find expiries: %v\n", err)
}
return result
}
// GetUserExpiryKey returns the value stored in the store's key.
func (st *Storage) GetUserExpiryKey(k string) (UserExpiry, bool) {
result := UserExpiry{}
err := st.db.Get(k, &result)
ok := true
if err != nil {
// fmt.Printf("Failed to find expiry: %v\n", err)
ok = false
}
return result, ok
}
// SetUserExpiryKey stores value v in key k.
func (st *Storage) SetUserExpiryKey(k string, v UserExpiry) {
v.JellyfinID = k
err := st.db.Upsert(k, v)
if err != nil {
// fmt.Printf("Failed to set expiry: %v\n", err)
}
}
// DeleteUserExpiryKey deletes value at key k.
func (st *Storage) DeleteUserExpiryKey(k string) {
st.db.Delete(k, UserExpiry{})
}
// GetProfiles returns a copy of the store.
func (st *Storage) GetProfiles() []Profile {
result := []Profile{}
err := st.db.Find(&result, &badgerhold.Query{})
if err != nil {
// fmt.Printf("Failed to find profiles: %v\n", err)
}
return result
}
// GetProfileKey returns the value stored in the store's key.
func (st *Storage) GetProfileKey(k string) (Profile, bool) {
result := Profile{}
err := st.db.Get(k, &result)
ok := true
if err != nil {
// fmt.Printf("Failed to find profile: %v\n", err)
ok = false
}
return result, ok
}
// SetProfileKey stores value v in key k.
func (st *Storage) SetProfileKey(k string, v Profile) {
v.Name = k
v.Admin = v.Policy.IsAdministrator
if v.Policy.EnabledFolders != nil {
if len(v.Policy.EnabledFolders) == 0 {
v.LibraryAccess = "All"
} else {
v.LibraryAccess = strconv.Itoa(len(v.Policy.EnabledFolders))
}
}
if v.FromUser == "" {
v.FromUser = "Unknown"
}
err := st.db.Upsert(k, v)
if err != nil {
// fmt.Printf("Failed to set profile: %v\n", err)
}
}
// DeleteProfileKey deletes value at key k.
func (st *Storage) DeleteProfileKey(k string) {
st.db.Delete(k, Profile{})
}
// GetDefaultProfile returns the first profile set as default, or anything available if there isn't one.
func (st *Storage) GetDefaultProfile() Profile {
defaultProfile := Profile{}
err := st.db.FindOne(&defaultProfile, badgerhold.Where("Default").Eq(true))
if err != nil {
st.db.FindOne(&defaultProfile, &badgerhold.Query{})
}
return defaultProfile
}
// GetCustomContent returns a copy of the store.
func (st *Storage) GetCustomContent() []CustomContent {
result := []CustomContent{}
err := st.db.Find(&result, &badgerhold.Query{})
if err != nil {
// fmt.Printf("Failed to find custom content: %v\n", err)
}
return result
}
// GetCustomContentKey returns the value stored in the store's key.
func (st *Storage) GetCustomContentKey(k string) (CustomContent, bool) {
result := CustomContent{}
err := st.db.Get(k, &result)
ok := true
if err != nil {
// fmt.Printf("Failed to find custom content: %v\n", err)
ok = false
}
return result, ok
}
// MustGetCustomContentKey returns the value stored in the store's key, or an empty value.
func (st *Storage) MustGetCustomContentKey(k string) CustomContent {
result := CustomContent{}
st.db.Get(k, &result)
return result
}
// SetCustomContentKey stores value v in key k.
func (st *Storage) SetCustomContentKey(k string, v CustomContent) {
v.Name = k
err := st.db.Upsert(k, v)
if err != nil {
// fmt.Printf("Failed to set custom content: %v\n", err)
}
}
// DeleteCustomContentKey deletes value at key k.
func (st *Storage) DeleteCustomContentKey(k string) {
st.db.Delete(k, CustomContent{})
}
type TelegramUser struct {
ChatID int64
Username string
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 {
ChannelID string
ID string
Username string
ID string `badgerhold:"index"`
Username string `badgerhold:"index"`
Discriminator string
Lang string
Contact bool
JellyfinID string `json:"-"` // Used internally in discord.go
JellyfinID string `json:"-" badgerhold:"key"` // Used internally in discord.go
}
type EmailAddress struct {
Addr string
Addr string `badgerhold:"index"`
Label string // User Label.
Contact bool
Admin bool // Whether or not user is jfa-go admin.
JellyfinID string `badgerhold:"key"`
}
type customEmails struct {
UserCreated customContent `json:"userCreated"`
InviteExpiry customContent `json:"inviteExpiry"`
PasswordReset customContent `json:"passwordReset"`
UserDeleted customContent `json:"userDeleted"`
UserDisabled customContent `json:"userDisabled"`
UserEnabled customContent `json:"userEnabled"`
InviteEmail customContent `json:"inviteEmail"`
WelcomeEmail customContent `json:"welcomeEmail"`
EmailConfirmation customContent `json:"emailConfirmation"`
UserExpired customContent `json:"userExpired"`
UserCreated CustomContent `json:"userCreated"`
InviteExpiry CustomContent `json:"inviteExpiry"`
PasswordReset CustomContent `json:"passwordReset"`
UserDeleted CustomContent `json:"userDeleted"`
UserDisabled CustomContent `json:"userDisabled"`
UserEnabled CustomContent `json:"userEnabled"`
InviteEmail CustomContent `json:"inviteEmail"`
WelcomeEmail CustomContent `json:"welcomeEmail"`
EmailConfirmation CustomContent `json:"emailConfirmation"`
UserExpired CustomContent `json:"userExpired"`
}
type customContent struct {
// CustomContent stores customized versions of jfa-go content, including emails and user messages.
type CustomContent struct {
Name string `json:"name" badgerhold:"key"`
Enabled bool `json:"enabled,omitempty"`
Content string `json:"content"`
Variables []string `json:"variables,omitempty"`
@ -245,14 +460,15 @@ type customContent struct {
}
type userPageContent struct {
Login customContent `json:"login"`
Page customContent `json:"page"`
Login CustomContent `json:"login"`
Page CustomContent `json:"page"`
}
// timePattern: %Y-%m-%dT%H:%M:%S.%f
type Profile struct {
Admin bool `json:"admin,omitempty"`
Name string `badgerhold:"key"`
Admin bool `json:"admin,omitempty" badgerhold:"index"`
LibraryAccess string `json:"libraries,omitempty"`
FromUser string `json:"fromUser,omitempty"`
Policy mediabrowser.Policy `json:"policy,omitempty"`
@ -263,6 +479,7 @@ type Profile struct {
}
type Invite struct {
Code string `badgerhold:"key"`
Created time.Time `json:"created"`
NoLimit bool `json:"no-limit"`
RemainingUses int `json:"remaining-uses"`
@ -964,18 +1181,16 @@ func (st *Storage) loadLangTelegram(filesystems ...fs.FS) error {
type Invites map[string]Invite
func (st *Storage) loadInvites() error {
return loadJSON(st.invite_path, &st.invites)
return loadJSON(st.invite_path, &st.deprecatedInvites)
}
func (st *Storage) storeInvites() error {
return storeJSON(st.invite_path, st.invites)
return storeJSON(st.invite_path, st.deprecatedInvites)
}
func (st *Storage) loadUsers() error {
st.usersLock.Lock()
defer st.usersLock.Unlock()
if st.users == nil {
st.users = map[string]time.Time{}
func (st *Storage) loadUserExpiries() error {
if st.deprecatedUserExpiries == nil {
st.deprecatedUserExpiries = map[string]time.Time{}
}
temp := map[string]time.Time{}
err := loadJSON(st.users_path, &temp)
@ -983,111 +1198,111 @@ func (st *Storage) loadUsers() error {
return err
}
for id, t1 := range temp {
if _, ok := st.users[id]; !ok {
st.users[id] = t1
if _, ok := st.deprecatedUserExpiries[id]; !ok {
st.deprecatedUserExpiries[id] = t1
}
}
return nil
}
func (st *Storage) storeUsers() error {
return storeJSON(st.users_path, st.users)
func (st *Storage) storeUserExpiries() error {
return storeJSON(st.users_path, st.deprecatedUserExpiries)
}
func (st *Storage) loadEmails() error {
return loadJSON(st.emails_path, &st.emails)
return loadJSON(st.emails_path, &st.deprecatedEmails)
}
func (st *Storage) storeEmails() error {
return storeJSON(st.emails_path, st.emails)
return storeJSON(st.emails_path, st.deprecatedEmails)
}
func (st *Storage) loadTelegramUsers() error {
return loadJSON(st.telegram_path, &st.telegram)
return loadJSON(st.telegram_path, &st.deprecatedTelegram)
}
func (st *Storage) storeTelegramUsers() error {
return storeJSON(st.telegram_path, st.telegram)
return storeJSON(st.telegram_path, st.deprecatedTelegram)
}
func (st *Storage) loadDiscordUsers() error {
return loadJSON(st.discord_path, &st.discord)
return loadJSON(st.discord_path, &st.deprecatedDiscord)
}
func (st *Storage) storeDiscordUsers() error {
return storeJSON(st.discord_path, st.discord)
return storeJSON(st.discord_path, st.deprecatedDiscord)
}
func (st *Storage) loadMatrixUsers() error {
return loadJSON(st.matrix_path, &st.matrix)
return loadJSON(st.matrix_path, &st.deprecatedMatrix)
}
func (st *Storage) storeMatrixUsers() error {
return storeJSON(st.matrix_path, st.matrix)
return storeJSON(st.matrix_path, st.deprecatedMatrix)
}
func (st *Storage) loadCustomEmails() error {
return loadJSON(st.customEmails_path, &st.customEmails)
return loadJSON(st.customEmails_path, &st.deprecatedCustomEmails)
}
func (st *Storage) storeCustomEmails() error {
return storeJSON(st.customEmails_path, st.customEmails)
return storeJSON(st.customEmails_path, st.deprecatedCustomEmails)
}
func (st *Storage) loadUserPageContent() error {
return loadJSON(st.userPage_path, &st.userPage)
return loadJSON(st.userPage_path, &st.deprecatedUserPageContent)
}
func (st *Storage) storeUserPageContent() error {
return storeJSON(st.userPage_path, st.userPage)
return storeJSON(st.userPage_path, st.deprecatedUserPageContent)
}
func (st *Storage) loadPolicy() error {
return loadJSON(st.policy_path, &st.policy)
return loadJSON(st.policy_path, &st.deprecatedPolicy)
}
func (st *Storage) storePolicy() error {
return storeJSON(st.policy_path, st.policy)
return storeJSON(st.policy_path, st.deprecatedPolicy)
}
func (st *Storage) loadConfiguration() error {
return loadJSON(st.configuration_path, &st.configuration)
return loadJSON(st.configuration_path, &st.deprecatedConfiguration)
}
func (st *Storage) storeConfiguration() error {
return storeJSON(st.configuration_path, st.configuration)
return storeJSON(st.configuration_path, st.deprecatedConfiguration)
}
func (st *Storage) loadDisplayprefs() error {
return loadJSON(st.displayprefs_path, &st.displayprefs)
return loadJSON(st.displayprefs_path, &st.deprecatedDisplayprefs)
}
func (st *Storage) storeDisplayprefs() error {
return storeJSON(st.displayprefs_path, st.displayprefs)
return storeJSON(st.displayprefs_path, st.deprecatedDisplayprefs)
}
func (st *Storage) loadOmbiTemplate() error {
return loadJSON(st.ombi_path, &st.ombi_template)
return loadJSON(st.ombi_path, &st.deprecatedOmbiTemplate)
}
func (st *Storage) storeOmbiTemplate() error {
return storeJSON(st.ombi_path, st.ombi_template)
return storeJSON(st.ombi_path, st.deprecatedOmbiTemplate)
}
func (st *Storage) loadAnnouncements() error {
return loadJSON(st.announcements_path, &st.announcements)
return loadJSON(st.announcements_path, &st.deprecatedAnnouncements)
}
func (st *Storage) storeAnnouncements() error {
return storeJSON(st.announcements_path, st.announcements)
return storeJSON(st.announcements_path, st.deprecatedAnnouncements)
}
func (st *Storage) loadProfiles() error {
err := loadJSON(st.profiles_path, &st.profiles)
for name, profile := range st.profiles {
if profile.Default {
st.defaultProfile = name
}
err := loadJSON(st.profiles_path, &st.deprecatedProfiles)
for name, profile := range st.deprecatedProfiles {
// if profile.Default {
// st.defaultProfile = name
// }
change := false
if profile.Policy.IsAdministrator != profile.Admin {
change = true
@ -1107,19 +1322,19 @@ func (st *Storage) loadProfiles() error {
change = true
}
if change {
st.profiles[name] = profile
}
}
if st.defaultProfile == "" {
for n := range st.profiles {
st.defaultProfile = n
st.deprecatedProfiles[name] = profile
}
}
// if st.defaultProfile == "" {
// for n := range st.deprecatedProfiles {
// st.defaultProfile = n
// }
// }
return err
}
func (st *Storage) storeProfiles() error {
return storeJSON(st.profiles_path, st.profiles)
return storeJSON(st.profiles_path, st.deprecatedProfiles)
}
func (st *Storage) migrateToProfile() error {
@ -1127,10 +1342,10 @@ func (st *Storage) migrateToProfile() error {
st.loadConfiguration()
st.loadDisplayprefs()
st.loadProfiles()
st.profiles["Default"] = Profile{
Policy: st.policy,
Configuration: st.configuration,
Displayprefs: st.displayprefs,
st.deprecatedProfiles["Default"] = Profile{
Policy: st.deprecatedPolicy,
Configuration: st.deprecatedConfiguration,
Displayprefs: st.deprecatedDisplayprefs,
}
return st.storeProfiles()
}

View File

@ -7,6 +7,7 @@ import (
"time"
tg "github.com/go-telegram-bot-api/telegram-bot-api"
"github.com/timshannon/badgerhold/v4"
)
const (
@ -216,13 +217,10 @@ func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string
}
if _, ok := t.app.storage.lang.Telegram[sects[1]]; ok {
t.languages[upd.Message.Chat.ID] = sects[1]
for jfID, user := range t.app.storage.GetTelegram() {
for _, user := range t.app.storage.GetTelegram() {
if user.ChatID == upd.Message.Chat.ID {
user.Lang = sects[1]
t.app.storage.SetTelegramKey(jfID, user)
if err := t.app.storage.storeTelegramUsers(); err != nil {
t.app.err.Printf("Failed to store Telegram users: %v", err)
}
t.app.storage.SetTelegramKey(user.JellyfinID, user)
break
}
}
@ -270,15 +268,9 @@ func (t *TelegramDaemon) AssignedTokenVerified(pin string, jfID string) (token T
}
// UserExists returns whether or not a user with the given username exists.
func (t *TelegramDaemon) UserExists(username string) (ok bool) {
ok = false
for _, u := range t.app.storage.GetTelegram() {
if u.Username == username {
ok = true
break
}
}
return
func (t *TelegramDaemon) UserExists(username string) bool {
c, err := t.app.storage.db.Count(&TelegramUser{}, badgerhold.Where("Username").Eq(username))
return err != nil || c > 0
}
// DeleteVerifiedToken removes the token with the given PIN.

View File

@ -50,13 +50,7 @@ func (rt *userDaemon) shutdown() {
}
func (app *appContext) checkUsers() {
if err := app.storage.loadUsers(); err != nil {
app.err.Printf("Failed to load user expiries: %v", err)
return
}
app.storage.usersLock.Lock()
defer app.storage.usersLock.Unlock()
if len(app.storage.users) == 0 {
if len(app.storage.GetUserExpiries()) == 0 {
return
}
app.info.Println("Daemon: Checking for user expiry")
@ -80,11 +74,12 @@ func (app *appContext) checkUsers() {
for _, user := range users {
userExists[user.ID] = true
}
for id, expiry := range app.storage.users {
for _, expiry := range app.storage.GetUserExpiries() {
id := expiry.JellyfinID
if _, ok := userExists[id]; !ok {
app.info.Printf("Deleting expiry for non-existent user \"%s\"", id)
delete(app.storage.users, id)
} else if time.Now().After(expiry) {
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
} else if time.Now().After(expiry.Expiry) {
found := false
var user mediabrowser.User
for _, u := range users {
@ -96,7 +91,7 @@ func (app *appContext) checkUsers() {
}
if !found {
app.info.Printf("Expired user already deleted, ignoring.")
delete(app.storage.users, id)
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
continue
}
app.info.Printf("%s expired user \"%s\"", termPlural, user.Name)
@ -112,7 +107,7 @@ func (app *appContext) checkUsers() {
app.err.Printf("Failed to %s \"%s\" (%d): %s", mode, user.Name, status, err)
continue
}
delete(app.storage.users, id)
app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
app.jf.CacheExpiry = time.Now()
if contact {
if !ok {
@ -130,8 +125,4 @@ func (app *appContext) checkUsers() {
}
}
}
err = app.storage.storeUsers()
if err != nil {
app.err.Printf("Failed to store user expiries: %s", err)
}
}

View File

@ -224,13 +224,13 @@ func (app *appContext) MyUserPage(gc *gin.Context) {
data["discordInviteLink"] = app.discord.inviteChannelName != ""
}
pageMessages := map[string]*customContent{
"Login": app.getCustomMessage("UserLogin"),
"Page": app.getCustomMessage("UserPage"),
}
pageMessagesExist := map[string]bool{}
pageMessages := map[string]CustomContent{}
pageMessages["Login"], pageMessagesExist["Login"] = app.storage.GetCustomContentKey("UserLogin")
pageMessages["Page"], pageMessagesExist["Page"] = app.storage.GetCustomContentKey("UserPage")
for name, msg := range pageMessages {
if msg == nil {
if !pageMessagesExist[name] {
continue
}
data[name+"MessageEnabled"] = msg.Enabled