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

View File

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

View File

@ -71,7 +71,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
var req ombiUser var req ombiUser
gc.BindJSON(&req) gc.BindJSON(&req)
profileName := gc.Param("profile") profileName := gc.Param("profile")
profile, ok := app.storage.profiles[profileName] profile, ok := app.storage.GetProfileKey(profileName)
if !ok { if !ok {
respondBool(400, false, gc) respondBool(400, false, gc)
return return
@ -83,12 +83,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
return return
} }
profile.Ombi = template profile.Ombi = template
app.storage.profiles[profileName] = profile app.storage.SetProfileKey(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
}
respondBool(204, true, gc) respondBool(204, true, gc)
} }
@ -103,17 +98,12 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
// @tags Ombi // @tags Ombi
func (app *appContext) DeleteOmbiProfile(gc *gin.Context) { func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
profileName := gc.Param("profile") profileName := gc.Param("profile")
profile, ok := app.storage.profiles[profileName] profile, ok := app.storage.GetProfileKey(profileName)
if !ok { if !ok {
respondBool(400, false, gc) respondBool(400, false, gc)
return return
} }
profile.Ombi = nil profile.Ombi = nil
app.storage.profiles[profileName] = profile app.storage.SetProfileKey(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
}
respondBool(204, true, gc) respondBool(204, true, gc)
} }

View File

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

View File

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

View File

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

View File

@ -157,15 +157,6 @@ func (app *appContext) loadConfig() error {
app.MustSetValue("updates", "channel", releaseChannel) 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("") substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
if substituteStrings != "" { if substituteStrings != "" {

View File

@ -7,85 +7,53 @@ import "time"
// the user cache is fresh. // the user cache is fresh.
func (app *appContext) clearEmails() { func (app *appContext) clearEmails() {
app.debug.Println("Housekeeping: removing unused email addresses") app.debug.Println("Housekeeping: removing unused email addresses")
users, status, err := app.jf.GetUsers(false) emails := app.storage.GetEmails()
if status != 200 || err != nil || len(users) == 0 { for _, email := range emails {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) _, status, err := app.jf.UserByID(email.JellyfinID, false)
return 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. // clearDiscord does the same as clearEmails, but for Discord Users.
func (app *appContext) clearDiscord() { func (app *appContext) clearDiscord() {
app.debug.Println("Housekeeping: removing unused Discord IDs") app.debug.Println("Housekeeping: removing unused Discord IDs")
users, status, err := app.jf.GetUsers(false) discordUsers := app.storage.GetDiscord()
if status != 200 || err != nil || len(users) == 0 { for _, discordUser := range discordUsers {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) _, status, err := app.jf.UserByID(discordUser.JellyfinID, false)
return 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. // clearMatrix does the same as clearEmails, but for Matrix Users.
func (app *appContext) clearMatrix() { func (app *appContext) clearMatrix() {
app.debug.Println("Housekeeping: removing unused Matrix IDs") app.debug.Println("Housekeeping: removing unused Matrix IDs")
users, status, err := app.jf.GetUsers(false) matrixUsers := app.storage.GetMatrix()
if status != 200 || err != nil || len(users) == 0 { for _, matrixUser := range matrixUsers {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) _, status, err := app.jf.UserByID(matrixUser.JellyfinID, false)
return 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. // clearTelegram does the same as clearEmails, but for Telegram Users.
func (app *appContext) clearTelegram() { func (app *appContext) clearTelegram() {
app.debug.Println("Housekeeping: removing unused Telegram IDs") app.debug.Println("Housekeeping: removing unused Telegram IDs")
users, status, err := app.jf.GetUsers(false) telegramUsers := app.storage.GetTelegram()
if status != 200 || err != nil || len(users) == 0 { for _, telegramUser := range telegramUsers {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) _, status, err := app.jf.UserByID(telegramUser.JellyfinID, false)
return 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 // https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
@ -148,7 +116,6 @@ func (rt *housekeepingDaemon) run() {
break break
} }
started := time.Now() started := time.Now()
rt.app.storage.loadInvites()
for _, job := range rt.jobs { for _, job := range rt.jobs {
job(rt.app) job(rt.app)

View File

@ -6,6 +6,7 @@ import (
"time" "time"
dg "github.com/bwmarrin/discordgo" dg "github.com/bwmarrin/discordgo"
"github.com/timshannon/badgerhold/v4"
) )
type DiscordDaemon struct { 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() code := i.ApplicationCommandData().Options[0].StringValue()
if _, ok := d.app.storage.lang.Telegram[code]; ok { if _, ok := d.app.storage.lang.Telegram[code]; ok {
var user DiscordUser 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 { if u.ID == i.Interaction.Member.User.ID {
u.Lang = code u.Lang = code
lang = code lang = code
d.app.storage.SetDiscordKey(jfID, u) d.app.storage.SetDiscordKey(u.JellyfinID, u)
user = u user = u
break 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 { if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
var user DiscordUser var user DiscordUser
for jfID, u := range d.app.storage.GetDiscord() { for _, u := range d.app.storage.GetDiscord() {
if u.ID == m.Author.ID { if u.ID == m.Author.ID {
u.Lang = sects[1] u.Lang = sects[1]
d.app.storage.SetDiscordKey(jfID, u) d.app.storage.SetDiscordKey(u.JellyfinID, u)
user = u user = u
break 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. // UserExists returns whether or not a user with the given ID exists.
func (d *DiscordDaemon) UserExists(id string) (ok bool) { func (d *DiscordDaemon) UserExists(id string) bool {
ok = false c, err := d.app.storage.db.Count(&DiscordUser{}, badgerhold.Where("ID").Eq(id))
for _, u := range d.app.storage.GetDiscord() { return err != nil || c > 0
if u.ID == id {
ok = true
break
}
}
return
} }
// DeleteVerifiedUser removes the token with the given PIN. // 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"
"github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/html"
"github.com/hrfee/mediabrowser"
"github.com/itchyny/timefmt-go" "github.com/itchyny/timefmt-go"
"github.com/mailgun/mailgun-go/v4" "github.com/mailgun/mailgun-go/v4"
"github.com/timshannon/badgerhold/v4"
sMail "github.com/xhit/go-simple-mail/v2" 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 var err error
template := emailer.confirmationValues(code, username, key, app, noSub) template := emailer.confirmationValues(code, username, key, app, noSub)
if app.storage.customEmails.EmailConfirmation.Enabled { message := app.storage.MustGetCustomContentKey("EmailConfirmation")
if message.Enabled {
content := templateEmail( content := templateEmail(
app.storage.customEmails.EmailConfirmation.Content, message.Content,
app.storage.customEmails.EmailConfirmation.Variables, message.Variables,
nil, nil,
template, template,
) )
@ -412,10 +415,11 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
} }
template := emailer.inviteValues(code, invite, app, noSub) template := emailer.inviteValues(code, invite, app, noSub)
var err error var err error
if app.storage.customEmails.InviteEmail.Enabled { message := app.storage.MustGetCustomContentKey("InviteEmail")
if message.Enabled {
content := templateEmail( content := templateEmail(
app.storage.customEmails.InviteEmail.Content, message.Content,
app.storage.customEmails.InviteEmail.Variables, message.Variables,
nil, nil,
template, template,
) )
@ -451,10 +455,11 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
} }
var err error var err error
template := emailer.expiryValues(code, invite, app, noSub) template := emailer.expiryValues(code, invite, app, noSub)
if app.storage.customEmails.InviteExpiry.Enabled { message := app.storage.MustGetCustomContentKey("InviteExpiry")
if message.Enabled {
content := templateEmail( content := templateEmail(
app.storage.customEmails.InviteExpiry.Content, message.Content,
app.storage.customEmails.InviteExpiry.Variables, message.Variables,
nil, nil,
template, template,
) )
@ -505,10 +510,11 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
} }
template := emailer.createdValues(code, username, address, invite, app, noSub) template := emailer.createdValues(code, username, address, invite, app, noSub)
var err error var err error
if app.storage.customEmails.UserCreated.Enabled { message := app.storage.MustGetCustomContentKey("UserCreated")
if message.Enabled {
content := templateEmail( content := templateEmail(
app.storage.customEmails.UserCreated.Content, message.Content,
app.storage.customEmails.UserCreated.Variables, message.Variables,
nil, nil,
template, template,
) )
@ -578,10 +584,11 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
} }
template := emailer.resetValues(pwr, app, noSub) template := emailer.resetValues(pwr, app, noSub)
var err error var err error
if app.storage.customEmails.PasswordReset.Enabled { message := app.storage.MustGetCustomContentKey("PasswordReset")
if message.Enabled {
content := templateEmail( content := templateEmail(
app.storage.customEmails.PasswordReset.Content, message.Content,
app.storage.customEmails.PasswordReset.Variables, message.Variables,
nil, nil,
template, template,
) )
@ -619,10 +626,11 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
} }
var err error var err error
template := emailer.deletedValues(reason, app, noSub) template := emailer.deletedValues(reason, app, noSub)
if app.storage.customEmails.UserDeleted.Enabled { message := app.storage.MustGetCustomContentKey("UserDeleted")
if message.Enabled {
content := templateEmail( content := templateEmail(
app.storage.customEmails.UserDeleted.Content, message.Content,
app.storage.customEmails.UserDeleted.Variables, message.Variables,
nil, nil,
template, template,
) )
@ -660,10 +668,11 @@ func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub
} }
var err error var err error
template := emailer.disabledValues(reason, app, noSub) template := emailer.disabledValues(reason, app, noSub)
if app.storage.customEmails.UserDisabled.Enabled { message := app.storage.MustGetCustomContentKey("UserDisabled")
if message.Enabled {
content := templateEmail( content := templateEmail(
app.storage.customEmails.UserDisabled.Content, message.Content,
app.storage.customEmails.UserDisabled.Variables, message.Variables,
nil, nil,
template, template,
) )
@ -701,10 +710,11 @@ func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub b
} }
var err error var err error
template := emailer.enabledValues(reason, app, noSub) template := emailer.enabledValues(reason, app, noSub)
if app.storage.customEmails.UserEnabled.Enabled { message := app.storage.MustGetCustomContentKey("UserEnabled")
if message.Enabled {
content := templateEmail( content := templateEmail(
app.storage.customEmails.UserEnabled.Content, message.Content,
app.storage.customEmails.UserEnabled.Variables, message.Variables,
nil, nil,
template, template,
) )
@ -756,7 +766,8 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app
} }
var err error var err error
var template map[string]interface{} 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) template = emailer.welcomeValues(username, expiry, app, noSub, true)
} else { } else {
template = emailer.welcomeValues(username, expiry, app, noSub, false) template = emailer.welcomeValues(username, expiry, app, noSub, false)
@ -766,11 +777,11 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app
"date": "{yourAccountWillExpire}", "date": "{yourAccountWillExpire}",
}) })
} }
if app.storage.customEmails.WelcomeEmail.Enabled { if message.Enabled {
content := templateEmail( content := templateEmail(
app.storage.customEmails.WelcomeEmail.Content, message.Content,
app.storage.customEmails.WelcomeEmail.Variables, message.Variables,
app.storage.customEmails.WelcomeEmail.Conditionals, message.Conditionals,
template, template,
) )
email, err = emailer.constructTemplate(email.Subject, content, app) email, err = emailer.constructTemplate(email.Subject, content, app)
@ -801,10 +812,11 @@ func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Mess
} }
var err error var err error
template := emailer.userExpiredValues(app, noSub) template := emailer.userExpiredValues(app, noSub)
if app.storage.customEmails.UserExpired.Enabled { message := app.storage.MustGetCustomContentKey("UserExpired")
if message.Enabled {
content := templateEmail( content := templateEmail(
app.storage.customEmails.UserExpired.Content, message.Content,
app.storage.customEmails.UserExpired.Variables, message.Variables,
nil, nil,
template, 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. // 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. // 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) user, status, err := app.jf.UserByName(address, false)
if status == 200 && err == nil { if status == 200 && err == nil {
return user.ID ok = true
return
} }
for id, email := range app.storage.GetEmails() { emailAddresses := []EmailAddress{}
if strings.ToLower(address) == strings.ToLower(email.Addr) { err = app.storage.db.Find(&emailAddresses, badgerhold.Where("Addr").Eq(address))
return id 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) { 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, "@") tgUsername := strings.TrimPrefix(address, "@")
for id, tgUser := range app.storage.GetTelegram() { telegramUsers := []TelegramUser{}
if tgUsername == tgUser.Username { err = app.storage.db.Find(&telegramUsers, badgerhold.Where("Username").Eq(tgUsername))
return id 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() { matrixUsers := []MatrixUser{}
if address == mxUser.UserID { err = app.storage.db.Find(&matrixUsers, badgerhold.Where("UserID").Eq(address))
return id 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 ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.9.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/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/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
github.com/getlantern/errors v1.0.3 // 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-stack/stack v1.8.1 // indirect
github.com/go-test/deep v1.1.0 // indirect github.com/go-test/deep v1.1.0 // indirect
github.com/goccy/go-json v0.10.2 // 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/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/google/uuid v1.3.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // 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/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // 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/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // 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 v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/trace 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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 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 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 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.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/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/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/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 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= 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.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 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 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-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 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 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/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/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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:M88ob4TyDnEqNuL3PgsE/p3bDujfspnulR+0dQWNYZs=
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:buzQsO8HHkZX2Q45fdfGH1xejPjuDQaXH8btcYMFzPM= 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 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= 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= 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.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 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 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 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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/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 h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 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 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 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.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.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.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.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 h1:AWZzzFrqyjYlRloN6edwTLTUbKxf5flLXNuTBDm3Ews=
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= 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.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.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 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/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.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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.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 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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 h1:y0iBCb6jE3QKcsiCJSYva2fFPHRn4UA+sGRzoPuJ/Dk=
github.com/hrfee/mediabrowser v0.3.8/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U= 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 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= 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= 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.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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.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 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 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/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/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/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 h1:wRbxvVQ5QObFewLxc1uVvipA16D8gxeiO+cBOca51Iw=
github.com/mailgun/mailgun-go/v4 v4.9.0/go.mod h1:FJlF9rI5cQT+mrwujtJjPMbIVy3Ebor9bKTVsJ0QU40= 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= 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 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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/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 h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= 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.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 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc=
github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE= 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= 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/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 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= 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/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/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/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/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 h1:OmMdxLCWCqJvsFaFYwRpvMckIuvI6s8s1LsBrBw97P0=
github.com/steambap/captcha v1.4.1/go.mod h1:oC9T7IfEgnrhzjDz5Djf1H7GPffCzRMbsQfFkJmhlnk= github.com/steambap/captcha v1.4.1/go.mod h1:oC9T7IfEgnrhzjDz5Djf1H7GPffCzRMbsQfFkJmhlnk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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/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 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 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 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= 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= 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.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 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-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.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.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 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/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 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI=
github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98= 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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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.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 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= 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.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 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 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-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-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-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-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-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.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 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= 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 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 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.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 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= 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/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.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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= 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-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-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-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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-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-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-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-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-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.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.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.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 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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-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-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-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.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/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-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-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-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-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-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-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-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-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-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-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-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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 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-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-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-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-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-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-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.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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= 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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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-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.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 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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/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 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-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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
maunium.net/go/mautrix v0.15.2 h1:fUiVajeoOR92uJoSShHbCvh7uG6lDY4ZO4Mvt90LbjU= maunium.net/go/mautrix v0.15.2 h1:fUiVajeoOR92uJoSShHbCvh7uG6lDY4ZO4Mvt90LbjU=

View File

@ -79,7 +79,7 @@
"ombiProfile": "Ombi user profile", "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.", "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", "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", "userProfilesIsDefault": "Default",
"userProfilesLibraries": "Libraries", "userProfilesLibraries": "Libraries",
"addProfile": "Add Profile", "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.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) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
app.storage.ombi_path = app.config.Section("files").Key("ombi_template").String() app.debug.Printf("Connecting to Ombi")
app.storage.loadOmbiTemplate()
ombiServer := app.config.Section("ombi").Key("server").String() ombiServer := app.config.Section("ombi").Key("server").String()
app.ombi = ombi.NewOmbi( app.ombi = ombi.NewOmbi(
ombiServer, 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. // Read config-base for settings on web.
app.configBasePath = "config-base.json" app.configBasePath = "config-base.json"
configBase, _ := fs.ReadFile(localFS, app.configBasePath) configBase, _ := fs.ReadFile(localFS, app.configBasePath)

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown"
"github.com/timshannon/badgerhold/v4"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
@ -31,11 +32,12 @@ type UnverifiedUser struct {
} }
type MatrixUser struct { type MatrixUser struct {
RoomID string RoomID string
Encrypted bool Encrypted bool
UserID string UserID string
Lang string Lang string
Contact bool Contact bool
JellyfinID string `badgerhold:"key"`
} }
var matrixFilter = mautrix.Filter{ var matrixFilter = mautrix.Filter{
@ -268,6 +270,12 @@ func (d *MatrixDaemon) Send(message *Message, users ...MatrixUser) (err error) {
return 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. // User enters ID on sign-up, a PIN is sent to them. They enter it on sign-up.
// Message the user first, to avoid E2EE by default // Message the user first, to avoid E2EE by default

View File

@ -16,26 +16,28 @@ func runMigrations(app *appContext) {
migrateNotificationMethods(app) migrateNotificationMethods(app)
linkExistingOmbiDiscordTelegram(app) linkExistingOmbiDiscordTelegram(app)
// migrateHyphens(app) // migrateHyphens(app)
migrateToBadger(app)
} }
// Migrate pre-0.2.0 user templates to profiles // Migrate pre-0.2.0 user templates to profiles
func migrateProfiles(app *appContext) { 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 {
app.info.Println("Migrating user template files to new profile format") return
app.storage.migrateToProfile() }
for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} { app.info.Println("Migrating user template files to new profile format")
if _, err := os.Stat(path); !os.IsNotExist(err) { app.storage.migrateToProfile()
dir, fname := filepath.Split(path) for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} {
newFname := strings.Replace(fname, ".json", ".old.json", 1) if _, err := os.Stat(path); !os.IsNotExist(err) {
err := os.Rename(path, filepath.Join(dir, newFname)) dir, fname := filepath.Split(path)
if err != nil { newFname := strings.Replace(fname, ".json", ".old.json", 1)
app.err.Fatalf("Failed to rename %s: %s", fname, err) err := os.Rename(path, filepath.Join(dir, newFname))
} if err != nil {
app.err.Fatalf("Failed to rename %s: %s", fname, err)
} }
} }
app.info.Println("In case of a problem, your original files have been renamed to <file>.old.json")
app.storage.storeProfiles()
} }
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. // Migrate pre-0.2.5 bootstrap theme choice to a17t version.
@ -131,7 +133,7 @@ func migrateNotificationMethods(app *appContext) error {
return nil return nil
} }
changes := false changes := false
for code, invite := range app.storage.invites { for code, invite := range app.storage.deprecatedInvites {
if invite.Notify == nil { if invite.Notify == nil {
continue continue
} }
@ -139,9 +141,9 @@ func migrateNotificationMethods(app *appContext) error {
if !strings.Contains(address, "@") { if !strings.Contains(address, "@") {
continue continue
} }
for id, email := range app.storage.GetEmails() { for _, email := range app.storage.GetEmails() {
if email.Addr == address { if email.Addr == address {
invite.Notify[id] = notifyPrefs invite.Notify[email.JellyfinID] = notifyPrefs
delete(invite.Notify, address) delete(invite.Notify, address)
changes = true changes = true
break break
@ -149,7 +151,7 @@ func migrateNotificationMethods(app *appContext) error {
} }
} }
if changes { if changes {
app.storage.invites[code] = invite app.storage.deprecatedInvites[code] = invite
} }
} }
if changes { if changes {
@ -168,16 +170,16 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
return nil return nil
} }
idList := map[string][2]string{} idList := map[string][2]string{}
for jfID, user := range app.storage.GetDiscord() { for _, user := range app.storage.GetDiscord() {
idList[jfID] = [2]string{user.ID, ""} idList[user.JellyfinID] = [2]string{user.ID, ""}
} }
for jfID, user := range app.storage.GetTelegram() { for _, user := range app.storage.GetTelegram() {
vals, ok := idList[jfID] vals, ok := idList[user.JellyfinID]
if !ok { if !ok {
vals = [2]string{"", ""} vals = [2]string{"", ""}
} }
vals[1] = user.Username vals[1] = user.Username
idList[jfID] = vals idList[user.JellyfinID] = vals
} }
for jfID, ids := range idList { for jfID, ids := range idList {
ombiUser, status, err := app.getOmbiUser(jfID) ombiUser, status, err := app.getOmbiUser(jfID)
@ -194,6 +196,143 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
return nil 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. // Migrate between hyphenated & non-hyphenated user IDs. Doesn't seem to happen anymore, so disabled.
// func migrateHyphens(app *appContext) { // func migrateHyphens(app *appContext) {
// checkVersion := func(version string) int { // checkVersion := func(version string) int {
@ -212,8 +351,8 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
// app.jf.GetUsers(false) // app.jf.GetUsers(false)
// //
// noHyphens := true // noHyphens := true
// for id := range app.storage.GetEmails() { // for _, e := range app.storage.GetEmails() {
// if strings.Contains(id, "-") { // if strings.Contains(e.JellyfinID, "-") {
// noHyphens = false // noHyphens = false
// break // break
// } // }
@ -255,7 +394,7 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
// app.storage.emails = newEmails // app.storage.emails = newEmails
// app.storage.users = newUsers // app.storage.users = newUsers
// err = app.storage.storeEmails() // err = app.storage.storeEmails()
// err2 = app.storage.storeUsers() // err2 = app.storage.storeUserExpiries()
// if err != nil { // if err != nil {
// app.err.Fatalf("couldn't store emails.json: %v", err) // 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) app.debug.Printf("Error: %s", err)
return return
} }
app.storage.loadEmails()
uid := user.ID uid := user.ID
if uid == "" { if uid == "" {
app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username) app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username)

View File

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

View File

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

View File

@ -50,13 +50,7 @@ func (rt *userDaemon) shutdown() {
} }
func (app *appContext) checkUsers() { func (app *appContext) checkUsers() {
if err := app.storage.loadUsers(); err != nil { if len(app.storage.GetUserExpiries()) == 0 {
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 {
return return
} }
app.info.Println("Daemon: Checking for user expiry") app.info.Println("Daemon: Checking for user expiry")
@ -80,11 +74,12 @@ func (app *appContext) checkUsers() {
for _, user := range users { for _, user := range users {
userExists[user.ID] = true 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 { if _, ok := userExists[id]; !ok {
app.info.Printf("Deleting expiry for non-existent user \"%s\"", id) app.info.Printf("Deleting expiry for non-existent user \"%s\"", id)
delete(app.storage.users, id) app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
} else if time.Now().After(expiry) { } else if time.Now().After(expiry.Expiry) {
found := false found := false
var user mediabrowser.User var user mediabrowser.User
for _, u := range users { for _, u := range users {
@ -96,7 +91,7 @@ func (app *appContext) checkUsers() {
} }
if !found { if !found {
app.info.Printf("Expired user already deleted, ignoring.") app.info.Printf("Expired user already deleted, ignoring.")
delete(app.storage.users, id) app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
continue continue
} }
app.info.Printf("%s expired user \"%s\"", termPlural, user.Name) 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) app.err.Printf("Failed to %s \"%s\" (%d): %s", mode, user.Name, status, err)
continue continue
} }
delete(app.storage.users, id) app.storage.DeleteUserExpiryKey(expiry.JellyfinID)
app.jf.CacheExpiry = time.Now() app.jf.CacheExpiry = time.Now()
if contact { if contact {
if !ok { 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 != "" data["discordInviteLink"] = app.discord.inviteChannelName != ""
} }
pageMessages := map[string]*customContent{ pageMessagesExist := map[string]bool{}
"Login": app.getCustomMessage("UserLogin"), pageMessages := map[string]CustomContent{}
"Page": app.getCustomMessage("UserPage"), pageMessages["Login"], pageMessagesExist["Login"] = app.storage.GetCustomContentKey("UserLogin")
} pageMessages["Page"], pageMessagesExist["Page"] = app.storage.GetCustomContentKey("UserPage")
for name, msg := range pageMessages { for name, msg := range pageMessages {
if msg == nil { if !pageMessagesExist[name] {
continue continue
} }
data[name+"MessageEnabled"] = msg.Enabled data[name+"MessageEnabled"] = msg.Enabled