diff --git a/api-invites.go b/api-invites.go index 268fdac..4d94a0d 100644 --- a/api-invites.go +++ b/api-invites.go @@ -10,21 +10,20 @@ import ( "github.com/gin-gonic/gin" "github.com/itchyny/timefmt-go" "github.com/lithammer/shortuuid/v3" + "github.com/timshannon/badgerhold/v4" ) func (app *appContext) checkInvites() { currentTime := time.Now() - app.storage.loadInvites() - changed := false - for code, data := range app.storage.GetInvites() { + for _, data := range app.storage.GetInvites() { expiry := data.ValidTill if !currentTime.After(expiry) { continue } - app.debug.Printf("Housekeeping: Deleting old invite %s", code) + app.debug.Printf("Housekeeping: Deleting old invite %s", data.Code) notify := data.Notify if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 { - app.debug.Printf("%s: Expiry notification", code) + app.debug.Printf("%s: Expiry notification", data.Code) var wait sync.WaitGroup for address, settings := range notify { if !settings["notify-expiry"] { @@ -33,9 +32,9 @@ func (app *appContext) checkInvites() { wait.Add(1) go func(addr string) { defer wait.Done() - msg, err := app.email.constructExpiry(code, data, app, false) + msg, err := app.email.constructExpiry(data.Code, data, app, false) if err != nil { - app.err.Printf("%s: Failed to construct expiry notification: %v", code, err) + app.err.Printf("%s: Failed to construct expiry notification: %v", data.Code, err) } else { // Check whether notify "address" is an email address of Jellyfin ID if strings.Contains(addr, "@") { @@ -44,7 +43,7 @@ func (app *appContext) checkInvites() { err = app.sendByID(msg, addr) } if err != nil { - app.err.Printf("%s: Failed to send expiry notification: %v", code, err) + app.err.Printf("%s: Failed to send expiry notification: %v", data.Code, err) } else { app.info.Printf("Sent expiry notification to %s", addr) } @@ -53,18 +52,12 @@ func (app *appContext) checkInvites() { } wait.Wait() } - changed = true - app.storage.DeleteInvitesKey(code) - } - if changed { - app.storage.storeInvites() + app.storage.DeleteInvitesKey(data.Code) } } func (app *appContext) checkInvite(code string, used bool, username string) bool { currentTime := time.Now() - app.storage.loadInvites() - changed := false inv, match := app.storage.GetInvitesKey(code) if !match { return false @@ -103,11 +96,9 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool } wait.Wait() } - changed = true match = false app.storage.DeleteInvitesKey(code) } else if used { - changed = true del := false newInv := inv if newInv.RemainingUses == 1 { @@ -122,9 +113,6 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool app.storage.SetInvitesKey(code, newInv) } } - if changed { - app.storage.storeInvites() - } return match } @@ -138,7 +126,6 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool func (app *appContext) GenerateInvite(gc *gin.Context) { var req generateInviteDTO app.debug.Println("Generating new invite") - app.storage.loadInvites() gc.BindJSON(&req) currentTime := time.Now() validTill := currentTime.AddDate(0, req.Months, req.Days) @@ -213,14 +200,13 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { } } if req.Profile != "" { - if _, ok := app.storage.profiles[req.Profile]; ok { + if _, ok := app.storage.GetProfileKey(req.Profile); ok { invite.Profile = req.Profile } else { invite.Profile = "Default" } } app.storage.SetInvitesKey(inviteCode, invite) - app.storage.storeInvites() respondBool(200, true, gc) } @@ -233,13 +219,12 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { func (app *appContext) GetInvites(gc *gin.Context) { app.debug.Println("Invites requested") currentTime := time.Now() - app.storage.loadInvites() app.checkInvites() var invites []inviteDTO - for code, inv := range app.storage.GetInvites() { + for _, inv := range app.storage.GetInvites() { _, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime) invite := inviteDTO{ - Code: code, + Code: inv.Code, Months: months, Days: days, Hours: hours, @@ -277,37 +262,36 @@ func (app *appContext) GetInvites(gc *gin.Context) { invite.SendTo = inv.SendTo } if len(inv.Notify) != 0 { - var address string + // app.err.Printf("%s has notify section: %+v, you are %s\n", inv.Code, inv.Notify, gc.GetString("jfId")) + var addressOrID string if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { - app.storage.loadEmails() - if addr, ok := app.storage.GetEmailsKey(gc.GetString("jfId")); ok && addr.Addr != "" { - address = addr.Addr - } + addressOrID = gc.GetString("jfId") } else { - address = app.config.Section("ui").Key("email").String() + addressOrID = app.config.Section("ui").Key("email").String() } - if _, ok := inv.Notify[address]; ok { - if _, ok = inv.Notify[address]["notify-expiry"]; ok { - invite.NotifyExpiry = inv.Notify[address]["notify-expiry"] + if _, ok := inv.Notify[addressOrID]; ok { + if _, ok = inv.Notify[addressOrID]["notify-expiry"]; ok { + invite.NotifyExpiry = inv.Notify[addressOrID]["notify-expiry"] } - if _, ok = inv.Notify[address]["notify-creation"]; ok { - invite.NotifyCreation = inv.Notify[address]["notify-creation"] + if _, ok = inv.Notify[addressOrID]["notify-creation"]; ok { + invite.NotifyCreation = inv.Notify[addressOrID]["notify-creation"] } } } invites = append(invites, invite) } - profiles := make([]string, len(app.storage.profiles)) - if len(app.storage.profiles) != 0 { - profiles[0] = app.storage.defaultProfile + fullProfileList := app.storage.GetProfiles() + profiles := make([]string, len(fullProfileList)) + if len(profiles) != 0 { + defaultProfile := app.storage.GetDefaultProfile() + profiles[0] = defaultProfile.Name i := 1 - if len(app.storage.profiles) > 1 { - for p := range app.storage.profiles { - if p != app.storage.defaultProfile { - profiles[i] = p - i++ - } - } + if len(fullProfileList) > 1 { + app.storage.db.ForEach(badgerhold.Where("Name").Ne(profiles[0]), func(p *Profile) error { + profiles[i] = p.Name + i++ + return nil + }) } } resp := getInvitesDTO{ @@ -330,7 +314,7 @@ func (app *appContext) SetProfile(gc *gin.Context) { gc.BindJSON(&req) app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile) // "" means "Don't apply profile" - if _, ok := app.storage.profiles[req.Profile]; !ok && req.Profile != "" { + if _, ok := app.storage.GetProfileKey(req.Profile); !ok && req.Profile != "" { app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile) respond(500, "Profile not found", gc) return @@ -338,7 +322,6 @@ func (app *appContext) SetProfile(gc *gin.Context) { inv, _ := app.storage.GetInvitesKey(req.Invite) inv.Profile = req.Profile app.storage.SetInvitesKey(req.Invite, inv) - app.storage.storeInvites() respondBool(200, true, gc) } @@ -357,8 +340,6 @@ func (app *appContext) SetNotify(gc *gin.Context) { changed := false for code, settings := range req { app.debug.Printf("%s: Notification settings change requested", code) - app.storage.loadInvites() - app.storage.loadEmails() invite, ok := app.storage.GetInvitesKey(code) if !ok { app.err.Printf("%s Notification setting change failed: Invalid code", code) @@ -401,9 +382,6 @@ func (app *appContext) SetNotify(gc *gin.Context) { app.storage.SetInvitesKey(code, invite) } } - if changed { - app.storage.storeInvites() - } } // @Summary Delete an invite. @@ -422,7 +400,6 @@ func (app *appContext) DeleteInvite(gc *gin.Context) { _, ok = app.storage.GetInvitesKey(req.Code) if ok { app.storage.DeleteInvitesKey(req.Code) - app.storage.storeInvites() app.info.Printf("%s: Invite deleted", req.Code) respondBool(200, true, gc) return diff --git a/api-messages.go b/api-messages.go index c571eb7..9973907 100644 --- a/api-messages.go +++ b/api-messages.go @@ -25,18 +25,18 @@ func (app *appContext) GetCustomContent(gc *gin.Context) { adminLang = app.storage.lang.chosenAdminLang } list := emailListDTO{ - "UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.customEmails.UserCreated.Enabled}, - "InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.customEmails.InviteExpiry.Enabled}, - "PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.customEmails.PasswordReset.Enabled}, - "UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.customEmails.UserDeleted.Enabled}, - "UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.customEmails.UserDisabled.Enabled}, - "UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.customEmails.UserEnabled.Enabled}, - "InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.customEmails.InviteEmail.Enabled}, - "WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.customEmails.WelcomeEmail.Enabled}, - "EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.customEmails.EmailConfirmation.Enabled}, - "UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.customEmails.UserExpired.Enabled}, - "UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.userPage.Login.Enabled}, - "UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.userPage.Page.Enabled}, + "UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.MustGetCustomContentKey("UserCreated").Enabled}, + "InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.MustGetCustomContentKey("InviteExpiry").Enabled}, + "PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.MustGetCustomContentKey("PasswordReset").Enabled}, + "UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.MustGetCustomContentKey("UserDeleted").Enabled}, + "UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserDisabled").Enabled}, + "UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserEnabled").Enabled}, + "InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.MustGetCustomContentKey("InviteEmail").Enabled}, + "WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.MustGetCustomContentKey("WelcomeEmail").Enabled}, + "EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.MustGetCustomContentKey("EmailConfirmation").Enabled}, + "UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpired").Enabled}, + "UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.MustGetCustomContentKey("Login").Enabled}, + "UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.MustGetCustomContentKey("Page").Enabled}, } filter := gc.Query("filter") @@ -50,10 +50,11 @@ func (app *appContext) GetCustomContent(gc *gin.Context) { gc.JSON(200, list) } -func (app *appContext) getCustomMessage(id string) *customContent { +// No longer needed, these are stored by string keys in the database now. +/* func (app *appContext) getCustomMessage(id string) *CustomContent { switch id { case "Announcement": - return &customContent{} + return &CustomContent{} case "UserCreated": return &app.storage.customEmails.UserCreated case "InviteExpiry": @@ -80,45 +81,38 @@ func (app *appContext) getCustomMessage(id string) *customContent { return &app.storage.userPage.Page } return nil -} +} */ -// @Summary Sets the corresponding custom email. +// @Summary Sets the corresponding custom content. // @Produce json -// @Param customEmails body customEmails true "Content = email (in markdown)." +// @Param CustomContent body CustomContent true "Content = email (in markdown)." // @Success 200 {object} boolResponse // @Failure 400 {object} boolResponse // @Failure 500 {object} boolResponse -// @Param id path string true "ID of email" +// @Param id path string true "ID of content" // @Router /config/emails/{id} [post] // @Security Bearer // @tags Configuration func (app *appContext) SetCustomMessage(gc *gin.Context) { - var req customContent + var req CustomContent gc.BindJSON(&req) id := gc.Param("id") if req.Content == "" { respondBool(400, false, gc) return } - message := app.getCustomMessage(id) - if message == nil { + message, ok := app.storage.GetCustomContentKey(id) + if !ok { respondBool(400, false, gc) return } message.Content = req.Content message.Enabled = true - if app.storage.storeCustomEmails() != nil { - respondBool(500, false, gc) - return - } - if app.storage.storeUserPageContent() != nil { - respondBool(500, false, gc) - return - } + app.storage.SetCustomContentKey(id, message) respondBool(200, true, gc) } -// @Summary Enable/Disable custom email. +// @Summary Enable/Disable custom content. // @Produce json // @Success 200 {object} boolResponse // @Failure 400 {object} boolResponse @@ -137,24 +131,17 @@ func (app *appContext) SetCustomMessageState(gc *gin.Context) { } else if s != "disable" { respondBool(400, false, gc) } - message := app.getCustomMessage(id) - if message == nil { + message, ok := app.storage.GetCustomContentKey(id) + if !ok { respondBool(400, false, gc) return } message.Enabled = enabled - if app.storage.storeCustomEmails() != nil { - respondBool(500, false, gc) - return - } - if app.storage.storeUserPageContent() != nil { - respondBool(500, false, gc) - return - } + app.storage.SetCustomContentKey(id, message) respondBool(200, true, gc) } -// @Summary Returns the custom email/message (generating it if not set) and list of used variables in it. +// @Summary Returns the custom content/message (generating it if not set) and list of used variables in it. // @Produce json // @Success 200 {object} customEmailDTO // @Failure 400 {object} boolResponse @@ -174,8 +161,8 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) { var values map[string]interface{} username := app.storage.lang.Email[lang].Strings.get("username") emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress") - customMessage := app.getCustomMessage(id) - if customMessage == nil { + customMessage, ok := app.storage.GetCustomContentKey(id) + if !ok { app.err.Printf("Failed to get custom message with ID \"%s\"", id) respondBool(400, false, gc) return @@ -280,13 +267,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) { if variables == nil { variables = []string{} } - if app.storage.storeCustomEmails() != nil { - respondBool(500, false, gc) - return - } - if app.storage.storeUserPageContent() != nil { - respondBool(500, false, gc) - } + app.storage.SetCustomContentKey(id, customMessage) var mail *Message if id != "UserLogin" && id != "UserPage" { mail, err = app.email.constructTemplate("", "
", app) @@ -387,11 +368,6 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte change := dcUser.Contact != req.Discord dcUser.Contact = req.Discord app.storage.SetDiscordKey(req.ID, dcUser) - if err := app.storage.storeDiscordUsers(); err != nil { - respondBool(500, false, gc) - app.err.Printf("Discord: Failed to store users: %v", err) - return - } if change { msg := "" if !req.Discord { @@ -404,11 +380,6 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte change := mxUser.Contact != req.Matrix mxUser.Contact = req.Matrix app.storage.SetMatrixKey(req.ID, mxUser) - if err := app.storage.storeMatrixUsers(); err != nil { - respondBool(500, false, gc) - app.err.Printf("Matrix: Failed to store users: %v", err) - return - } if change { msg := "" if !req.Matrix { @@ -421,11 +392,6 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte change := email.Contact != req.Email email.Contact = req.Email app.storage.SetEmailsKey(req.ID, email) - if err := app.storage.storeEmails(); err != nil { - respondBool(500, false, gc) - app.err.Printf("Failed to store emails: %v", err) - return - } if change { msg := "" if !req.Email { @@ -646,7 +612,7 @@ func (app *appContext) MatrixConnect(gc *gin.Context) { var req MatrixConnectUserDTO gc.BindJSON(&req) if app.storage.GetMatrix() == nil { - app.storage.matrix = matrixStore{} + app.storage.deprecatedMatrix = matrixStore{} } roomID, encrypted, err := app.matrix.CreateRoom(req.UserID) if err != nil { @@ -662,11 +628,6 @@ func (app *appContext) MatrixConnect(gc *gin.Context) { Encrypted: encrypted, }) app.matrix.isEncrypted[roomID] = encrypted - if err := app.storage.storeMatrixUsers(); err != nil { - app.err.Printf("Failed to store Matrix users: %v", err) - respondBool(500, false, gc) - return - } respondBool(200, true, gc) } @@ -717,11 +678,6 @@ func (app *appContext) DiscordConnect(gc *gin.Context) { return } app.storage.SetDiscordKey(req.JellyfinID, user) - if err := app.storage.storeDiscordUsers(); err != nil { - app.err.Printf("Failed to store Discord users: %v", err) - respondBool(500, false, gc) - return - } linkExistingOmbiDiscordTelegram(app) respondBool(200, true, gc) } diff --git a/api-ombi.go b/api-ombi.go index 40a8f72..0113885 100644 --- a/api-ombi.go +++ b/api-ombi.go @@ -71,7 +71,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) { var req ombiUser gc.BindJSON(&req) profileName := gc.Param("profile") - profile, ok := app.storage.profiles[profileName] + profile, ok := app.storage.GetProfileKey(profileName) if !ok { respondBool(400, false, gc) return @@ -83,12 +83,7 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) { return } profile.Ombi = template - app.storage.profiles[profileName] = profile - if err := app.storage.storeProfiles(); err != nil { - respond(500, "Failed to store profile", gc) - app.err.Printf("Failed to store profiles: %v", err) - return - } + app.storage.SetProfileKey(profileName, profile) respondBool(204, true, gc) } @@ -103,17 +98,12 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) { // @tags Ombi func (app *appContext) DeleteOmbiProfile(gc *gin.Context) { profileName := gc.Param("profile") - profile, ok := app.storage.profiles[profileName] + profile, ok := app.storage.GetProfileKey(profileName) if !ok { respondBool(400, false, gc) return } profile.Ombi = nil - app.storage.profiles[profileName] = profile - if err := app.storage.storeProfiles(); err != nil { - respond(500, "Failed to store profile", gc) - app.err.Printf("Failed to store profiles: %v", err) - return - } + app.storage.SetProfileKey(profileName, profile) respondBool(204, true, gc) } diff --git a/api-profiles.go b/api-profiles.go index 475857d..579cdff 100644 --- a/api-profiles.go +++ b/api-profiles.go @@ -4,6 +4,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/timshannon/badgerhold/v4" ) // @Summary Get a list of profiles @@ -13,14 +14,13 @@ import ( // @Security Bearer // @tags Profiles & Settings func (app *appContext) GetProfiles(gc *gin.Context) { - app.storage.loadProfiles() app.debug.Println("Profiles requested") out := getProfilesDTO{ - DefaultProfile: app.storage.defaultProfile, + DefaultProfile: app.storage.GetDefaultProfile().Name, Profiles: map[string]profileDTO{}, } - for name, p := range app.storage.profiles { - out.Profiles[name] = profileDTO{ + for _, p := range app.storage.GetProfiles() { + out.Profiles[p.Name] = profileDTO{ Admin: p.Admin, LibraryAccess: p.LibraryAccess, FromUser: p.FromUser, @@ -42,20 +42,20 @@ func (app *appContext) SetDefaultProfile(gc *gin.Context) { req := profileChangeDTO{} gc.BindJSON(&req) app.info.Printf("Setting default profile to \"%s\"", req.Name) - if _, ok := app.storage.profiles[req.Name]; !ok { + if _, ok := app.storage.GetProfileKey(req.Name); !ok { app.err.Printf("Profile not found: \"%s\"", req.Name) respond(500, "Profile not found", gc) return } - for name, profile := range app.storage.profiles { - if name == req.Name { - profile.Admin = true - app.storage.profiles[name] = profile + app.storage.db.ForEach(&badgerhold.Query{}, func(profile *Profile) error { + if profile.Name == req.Name { + profile.Default = true } else { - profile.Admin = false + profile.Default = false } - } - app.storage.defaultProfile = req.Name + app.storage.SetProfileKey(profile.Name, *profile) + return nil + }) respondBool(200, true, gc) } @@ -92,10 +92,7 @@ func (app *appContext) CreateProfile(gc *gin.Context) { return } } - app.storage.loadProfiles() - app.storage.profiles[req.Name] = profile - app.storage.storeProfiles() - app.storage.loadProfiles() + app.storage.SetProfileKey(req.Name, profile) respondBool(200, true, gc) } @@ -110,12 +107,6 @@ func (app *appContext) DeleteProfile(gc *gin.Context) { req := profileChangeDTO{} gc.BindJSON(&req) name := req.Name - if _, ok := app.storage.profiles[name]; ok { - if app.storage.defaultProfile == name { - app.storage.defaultProfile = "" - } - delete(app.storage.profiles, name) - } - app.storage.storeProfiles() + app.storage.DeleteProfileKey(name) respondBool(200, true, gc) } diff --git a/api-userpage.go b/api-userpage.go index 7996157..a517ee8 100644 --- a/api-userpage.go +++ b/api-userpage.go @@ -38,15 +38,10 @@ func (app *appContext) MyDetails(gc *gin.Context) { } resp.Disabled = user.Policy.IsDisabled - if exp, ok := app.storage.users[user.ID]; ok { - resp.Expiry = exp.Unix() + if exp, ok := app.storage.GetUserExpiryKey(user.ID); ok { + resp.Expiry = exp.Expiry.Unix() } - app.storage.loadEmails() - app.storage.loadDiscordUsers() - app.storage.loadMatrixUsers() - app.storage.loadTelegramUsers() - if emailEnabled { resp.Email = &MyDetailsContactMethodsDTO{} if email, ok := app.storage.GetEmailsKey(user.ID); ok && email.Addr != "" { @@ -199,7 +194,6 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) { } } - app.storage.storeEmails() app.info.Println("Email list modified") gc.Redirect(http.StatusSeeOther, "/my/account") return @@ -511,8 +505,8 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) { var pwr InternalPWR var err error - jfID := app.ReverseUserSearch(address) - if jfID == "" { + jfUser, ok := app.ReverseUserSearch(address) + if !ok { app.debug.Printf("Ignoring PWR request: User not found") for range timerWait { @@ -521,7 +515,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) { } return } - pwr, err = app.GenInternalReset(jfID) + pwr, err = app.GenInternalReset(jfUser.ID) if err != nil { app.err.Printf("Failed to get user from Jellyfin: %v", err) for range timerWait { @@ -550,7 +544,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) { return } return - } else if err := app.sendByID(msg, jfID); err != nil { + } else if err := app.sendByID(msg, jfUser.ID); err != nil { app.err.Printf("Failed to send password reset message to \"%s\": %v", address, err) } else { app.info.Printf("Sent password reset message to \"%s\"", address) diff --git a/api-users.go b/api-users.go index 18b4a10..5a01915 100644 --- a/api-users.go +++ b/api-users.go @@ -44,16 +44,16 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { return } id := user.ID - if app.storage.policy.BlockedTags != nil { - status, err = app.jf.SetPolicy(id, app.storage.policy) + profile := app.storage.GetDefaultProfile() + // Check profile isn't empty + if profile.Policy.BlockedTags != nil { + status, err = app.jf.SetPolicy(id, profile.Policy) if !(status == 200 || status == 204 || err == nil) { app.err.Printf("%s: Failed to set user policy (%d): %v", req.Username, status, err) } - } - if app.storage.configuration.GroupedFolders != nil && len(app.storage.displayprefs) != 0 { - status, err = app.jf.SetConfiguration(id, app.storage.configuration) + status, err = app.jf.SetConfiguration(id, profile.Configuration) if (status == 200 || status == 204) && err == nil { - status, err = app.jf.SetDisplayPreferences(id, app.storage.displayprefs) + status, err = app.jf.SetDisplayPreferences(id, profile.Displayprefs) } if !((status == 200 || status == 204) && err == nil) { app.err.Printf("%s: Failed to set configuration template (%d): %v", req.Username, status, err) @@ -62,18 +62,18 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { app.jf.CacheExpiry = time.Now() if emailEnabled { app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true}) - app.storage.storeEmails() } if app.config.Section("ombi").Key("enabled").MustBool(false) { - app.storage.loadOmbiTemplate() - if len(app.storage.ombi_template) != 0 { - errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, app.storage.ombi_template) - if err != nil || code != 200 { - app.err.Printf("Failed to create Ombi user (%d): %v", code, err) - app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", ")) - } else { - app.info.Println("Created Ombi user") - } + profile := app.storage.GetDefaultProfile() + if profile.Ombi == nil { + profile.Ombi = map[string]interface{}{} + } + errors, code, err := app.ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi) + if err != nil || code != 200 { + app.err.Printf("Failed to create Ombi user (%d): %v", code, err) + app.debug.Printf("Errors reported by Ombi: %s", strings.Join(errors, ", ")) + } else { + app.info.Println("Created Ombi user") } } 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 return } - if app.config.Section("discord").Key("require_unique").MustBool(false) { - for _, u := range app.storage.GetDiscord() { - if discordUser.ID == u.ID { - f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Discord user already linked", req.Code) - respond(400, "errorAccountLinked", gc) - } - success = false - return - } + if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(discordUser.ID) { + f = func(gc *gin.Context) { + app.debug.Printf("%s: New user failed: Discord user already linked", req.Code) + respond(400, "errorAccountLinked", gc) } + success = false + return } err := app.discord.ApplyRole(discordUser.ID) if err != nil { @@ -176,17 +172,13 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc success = false return } - if app.config.Section("matrix").Key("require_unique").MustBool(false) { - for _, u := range app.storage.GetMatrix() { - if user.User.UserID == u.UserID { - f = func(gc *gin.Context) { - app.debug.Printf("%s: New user failed: Matrix user already linked", req.Code) - respond(400, "errorAccountLinked", gc) - } - success = false - return - } + if app.config.Section("matrix").Key("require_unique").MustBool(false) && app.matrix.UserExists(user.User.UserID) { + f = func(gc *gin.Context) { + app.debug.Printf("%s: New user failed: Matrix user already linked", req.Code) + respond(400, "errorAccountLinked", gc) } + success = false + return } matrixVerified = user.Verified matrixUser = *user.User @@ -278,7 +270,6 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc success = false return } - app.storage.loadProfiles() invite, _ := app.storage.GetInvitesKey(req.Code) app.checkInvite(req.Code, true, req.Username) if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) { @@ -310,9 +301,9 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc if invite.Profile != "" { app.debug.Printf("Applying settings from profile \"%s\"", invite.Profile) var ok bool - profile, ok = app.storage.profiles[invite.Profile] + profile, ok = app.storage.GetProfileKey(invite.Profile) if !ok { - profile = app.storage.profiles["Default"] + profile = app.storage.GetDefaultProfile() } if profile.Policy.BlockedTags != nil { app.debug.Printf("Applying policy from profile \"%s\"", invite.Profile) @@ -335,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 req.Email != "" { app.storage.SetEmailsKey(id, EmailAddress{Addr: req.Email, Contact: true}) - app.storage.storeEmails() } expiry := time.Time{} if invite.UserExpiry { - app.storage.usersLock.Lock() - defer app.storage.usersLock.Unlock() expiry = time.Now().AddDate(0, invite.UserMonths, invite.UserDays).Add(time.Duration((60*invite.UserHours)+invite.UserMinutes) * time.Minute) - app.storage.users[id] = expiry - if err := app.storage.storeUsers(); err != nil { - app.err.Printf("Failed to store user duration: %v", err) - } + app.storage.SetUserExpiryKey(id, UserExpiry{Expiry: expiry}) } if discordVerified { discordUser.Contact = req.DiscordContact - if app.storage.discord == nil { - app.storage.discord = discordStore{} + if app.storage.deprecatedDiscord == nil { + app.storage.deprecatedDiscord = discordStore{} } app.storage.SetDiscordKey(user.ID, discordUser) - if err := app.storage.storeDiscordUsers(); err != nil { - app.err.Printf("Failed to store Discord users: %v", err) - } else { - delete(app.discord.verifiedTokens, req.DiscordPIN) - } + delete(app.discord.verifiedTokens, req.DiscordPIN) } if telegramVerified { 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 { tgUser.Lang = lang } - if app.storage.telegram == nil { - app.storage.telegram = telegramStore{} + if app.storage.deprecatedTelegram == nil { + app.storage.deprecatedTelegram = telegramStore{} } app.telegram.DeleteVerifiedToken(req.TelegramPIN) app.storage.SetTelegramKey(user.ID, tgUser) @@ -412,13 +393,10 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc if matrixVerified { matrixUser.Contact = req.MatrixContact delete(app.matrix.tokens, req.MatrixPIN) - if app.storage.matrix == nil { - app.storage.matrix = matrixStore{} + if app.storage.deprecatedMatrix == nil { + app.storage.deprecatedMatrix = matrixStore{} } app.storage.SetMatrixKey(user.ID, matrixUser) - if err := app.storage.storeMatrixUsers(); err != nil { - app.err.Printf("Failed to store Matrix users: %v", err) - } } if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramVerified || discordVerified || matrixVerified { name := app.getAddressOrName(user.ID) @@ -478,14 +456,10 @@ func (app *appContext) NewUser(gc *gin.Context) { respond(400, "errorNoEmail", gc) return } - if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" { - for _, email := range app.storage.GetEmails() { - if req.Email == email.Addr { - app.info.Printf("%s: New user failed: Email already in use", req.Code) - respond(400, "errorEmailLinked", gc) - return - } - } + if app.config.Section("email").Key("require_unique").MustBool(false) && req.Email != "" && app.EmailAddressExists(req.Email) { + app.info.Printf("%s: New user failed: Email already in use", req.Code) + respond(400, "errorEmailLinked", gc) + return } } f, success := app.newUser(req, false) @@ -641,21 +615,16 @@ func (app *appContext) ExtendExpiry(gc *gin.Context) { respondBool(400, false, gc) return } - app.storage.usersLock.Lock() - defer app.storage.usersLock.Unlock() for _, id := range req.Users { - if expiry, ok := app.storage.users[id]; ok { - app.storage.users[id] = expiry.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute) + base := time.Now() + if expiry, ok := app.storage.GetUserExpiryKey(id); ok { + base = expiry.Expiry app.debug.Printf("Expiry extended for \"%s\"", id) } else { - app.storage.users[id] = time.Now().AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute) app.debug.Printf("Created expiry for \"%s\"", id) } - } - if err := app.storage.storeUsers(); err != nil { - app.err.Printf("Failed to store user duration: %v", err) - respondBool(500, false, gc) - return + expiry := UserExpiry{Expiry: base.AddDate(0, req.Months, req.Days).Add(time.Duration(((60 * req.Hours) + req.Minutes)) * time.Minute)} + app.storage.SetUserExpiryKey(id, expiry) } respondBool(204, true, gc) } @@ -727,27 +696,21 @@ func (app *appContext) SaveAnnounceTemplate(gc *gin.Context) { respondBool(400, false, gc) return } - app.storage.announcements[req.Name] = req - if err := app.storage.storeAnnouncements(); err != nil { - respondBool(500, false, gc) - app.err.Printf("Failed to store announcement templates: %v", err) - return - } + + app.storage.SetAnnouncementsKey(req.Name, req) respondBool(200, true, gc) } // @Summary Save an announcement as a template for use or editing later. // @Produce json // @Success 200 {object} getAnnouncementsDTO -// @Router /users/announce/template [get] +// @Router /users/announce [get] // @Security Bearer // @tags Users func (app *appContext) GetAnnounceTemplates(gc *gin.Context) { - resp := &getAnnouncementsDTO{make([]string, len(app.storage.announcements))} - i := 0 - for name := range app.storage.announcements { - resp.Announcements[i] = name - i++ + resp := &getAnnouncementsDTO{make([]string, len(app.storage.GetAnnouncements()))} + for i, a := range app.storage.GetAnnouncements() { + resp.Announcements[i] = a.Name } gc.JSON(200, resp) } @@ -762,7 +725,7 @@ func (app *appContext) GetAnnounceTemplates(gc *gin.Context) { // @tags Users func (app *appContext) GetAnnounceTemplate(gc *gin.Context) { name := gc.Param("name") - if announcement, ok := app.storage.announcements[name]; ok { + if announcement, ok := app.storage.GetAnnouncementsKey(name); ok { gc.JSON(200, announcement) return } @@ -779,12 +742,7 @@ func (app *appContext) GetAnnounceTemplate(gc *gin.Context) { // @tags Users func (app *appContext) DeleteAnnounceTemplate(gc *gin.Context) { name := gc.Param("name") - delete(app.storage.announcements, name) - if err := app.storage.storeAnnouncements(); err != nil { - respondBool(500, false, gc) - app.err.Printf("Failed to store announcement templates: %v", err) - return - } + app.storage.DeleteAnnouncementsKey(name) respondBool(200, false, gc) } @@ -876,8 +834,6 @@ func (app *appContext) GetUsers(gc *gin.Context) { adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true) allowAll := app.config.Section("ui").Key("allow_all").MustBool(false) i := 0 - app.storage.usersLock.Lock() - defer app.storage.usersLock.Unlock() for _, jfUser := range users { user := respUser{ ID: jfUser.ID, @@ -894,9 +850,9 @@ func (app *appContext) GetUsers(gc *gin.Context) { user.Label = email.Label user.AccountsAdmin = (app.jellyfinLogin) && (email.Admin || (adminOnly && jfUser.Policy.IsAdministrator) || allowAll) } - expiry, ok := app.storage.users[jfUser.ID] + expiry, ok := app.storage.GetUserExpiryKey(jfUser.ID) if ok { - user.Expiry = expiry.Unix() + user.Expiry = expiry.Expiry.Unix() } if tgUser, ok := app.storage.GetTelegramKey(jfUser.ID); ok { user.Telegram = tgUser.Username @@ -947,10 +903,6 @@ func (app *appContext) SetAccountsAdmin(gc *gin.Context) { app.storage.SetEmailsKey(id, emailStore) } } - if err := app.storage.storeEmails(); err != nil { - app.err.Printf("Failed to store email list: %v", err) - respondBool(500, false, gc) - } app.info.Println("Email list modified") respondBool(204, true, gc) } @@ -984,10 +936,6 @@ func (app *appContext) ModifyLabels(gc *gin.Context) { app.storage.SetEmailsKey(id, emailStore) } } - if err := app.storage.storeEmails(); err != nil { - app.err.Printf("Failed to store email list: %v", err) - respondBool(500, false, gc) - } app.info.Println("Email list modified") respondBool(204, true, gc) } @@ -1022,7 +970,6 @@ func (app *appContext) ModifyEmails(gc *gin.Context) { // Auto enable contact by email for newly added addresses if !ok || oldEmail.Addr == "" { emailStore.Contact = true - app.storage.storeEmails() } emailStore.Addr = address @@ -1039,7 +986,6 @@ func (app *appContext) ModifyEmails(gc *gin.Context) { } } } - app.storage.storeEmails() app.info.Println("Email list modified") respondBool(200, true, gc) } @@ -1062,25 +1008,24 @@ func (app *appContext) ApplySettings(gc *gin.Context) { var displayprefs map[string]interface{} var ombi map[string]interface{} if req.From == "profile" { - app.storage.loadProfiles() // Check profile exists & isn't empty - if _, ok := app.storage.profiles[req.Profile]; !ok || app.storage.profiles[req.Profile].Policy.BlockedTags == nil { + profile, ok := app.storage.GetProfileKey(req.Profile) + if !ok || profile.Policy.BlockedTags == nil { app.err.Printf("Couldn't find profile \"%s\" or profile was empty", req.Profile) respond(500, "Couldn't find profile", gc) return } if req.Homescreen { - if app.storage.profiles[req.Profile].Configuration.GroupedFolders == nil || len(app.storage.profiles[req.Profile].Displayprefs) == 0 { + if profile.Configuration.GroupedFolders == nil || len(profile.Displayprefs) == 0 { app.err.Printf("No homescreen saved in profile \"%s\"", req.Profile) respond(500, "No homescreen template available", gc) return } - configuration = app.storage.profiles[req.Profile].Configuration - displayprefs = app.storage.profiles[req.Profile].Displayprefs + configuration = profile.Configuration + displayprefs = profile.Displayprefs } - policy = app.storage.profiles[req.Profile].Policy + policy = profile.Policy if app.config.Section("ombi").Key("enabled").MustBool(false) { - profile := app.storage.profiles[req.Profile] if profile.Ombi != nil && len(profile.Ombi) != 0 { ombi = profile.Ombi } diff --git a/config.go b/config.go index 7fb388c..7053c12 100644 --- a/config.go +++ b/config.go @@ -157,15 +157,6 @@ func (app *appContext) loadConfig() error { app.MustSetValue("updates", "channel", releaseChannel) } - app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String() - app.storage.loadCustomEmails() - - app.MustSetValue("user_page", "enabled", "true") - if app.config.Section("user_page").Key("enabled").MustBool(false) { - app.storage.userPage_path = app.config.Section("files").Key("custom_user_page_content").String() - app.storage.loadUserPageContent() - } - substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("") if substituteStrings != "" { diff --git a/daemon.go b/daemon.go index 8b3b465..320afac 100644 --- a/daemon.go +++ b/daemon.go @@ -7,85 +7,53 @@ import "time" // the user cache is fresh. func (app *appContext) clearEmails() { app.debug.Println("Housekeeping: removing unused email addresses") - users, status, err := app.jf.GetUsers(false) - if status != 200 || err != nil || len(users) == 0 { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) - return - } - // 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 + emails := app.storage.GetEmails() + for _, email := range emails { + _, status, err := app.jf.UserByID(email.JellyfinID, false) + if status == 200 && err != nil { + continue } + app.storage.DeleteEmailsKey(email.JellyfinID) } - app.storage.emails = emails - app.storage.storeEmails() - app.storage.emailsLock.Unlock() } // clearDiscord does the same as clearEmails, but for Discord Users. func (app *appContext) clearDiscord() { app.debug.Println("Housekeeping: removing unused Discord IDs") - users, status, err := app.jf.GetUsers(false) - if status != 200 || err != nil || len(users) == 0 { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) - return - } - // 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 + discordUsers := app.storage.GetDiscord() + for _, discordUser := range discordUsers { + _, status, err := app.jf.UserByID(discordUser.JellyfinID, false) + if status == 200 && err != nil { + continue } + app.storage.DeleteDiscordKey(discordUser.JellyfinID) } - app.storage.discord = dcUsers - app.storage.storeDiscordUsers() - app.storage.discordLock.Unlock() } // clearMatrix does the same as clearEmails, but for Matrix Users. func (app *appContext) clearMatrix() { app.debug.Println("Housekeeping: removing unused Matrix IDs") - users, status, err := app.jf.GetUsers(false) - if status != 200 || err != nil || len(users) == 0 { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) - return - } - // 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 + matrixUsers := app.storage.GetMatrix() + for _, matrixUser := range matrixUsers { + _, status, err := app.jf.UserByID(matrixUser.JellyfinID, false) + if status == 200 && err != nil { + continue } + app.storage.DeleteMatrixKey(matrixUser.JellyfinID) } - app.storage.matrix = mxUsers - app.storage.storeMatrixUsers() - app.storage.matrixLock.Unlock() } // clearTelegram does the same as clearEmails, but for Telegram Users. func (app *appContext) clearTelegram() { app.debug.Println("Housekeeping: removing unused Telegram IDs") - users, status, err := app.jf.GetUsers(false) - if status != 200 || err != nil || len(users) == 0 { - app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) - return - } - // 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 + telegramUsers := app.storage.GetTelegram() + for _, telegramUser := range telegramUsers { + _, status, err := app.jf.UserByID(telegramUser.JellyfinID, false) + if status == 200 && err != nil { + continue } + app.storage.DeleteTelegramKey(telegramUser.JellyfinID) } - app.storage.telegram = tgUsers - app.storage.storeTelegramUsers() - app.storage.telegramLock.Unlock() } // https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS @@ -148,7 +116,6 @@ func (rt *housekeepingDaemon) run() { break } started := time.Now() - rt.app.storage.loadInvites() for _, job := range rt.jobs { job(rt.app) diff --git a/discord.go b/discord.go index da949ef..7164114 100644 --- a/discord.go +++ b/discord.go @@ -6,6 +6,7 @@ import ( "time" dg "github.com/bwmarrin/discordgo" + "github.com/timshannon/badgerhold/v4" ) type DiscordDaemon struct { @@ -478,11 +479,11 @@ func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang str code := i.ApplicationCommandData().Options[0].StringValue() if _, ok := d.app.storage.lang.Telegram[code]; ok { var user DiscordUser - for jfID, u := range d.app.storage.GetDiscord() { + for _, u := range d.app.storage.GetDiscord() { if u.ID == i.Interaction.Member.User.ID { u.Lang = code lang = code - d.app.storage.SetDiscordKey(jfID, u) + d.app.storage.SetDiscordKey(u.JellyfinID, u) user = u break } @@ -585,10 +586,10 @@ func (d *DiscordDaemon) msgLang(s *dg.Session, m *dg.MessageCreate, sects []stri } if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok { var user DiscordUser - for jfID, u := range d.app.storage.GetDiscord() { + for _, u := range d.app.storage.GetDiscord() { if u.ID == m.Author.ID { u.Lang = sects[1] - d.app.storage.SetDiscordKey(jfID, u) + d.app.storage.SetDiscordKey(u.JellyfinID, u) user = u break } @@ -708,15 +709,9 @@ func (d *DiscordDaemon) AssignedUserVerified(pin string, jfID string) (user Disc } // UserExists returns whether or not a user with the given ID exists. -func (d *DiscordDaemon) UserExists(id string) (ok bool) { - ok = false - for _, u := range d.app.storage.GetDiscord() { - if u.ID == id { - ok = true - break - } - } - return +func (d *DiscordDaemon) UserExists(id string) bool { + c, err := d.app.storage.db.Count(&DiscordUser{}, badgerhold.Where("ID").Eq(id)) + return err != nil || c > 0 } // DeleteVerifiedUser removes the token with the given PIN. diff --git a/email.go b/email.go index 4ee8f36..0f517ca 100644 --- a/email.go +++ b/email.go @@ -19,8 +19,10 @@ import ( "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" + "github.com/hrfee/mediabrowser" "github.com/itchyny/timefmt-go" "github.com/mailgun/mailgun-go/v4" + "github.com/timshannon/badgerhold/v4" sMail "github.com/xhit/go-simple-mail/v2" ) @@ -329,10 +331,11 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a } var err error template := emailer.confirmationValues(code, username, key, app, noSub) - if app.storage.customEmails.EmailConfirmation.Enabled { + message := app.storage.MustGetCustomContentKey("EmailConfirmation") + if message.Enabled { content := templateEmail( - app.storage.customEmails.EmailConfirmation.Content, - app.storage.customEmails.EmailConfirmation.Variables, + message.Content, + message.Variables, nil, template, ) @@ -412,10 +415,11 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont } template := emailer.inviteValues(code, invite, app, noSub) var err error - if app.storage.customEmails.InviteEmail.Enabled { + message := app.storage.MustGetCustomContentKey("InviteEmail") + if message.Enabled { content := templateEmail( - app.storage.customEmails.InviteEmail.Content, - app.storage.customEmails.InviteEmail.Variables, + message.Content, + message.Variables, nil, template, ) @@ -451,10 +455,11 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont } var err error template := emailer.expiryValues(code, invite, app, noSub) - if app.storage.customEmails.InviteExpiry.Enabled { + message := app.storage.MustGetCustomContentKey("InviteExpiry") + if message.Enabled { content := templateEmail( - app.storage.customEmails.InviteExpiry.Content, - app.storage.customEmails.InviteExpiry.Variables, + message.Content, + message.Variables, nil, template, ) @@ -505,10 +510,11 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite } template := emailer.createdValues(code, username, address, invite, app, noSub) var err error - if app.storage.customEmails.UserCreated.Enabled { + message := app.storage.MustGetCustomContentKey("UserCreated") + if message.Enabled { content := templateEmail( - app.storage.customEmails.UserCreated.Content, - app.storage.customEmails.UserCreated.Variables, + message.Content, + message.Variables, nil, template, ) @@ -578,10 +584,11 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub } template := emailer.resetValues(pwr, app, noSub) var err error - if app.storage.customEmails.PasswordReset.Enabled { + message := app.storage.MustGetCustomContentKey("PasswordReset") + if message.Enabled { content := templateEmail( - app.storage.customEmails.PasswordReset.Content, - app.storage.customEmails.PasswordReset.Variables, + message.Content, + message.Variables, nil, template, ) @@ -619,10 +626,11 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b } var err error template := emailer.deletedValues(reason, app, noSub) - if app.storage.customEmails.UserDeleted.Enabled { + message := app.storage.MustGetCustomContentKey("UserDeleted") + if message.Enabled { content := templateEmail( - app.storage.customEmails.UserDeleted.Content, - app.storage.customEmails.UserDeleted.Variables, + message.Content, + message.Variables, nil, template, ) @@ -660,10 +668,11 @@ func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub } var err error template := emailer.disabledValues(reason, app, noSub) - if app.storage.customEmails.UserDisabled.Enabled { + message := app.storage.MustGetCustomContentKey("UserDisabled") + if message.Enabled { content := templateEmail( - app.storage.customEmails.UserDisabled.Content, - app.storage.customEmails.UserDisabled.Variables, + message.Content, + message.Variables, nil, template, ) @@ -701,10 +710,11 @@ func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub b } var err error template := emailer.enabledValues(reason, app, noSub) - if app.storage.customEmails.UserEnabled.Enabled { + message := app.storage.MustGetCustomContentKey("UserEnabled") + if message.Enabled { content := templateEmail( - app.storage.customEmails.UserEnabled.Content, - app.storage.customEmails.UserEnabled.Variables, + message.Content, + message.Variables, nil, template, ) @@ -756,7 +766,8 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app } var err error var template map[string]interface{} - if app.storage.customEmails.WelcomeEmail.Enabled { + message := app.storage.MustGetCustomContentKey("WelcomeEmail") + if message.Enabled { template = emailer.welcomeValues(username, expiry, app, noSub, true) } else { template = emailer.welcomeValues(username, expiry, app, noSub, false) @@ -766,11 +777,11 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app "date": "{yourAccountWillExpire}", }) } - if app.storage.customEmails.WelcomeEmail.Enabled { + if message.Enabled { content := templateEmail( - app.storage.customEmails.WelcomeEmail.Content, - app.storage.customEmails.WelcomeEmail.Variables, - app.storage.customEmails.WelcomeEmail.Conditionals, + message.Content, + message.Variables, + message.Conditionals, template, ) email, err = emailer.constructTemplate(email.Subject, content, app) @@ -801,10 +812,11 @@ func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Mess } var err error template := emailer.userExpiredValues(app, noSub) - if app.storage.customEmails.UserExpired.Enabled { + message := app.storage.MustGetCustomContentKey("UserExpired") + if message.Enabled { content := templateEmail( - app.storage.customEmails.UserExpired.Content, - app.storage.customEmails.UserExpired.Variables, + message.Content, + message.Variables, nil, template, ) @@ -874,31 +886,63 @@ func (app *appContext) getAddressOrName(jfID string) string { // ReverseUserSearch returns the jellyfin ID of the user with the given username, email, or contact method username. // returns "" if none found. returns only the first match, might be an issue if there are users with the same contact method usernames. -func (app *appContext) ReverseUserSearch(address string) string { +func (app *appContext) ReverseUserSearch(address string) (user mediabrowser.User, ok bool) { + ok = false user, status, err := app.jf.UserByName(address, false) if status == 200 && err == nil { - return user.ID + ok = true + return } - for id, email := range app.storage.GetEmails() { - if strings.ToLower(address) == strings.ToLower(email.Addr) { - return id + emailAddresses := []EmailAddress{} + err = app.storage.db.Find(&emailAddresses, badgerhold.Where("Addr").Eq(address)) + if err == nil && len(emailAddresses) > 0 { + for _, emailUser := range emailAddresses { + user, status, err = app.jf.UserByID(emailUser.JellyfinID, false) + if status == 200 && err == nil { + ok = true + return + } } } - for id, dcUser := range app.storage.GetDiscord() { + // Dont know how we'd use badgerhold when we need to render each username, + // Apart from storing the rendered name in the db. + for _, dcUser := range app.storage.GetDiscord() { if RenderDiscordUsername(dcUser) == strings.ToLower(address) { - return id + user, status, err = app.jf.UserByID(dcUser.JellyfinID, false) + if status == 200 && err == nil { + ok = true + return + } } } tgUsername := strings.TrimPrefix(address, "@") - for id, tgUser := range app.storage.GetTelegram() { - if tgUsername == tgUser.Username { - return id + telegramUsers := []TelegramUser{} + err = app.storage.db.Find(&telegramUsers, badgerhold.Where("Username").Eq(tgUsername)) + if err == nil && len(telegramUsers) > 0 { + for _, telegramUser := range telegramUsers { + user, status, err = app.jf.UserByID(telegramUser.JellyfinID, false) + if status == 200 && err == nil { + ok = true + return + } } } - for id, mxUser := range app.storage.GetMatrix() { - if address == mxUser.UserID { - return id + matrixUsers := []MatrixUser{} + err = app.storage.db.Find(&matrixUsers, badgerhold.Where("UserID").Eq(address)) + if err == nil && len(matrixUsers) > 0 { + for _, matrixUser := range matrixUsers { + user, status, err = app.jf.UserByID(matrixUser.JellyfinID, false) + if status == 200 && err == nil { + ok = true + return + } } } - return "" + return +} + +// EmailAddressExists returns whether or not a user with the given email address exists. +func (app *appContext) EmailAddressExists(address string) bool { + c, err := app.storage.db.Count(&EmailAddress{}, badgerhold.Where("Addr").Eq(address)) + return err != nil || c > 0 } diff --git a/go.mod b/go.mod index 9267ab4..8dccf00 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,12 @@ require ( require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/dgraph-io/badger/v3 v3.2103.1 // indirect + github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect github.com/getlantern/errors v1.0.3 // indirect @@ -69,12 +74,19 @@ require ( github.com/go-stack/stack v1.8.1 // indirect github.com/go-test/deep v1.1.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/flatbuffers v2.0.0+incompatible // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.13.1 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -92,9 +104,11 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/timshannon/badgerhold/v4 v4.0.2 // indirect github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + go.opencensus.io v0.23.0 // indirect go.opentelemetry.io/otel v1.16.0 // indirect go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/otel/trace v1.16.0 // indirect diff --git a/go.sum b/go.sum index b0534a7..8a032c7 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,14 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= @@ -13,17 +16,39 @@ github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v3 v3.2103.1 h1:zaX53IRg7ycxVlkd5pYdCeFp1FynD6qBGQoQql3R3Hk= +github.com/dgraph-io/badger/v3 v3.2103.1/go.mod h1:dULbq6ehJ5K0cGW/1TQ9iSfUk0gbSiToDWmWmTsJ53E= +github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:M88ob4TyDnEqNuL3PgsE/p3bDujfspnulR+0dQWNYZs= github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:buzQsO8HHkZX2Q45fdfGH1xejPjuDQaXH8btcYMFzPM= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= @@ -33,6 +58,7 @@ github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+ne github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= @@ -131,20 +157,54 @@ github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB1/j8wa6xdKn85B5NzgVL/pTU= +github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a h1:AWZzzFrqyjYlRloN6edwTLTUbKxf5flLXNuTBDm3Ews= github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI= +github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -153,8 +213,10 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hrfee/mediabrowser v0.3.8 h1:y0iBCb6jE3QKcsiCJSYva2fFPHRn4UA+sGRzoPuJ/Dk= github.com/hrfee/mediabrowser v0.3.8/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -165,6 +227,11 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ= +github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -185,6 +252,7 @@ github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJV github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailgun/mailgun-go/v4 v4.9.0 h1:wRbxvVQ5QObFewLxc1uVvipA16D8gxeiO+cBOca51Iw= github.com/mailgun/mailgun-go/v4 v4.9.0/go.mod h1:FJlF9rI5cQT+mrwujtJjPMbIVy3Ebor9bKTVsJ0QU40= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -206,6 +274,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -216,6 +286,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= @@ -225,6 +296,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc= github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -233,10 +305,19 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/steambap/captcha v1.4.1 h1:OmMdxLCWCqJvsFaFYwRpvMckIuvI6s8s1LsBrBw97P0= github.com/steambap/captcha v1.4.1/go.mod h1:oC9T7IfEgnrhzjDz5Djf1H7GPffCzRMbsQfFkJmhlnk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -276,6 +357,9 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/timshannon/badgerhold/v3 v3.0.0-20210909134927-2b6764d68c1e/go.mod h1:/Seq5xGNo8jLhSbDX3jdbeZrp4yFIpQ6/7n4TjziEWs= +github.com/timshannon/badgerhold/v4 v4.0.2 h1:83OLY/NFnEaMnHEPd84bYtkLipVkjTsMbzQRYbk47g4= +github.com/timshannon/badgerhold/v4 v4.0.2/go.mod h1:rh6RyXLQFsvrvcKondPQQFZnNovpRzu+gS0FlLxYuHY= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -285,6 +369,7 @@ github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/o github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= @@ -296,8 +381,14 @@ github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6Fk github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE= github.com/xhit/go-simple-mail/v2 v2.13.0 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI= github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= @@ -320,26 +411,37 @@ go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -347,29 +449,45 @@ golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -396,11 +514,16 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= @@ -410,13 +533,37 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -435,6 +582,8 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8= maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho= maunium.net/go/mautrix v0.15.2 h1:fUiVajeoOR92uJoSShHbCvh7uG6lDY4ZO4Mvt90LbjU= diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json index 3ff043e..db9773f 100644 --- a/lang/admin/en-us.json +++ b/lang/admin/en-us.json @@ -79,7 +79,7 @@ "ombiProfile": "Ombi user profile", "ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go when this profile is selected.", "userProfiles": "User Profiles", - "userProfilesDescription": "Profiles are applied to users when they create an account. A profile include library access rights and homescreen layout.", + "userProfilesDescription": "Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.", "userProfilesIsDefault": "Default", "userProfilesLibraries": "Libraries", "addProfile": "Add Profile", diff --git a/main.go b/main.go index ef5b1c1..bfc8e8f 100644 --- a/main.go +++ b/main.go @@ -335,59 +335,8 @@ func start(asDaemon, firstCall bool) { app.debug.Printf("Loaded config file \"%s\"", app.configPath) - app.debug.Println("Loading storage") - - app.storage.invite_path = app.config.Section("files").Key("invites").String() - if err := app.storage.loadInvites(); err != nil { - app.err.Printf("Failed to load Invites: %v", err) - } - app.storage.emails_path = app.config.Section("files").Key("emails").String() - if err := app.storage.loadEmails(); err != nil { - app.err.Printf("Failed to load Emails: %v", err) - err := migrateEmailStorage(app) - if err != nil { - app.err.Printf("Failed to migrate Email storage: %v", err) - } - } - app.storage.policy_path = app.config.Section("files").Key("user_template").String() - if err := app.storage.loadPolicy(); err != nil { - app.err.Printf("Failed to load Policy: %v", err) - } - app.storage.configuration_path = app.config.Section("files").Key("user_configuration").String() - if err := app.storage.loadConfiguration(); err != nil { - app.err.Printf("Failed to load Configuration: %v", err) - } - app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String() - if err := app.storage.loadDisplayprefs(); err != nil { - app.err.Printf("Failed to load Displayprefs: %v", err) - } - app.storage.users_path = app.config.Section("files").Key("users").String() - if err := app.storage.loadUsers(); err != nil { - app.err.Printf("Failed to load Users: %v", err) - } - app.storage.telegram_path = app.config.Section("files").Key("telegram_users").String() - if err := app.storage.loadTelegramUsers(); err != nil { - app.err.Printf("Failed to load Telegram users: %v", err) - } - app.storage.discord_path = app.config.Section("files").Key("discord_users").String() - if err := app.storage.loadDiscordUsers(); err != nil { - app.err.Printf("Failed to load Discord users: %v", err) - } - app.storage.matrix_path = app.config.Section("files").Key("matrix_users").String() - if err := app.storage.loadMatrixUsers(); err != nil { - app.err.Printf("Failed to load Matrix users: %v", err) - } - app.storage.announcements_path = app.config.Section("files").Key("announcements").String() - if err := app.storage.loadAnnouncements(); err != nil { - app.err.Printf("Failed to load announcement templates: %v", err) - } - - app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String() - app.storage.loadProfiles() - if app.config.Section("ombi").Key("enabled").MustBool(false) { - app.storage.ombi_path = app.config.Section("files").Key("ombi_template").String() - app.storage.loadOmbiTemplate() + app.debug.Printf("Connecting to Ombi") ombiServer := app.config.Section("ombi").Key("server").String() app.ombi = ombi.NewOmbi( ombiServer, @@ -397,6 +346,9 @@ func start(asDaemon, firstCall bool) { } + app.storage.db_path = filepath.Join(app.dataPath, "db") + app.ConnectDB() + defer app.storage.db.Close() // Read config-base for settings on web. app.configBasePath = "config-base.json" configBase, _ := fs.ReadFile(localFS, app.configBasePath) diff --git a/matrix.go b/matrix.go index 9935a2c..d20814c 100644 --- a/matrix.go +++ b/matrix.go @@ -6,6 +6,7 @@ import ( "time" "github.com/gomarkdown/markdown" + "github.com/timshannon/badgerhold/v4" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" @@ -31,11 +32,12 @@ type UnverifiedUser struct { } type MatrixUser struct { - RoomID string - Encrypted bool - UserID string - Lang string - Contact bool + RoomID string + Encrypted bool + UserID string + Lang string + Contact bool + JellyfinID string `badgerhold:"key"` } var matrixFilter = mautrix.Filter{ @@ -268,6 +270,12 @@ func (d *MatrixDaemon) Send(message *Message, users ...MatrixUser) (err error) { return } +// UserExists returns whether or not a user with the given User ID exists. +func (d *MatrixDaemon) UserExists(userID string) bool { + c, err := d.app.storage.db.Count(&MatrixUser{}, badgerhold.Where("UserID").Eq(userID)) + return err != nil || c > 0 +} + // User enters ID on sign-up, a PIN is sent to them. They enter it on sign-up. // Message the user first, to avoid E2EE by default diff --git a/migrations.go b/migrations.go index 95f69eb..0b81aea 100644 --- a/migrations.go +++ b/migrations.go @@ -16,26 +16,28 @@ func runMigrations(app *appContext) { migrateNotificationMethods(app) linkExistingOmbiDiscordTelegram(app) // migrateHyphens(app) + migrateToBadger(app) } // Migrate pre-0.2.0 user templates to profiles func migrateProfiles(app *appContext) { - if !(app.storage.policy.BlockedTags == nil && app.storage.configuration.GroupedFolders == nil && len(app.storage.displayprefs) == 0) { - app.info.Println("Migrating user template files to new profile format") - app.storage.migrateToProfile() - for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} { - if _, err := os.Stat(path); !os.IsNotExist(err) { - dir, fname := filepath.Split(path) - newFname := strings.Replace(fname, ".json", ".old.json", 1) - err := os.Rename(path, filepath.Join(dir, newFname)) - if err != nil { - app.err.Fatalf("Failed to rename %s: %s", fname, err) - } + if app.storage.deprecatedPolicy.BlockedTags == nil && app.storage.deprecatedConfiguration.GroupedFolders == nil && len(app.storage.deprecatedDisplayprefs) == 0 { + return + } + app.info.Println("Migrating user template files to new profile format") + app.storage.migrateToProfile() + for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} { + if _, err := os.Stat(path); !os.IsNotExist(err) { + dir, fname := filepath.Split(path) + newFname := strings.Replace(fname, ".json", ".old.json", 1) + 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 .old.json") - app.storage.storeProfiles() } + app.info.Println("In case of a problem, your original files have been renamed to .old.json") + app.storage.storeProfiles() } // Migrate pre-0.2.5 bootstrap theme choice to a17t version. @@ -131,7 +133,7 @@ func migrateNotificationMethods(app *appContext) error { return nil } changes := false - for code, invite := range app.storage.invites { + for code, invite := range app.storage.deprecatedInvites { if invite.Notify == nil { continue } @@ -139,9 +141,9 @@ func migrateNotificationMethods(app *appContext) error { if !strings.Contains(address, "@") { continue } - for id, email := range app.storage.GetEmails() { + for _, email := range app.storage.GetEmails() { if email.Addr == address { - invite.Notify[id] = notifyPrefs + invite.Notify[email.JellyfinID] = notifyPrefs delete(invite.Notify, address) changes = true break @@ -149,7 +151,7 @@ func migrateNotificationMethods(app *appContext) error { } } if changes { - app.storage.invites[code] = invite + app.storage.deprecatedInvites[code] = invite } } if changes { @@ -168,16 +170,16 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error { return nil } idList := map[string][2]string{} - for jfID, user := range app.storage.GetDiscord() { - idList[jfID] = [2]string{user.ID, ""} + for _, user := range app.storage.GetDiscord() { + idList[user.JellyfinID] = [2]string{user.ID, ""} } - for jfID, user := range app.storage.GetTelegram() { - vals, ok := idList[jfID] + for _, user := range app.storage.GetTelegram() { + vals, ok := idList[user.JellyfinID] if !ok { vals = [2]string{"", ""} } vals[1] = user.Username - idList[jfID] = vals + idList[user.JellyfinID] = vals } for jfID, ids := range idList { ombiUser, status, err := app.getOmbiUser(jfID) @@ -194,6 +196,143 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error { return nil } +// MigrationStatus is just used to store whether data from JSON files has been migrated to the DB. +type MigrationStatus struct { + Done bool +} + +func loadLegacyData(app *appContext) { + app.storage.invite_path = app.config.Section("files").Key("invites").String() + if err := app.storage.loadInvites(); err != nil { + app.err.Printf("LegacyData: Failed to load Invites: %v", err) + } + app.storage.emails_path = app.config.Section("files").Key("emails").String() + if err := app.storage.loadEmails(); err != nil { + app.err.Printf("LegacyData: Failed to load Emails: %v", err) + err := migrateEmailStorage(app) + if err != nil { + app.err.Printf("LegacyData: Failed to migrate Email storage: %v", err) + } + } + app.storage.users_path = app.config.Section("files").Key("users").String() + if err := app.storage.loadUserExpiries(); err != nil { + app.err.Printf("LegacyData: Failed to load Users: %v", err) + } + app.storage.telegram_path = app.config.Section("files").Key("telegram_users").String() + if err := app.storage.loadTelegramUsers(); err != nil { + app.err.Printf("LegacyData: Failed to load Telegram users: %v", err) + } + app.storage.discord_path = app.config.Section("files").Key("discord_users").String() + if err := app.storage.loadDiscordUsers(); err != nil { + app.err.Printf("LegacyData: Failed to load Discord users: %v", err) + } + app.storage.matrix_path = app.config.Section("files").Key("matrix_users").String() + if err := app.storage.loadMatrixUsers(); err != nil { + app.err.Printf("LegacyData: Failed to load Matrix users: %v", err) + } + app.storage.announcements_path = app.config.Section("files").Key("announcements").String() + if err := app.storage.loadAnnouncements(); err != nil { + app.err.Printf("LegacyData: Failed to load announcement templates: %v", err) + } + + app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String() + app.storage.loadProfiles() + + app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String() + app.storage.loadCustomEmails() + + app.MustSetValue("user_page", "enabled", "true") + if app.config.Section("user_page").Key("enabled").MustBool(false) { + app.storage.userPage_path = app.config.Section("files").Key("custom_user_page_content").String() + app.storage.loadUserPageContent() + } +} + +func migrateToBadger(app *appContext) { + // Check the DB to see if we've already migrated + migrated := MigrationStatus{} + app.storage.db.Get("migrated_to_db", &migrated) + if migrated.Done { + return + } + app.info.Println("Migrating to Badger(hold)") + loadLegacyData(app) + for k, v := range app.storage.deprecatedAnnouncements { + app.storage.SetAnnouncementsKey(k, v) + } + + for jfID, v := range app.storage.deprecatedDiscord { + app.storage.SetDiscordKey(jfID, v) + } + + for jfID, v := range app.storage.deprecatedTelegram { + app.storage.SetTelegramKey(jfID, v) + } + + for jfID, v := range app.storage.deprecatedMatrix { + app.storage.SetMatrixKey(jfID, v) + } + + for jfID, v := range app.storage.deprecatedEmails { + app.storage.SetEmailsKey(jfID, v) + } + + for k, v := range app.storage.deprecatedInvites { + app.storage.SetInvitesKey(k, v) + } + + for k, v := range app.storage.deprecatedUserExpiries { + app.storage.SetUserExpiryKey(k, UserExpiry{Expiry: v}) + } + + for k, v := range app.storage.deprecatedProfiles { + app.storage.SetProfileKey(k, v) + } + + if _, ok := app.storage.GetCustomContentKey("UserCreated"); !ok { + app.storage.SetCustomContentKey("UserCreated", app.storage.deprecatedCustomEmails.UserCreated) + } + if _, ok := app.storage.GetCustomContentKey("InviteExpiry"); !ok { + app.storage.SetCustomContentKey("InviteExpiry", app.storage.deprecatedCustomEmails.InviteExpiry) + } + if _, ok := app.storage.GetCustomContentKey("PasswordReset"); !ok { + app.storage.SetCustomContentKey("PasswordReset", app.storage.deprecatedCustomEmails.PasswordReset) + } + if _, ok := app.storage.GetCustomContentKey("UserDeleted"); !ok { + app.storage.SetCustomContentKey("UserDeleted", app.storage.deprecatedCustomEmails.UserDeleted) + } + if _, ok := app.storage.GetCustomContentKey("UserDisabled"); !ok { + app.storage.SetCustomContentKey("UserDisabled", app.storage.deprecatedCustomEmails.UserDisabled) + } + if _, ok := app.storage.GetCustomContentKey("UserEnabled"); !ok { + app.storage.SetCustomContentKey("UserEnabled", app.storage.deprecatedCustomEmails.UserEnabled) + } + if _, ok := app.storage.GetCustomContentKey("InviteEmail"); !ok { + app.storage.SetCustomContentKey("InviteEmail", app.storage.deprecatedCustomEmails.InviteEmail) + } + if _, ok := app.storage.GetCustomContentKey("WelcomeEmail"); !ok { + app.storage.SetCustomContentKey("WelcomeEmail", app.storage.deprecatedCustomEmails.WelcomeEmail) + } + if _, ok := app.storage.GetCustomContentKey("EmailConfirmation"); !ok { + app.storage.SetCustomContentKey("EmailConfirmation", app.storage.deprecatedCustomEmails.EmailConfirmation) + } + if _, ok := app.storage.GetCustomContentKey("UserExpired"); !ok { + app.storage.SetCustomContentKey("UserExpired", app.storage.deprecatedCustomEmails.UserExpired) + } + if _, ok := app.storage.GetCustomContentKey("UserLogin"); !ok { + app.storage.SetCustomContentKey("UserLogin", app.storage.deprecatedUserPageContent.Login) + } + if _, ok := app.storage.GetCustomContentKey("UserPage"); !ok { + app.storage.SetCustomContentKey("UserPage", app.storage.deprecatedUserPageContent.Page) + } + + err := app.storage.db.Upsert("migrated_to_db", MigrationStatus{true}) + if err != nil { + app.err.Fatalf("Failed to migrate to DB: %v\n", err) + } + app.info.Println("All data migrated to database. JSON files in the config folder can be deleted if you are sure all data is correct in the app. Create an issue if you have problems.") +} + // Migrate between hyphenated & non-hyphenated user IDs. Doesn't seem to happen anymore, so disabled. // func migrateHyphens(app *appContext) { // checkVersion := func(version string) int { @@ -212,8 +351,8 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error { // app.jf.GetUsers(false) // // noHyphens := true -// for id := range app.storage.GetEmails() { -// if strings.Contains(id, "-") { +// for _, e := range app.storage.GetEmails() { +// if strings.Contains(e.JellyfinID, "-") { // noHyphens = false // break // } @@ -255,7 +394,7 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error { // app.storage.emails = newEmails // app.storage.users = newUsers // err = app.storage.storeEmails() -// err2 = app.storage.storeUsers() +// err2 = app.storage.storeUserExpiries() // if err != nil { // app.err.Fatalf("couldn't store emails.json: %v", err) // } diff --git a/pwreset.go b/pwreset.go index be3d3fb..ae387d1 100644 --- a/pwreset.go +++ b/pwreset.go @@ -100,7 +100,6 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) { app.debug.Printf("Error: %s", err) return } - app.storage.loadEmails() uid := user.ID if uid == "" { app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username) diff --git a/storage.go b/storage.go index 873d6c4..8a21d78 100644 --- a/storage.go +++ b/storage.go @@ -8,11 +8,11 @@ import ( "path/filepath" "strconv" "strings" - "sync" "time" "github.com/hrfee/mediabrowser" "github.com/steambap/captcha" + "github.com/timshannon/badgerhold/v4" ) type discordStore map[string]DiscordUser @@ -20,224 +20,439 @@ type telegramStore map[string]TelegramUser type matrixStore map[string]MatrixUser type emailStore map[string]EmailAddress +type UserExpiry struct { + JellyfinID string `badgerhold:"key"` + Expiry time.Time +} + type Storage struct { - timePattern string + timePattern string + + db_path string + db *badgerhold.Store + invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path, announcements_path, matrix_sql_path, userPage_path string - users map[string]time.Time // Map of Jellyfin User IDs to their expiry times. - invites Invites - profiles map[string]Profile - defaultProfile string - displayprefs, ombi_template map[string]interface{} - emails emailStore - telegram telegramStore // Map of Jellyfin User IDs to telegram users. - discord discordStore // Map of Jellyfin user IDs to discord users. - matrix matrixStore // Map of Jellyfin user IDs to Matrix users. - customEmails customEmails - userPage userPageContent - policy mediabrowser.Policy - configuration mediabrowser.Configuration + deprecatedUserExpiries map[string]time.Time // Map of Jellyfin User IDs to their expiry times. + deprecatedInvites Invites + deprecatedProfiles map[string]Profile + deprecatedDisplayprefs, deprecatedOmbiTemplate map[string]interface{} + deprecatedEmails emailStore // Map of Jellyfin User IDs to Email addresses. + deprecatedTelegram telegramStore // Map of Jellyfin User IDs to telegram users. + deprecatedDiscord discordStore // Map of Jellyfin user IDs to discord users. + deprecatedMatrix matrixStore // Map of Jellyfin user IDs to Matrix users. + deprecatedPolicy mediabrowser.Policy + deprecatedConfiguration mediabrowser.Configuration + deprecatedAnnouncements map[string]announcementTemplate + deprecatedCustomEmails customEmails + deprecatedUserPageContent userPageContent lang Lang - announcements map[string]announcementTemplate - invitesLock, usersLock, discordLock, telegramLock, matrixLock, emailsLock sync.Mutex +} + +func (app *appContext) ConnectDB() { + opts := badgerhold.DefaultOptions + opts.Dir = app.storage.db_path + opts.ValueDir = app.storage.db_path + db, err := badgerhold.Open(opts) + if err != nil { + app.err.Fatalf("Failed to open db \"%s\": %v", app.storage.db_path, err) + } + app.storage.db = db + app.info.Printf("Connected to DB \"%s\"", app.storage.db_path) } // GetEmails returns a copy of the store. -func (st *Storage) GetEmails() emailStore { - return st.emails +func (st *Storage) GetEmails() []EmailAddress { + result := []EmailAddress{} + err := st.db.Find(&result, &badgerhold.Query{}) + if err != nil { + // fmt.Printf("Failed to find emails: %v\n", err) + } + return result } // GetEmailsKey returns the value stored in the store's key. func (st *Storage) GetEmailsKey(k string) (EmailAddress, bool) { - v, ok := st.emails[k] - return v, ok + result := EmailAddress{} + err := st.db.Get(k, &result) + ok := true + if err != nil { + // fmt.Printf("Failed to find email: %v\n", err) + ok = false + } + return result, ok } // SetEmailsKey stores value v in key k. func (st *Storage) SetEmailsKey(k string, v EmailAddress) { - st.emailsLock.Lock() - st.emails[k] = v - st.storeEmails() - st.emailsLock.Unlock() + v.JellyfinID = k + err := st.db.Upsert(k, v) + if err != nil { + // fmt.Printf("Failed to set email: %v\n", err) + } } // DeleteEmailKey deletes value at key k. func (st *Storage) DeleteEmailsKey(k string) { - st.emailsLock.Lock() - delete(st.emails, k) - st.storeEmails() - st.emailsLock.Unlock() + st.db.Delete(k, EmailAddress{}) } // GetDiscord returns a copy of the store. -func (st *Storage) GetDiscord() discordStore { - if st.discord == nil { - st.discord = discordStore{} +func (st *Storage) GetDiscord() []DiscordUser { + result := []DiscordUser{} + err := st.db.Find(&result, &badgerhold.Query{}) + if err != nil { + // fmt.Printf("Failed to find users: %v\n", err) } - return st.discord + return result } // GetDiscordKey returns the value stored in the store's key. func (st *Storage) GetDiscordKey(k string) (DiscordUser, bool) { - v, ok := st.discord[k] - return v, ok + result := DiscordUser{} + err := st.db.Get(k, &result) + ok := true + if err != nil { + // fmt.Printf("Failed to find user: %v\n", err) + ok = false + } + return result, ok } // SetDiscordKey stores value v in key k. func (st *Storage) SetDiscordKey(k string, v DiscordUser) { - st.discordLock.Lock() - if st.discord == nil { - st.discord = discordStore{} + v.JellyfinID = k + err := st.db.Upsert(k, v) + if err != nil { + // fmt.Printf("Failed to set user: %v\n", err) } - st.discord[k] = v - st.storeDiscordUsers() - st.discordLock.Unlock() } // DeleteDiscordKey deletes value at key k. func (st *Storage) DeleteDiscordKey(k string) { - st.discordLock.Lock() - delete(st.discord, k) - st.storeDiscordUsers() - st.discordLock.Unlock() + st.db.Delete(k, DiscordUser{}) } // GetTelegram returns a copy of the store. -func (st *Storage) GetTelegram() telegramStore { - if st.telegram == nil { - st.telegram = telegramStore{} +func (st *Storage) GetTelegram() []TelegramUser { + result := []TelegramUser{} + err := st.db.Find(&result, &badgerhold.Query{}) + if err != nil { + // fmt.Printf("Failed to find users: %v\n", err) } - return st.telegram + return result } // GetTelegramKey returns the value stored in the store's key. func (st *Storage) GetTelegramKey(k string) (TelegramUser, bool) { - v, ok := st.telegram[k] - return v, ok + result := TelegramUser{} + err := st.db.Get(k, &result) + ok := true + if err != nil { + // fmt.Printf("Failed to find user: %v\n", err) + ok = false + } + return result, ok } // SetTelegramKey stores value v in key k. func (st *Storage) SetTelegramKey(k string, v TelegramUser) { - st.telegramLock.Lock() - if st.telegram == nil { - st.telegram = telegramStore{} + v.JellyfinID = k + err := st.db.Upsert(k, v) + if err != nil { + // fmt.Printf("Failed to set user: %v\n", err) } - st.telegram[k] = v - st.storeTelegramUsers() - st.telegramLock.Unlock() } // DeleteTelegramKey deletes value at key k. func (st *Storage) DeleteTelegramKey(k string) { - st.telegramLock.Lock() - delete(st.telegram, k) - st.storeTelegramUsers() - st.telegramLock.Unlock() + st.db.Delete(k, TelegramUser{}) } // GetMatrix returns a copy of the store. -func (st *Storage) GetMatrix() matrixStore { - if st.matrix == nil { - st.matrix = matrixStore{} +func (st *Storage) GetMatrix() []MatrixUser { + result := []MatrixUser{} + err := st.db.Find(&result, &badgerhold.Query{}) + if err != nil { + // fmt.Printf("Failed to find users: %v\n", err) } - return st.matrix + return result } // GetMatrixKey returns the value stored in the store's key. func (st *Storage) GetMatrixKey(k string) (MatrixUser, bool) { - v, ok := st.matrix[k] - return v, ok + result := MatrixUser{} + err := st.db.Get(k, &result) + ok := true + if err != nil { + // fmt.Printf("Failed to find user: %v\n", err) + ok = false + } + return result, ok } // SetMatrixKey stores value v in key k. func (st *Storage) SetMatrixKey(k string, v MatrixUser) { - st.matrixLock.Lock() - if st.matrix == nil { - st.matrix = matrixStore{} + v.JellyfinID = k + err := st.db.Upsert(k, v) + if err != nil { + // fmt.Printf("Failed to set user: %v\n", err) } - st.matrix[k] = v - st.storeMatrixUsers() - st.matrixLock.Unlock() } // DeleteMatrixKey deletes value at key k. func (st *Storage) DeleteMatrixKey(k string) { - st.matrixLock.Lock() - delete(st.matrix, k) - st.storeMatrixUsers() - st.matrixLock.Unlock() + st.db.Delete(k, MatrixUser{}) } // GetInvites returns a copy of the store. -func (st *Storage) GetInvites() Invites { - if st.invites == nil { - st.invites = Invites{} +func (st *Storage) GetInvites() []Invite { + result := []Invite{} + err := st.db.Find(&result, &badgerhold.Query{}) + if err != nil { + // fmt.Printf("Failed to find invites: %v\n", err) } - return st.invites + return result } // GetInvitesKey returns the value stored in the store's key. func (st *Storage) GetInvitesKey(k string) (Invite, bool) { - v, ok := st.invites[k] - return v, ok + result := Invite{} + err := st.db.Get(k, &result) + ok := true + if err != nil { + // fmt.Printf("Failed to find invite: %v\n", err) + ok = false + } + return result, ok } // SetInvitesKey stores value v in key k. func (st *Storage) SetInvitesKey(k string, v Invite) { - st.invitesLock.Lock() - if st.invites == nil { - st.invites = Invites{} + v.Code = k + err := st.db.Upsert(k, v) + if err != nil { + // fmt.Printf("Failed to set invite: %v\n", err) } - st.invites[k] = v - st.storeInvites() - st.invitesLock.Unlock() } // DeleteInvitesKey deletes value at key k. func (st *Storage) DeleteInvitesKey(k string) { - st.invitesLock.Lock() - delete(st.invites, k) - st.storeInvites() - st.invitesLock.Unlock() + st.db.Delete(k, Invite{}) +} + +// GetAnnouncements returns a copy of the store. +func (st *Storage) GetAnnouncements() []announcementTemplate { + result := []announcementTemplate{} + err := st.db.Find(&result, &badgerhold.Query{}) + if err != nil { + // fmt.Printf("Failed to find announcements: %v\n", err) + } + return result +} + +// GetAnnouncementsKey returns the value stored in the store's key. +func (st *Storage) GetAnnouncementsKey(k string) (announcementTemplate, bool) { + result := announcementTemplate{} + err := st.db.Get(k, &result) + ok := true + if err != nil { + // fmt.Printf("Failed to find announcement: %v\n", err) + ok = false + } + return result, ok +} + +// SetAnnouncementsKey stores value v in key k. +func (st *Storage) SetAnnouncementsKey(k string, v announcementTemplate) { + err := st.db.Upsert(k, v) + if err != nil { + // fmt.Printf("Failed to set announcement: %v\n", err) + } +} + +// DeleteAnnouncementsKey deletes value at key k. +func (st *Storage) DeleteAnnouncementsKey(k string) { + st.db.Delete(k, announcementTemplate{}) +} + +// GetUserExpiries returns a copy of the store. +func (st *Storage) GetUserExpiries() []UserExpiry { + result := []UserExpiry{} + err := st.db.Find(&result, &badgerhold.Query{}) + if err != nil { + // fmt.Printf("Failed to find expiries: %v\n", err) + } + return result +} + +// GetUserExpiryKey returns the value stored in the store's key. +func (st *Storage) GetUserExpiryKey(k string) (UserExpiry, bool) { + result := UserExpiry{} + err := st.db.Get(k, &result) + ok := true + if err != nil { + // fmt.Printf("Failed to find expiry: %v\n", err) + ok = false + } + return result, ok +} + +// SetUserExpiryKey stores value v in key k. +func (st *Storage) SetUserExpiryKey(k string, v UserExpiry) { + v.JellyfinID = k + err := st.db.Upsert(k, v) + if err != nil { + // fmt.Printf("Failed to set expiry: %v\n", err) + } +} + +// DeleteUserExpiryKey deletes value at key k. +func (st *Storage) DeleteUserExpiryKey(k string) { + st.db.Delete(k, UserExpiry{}) +} + +// GetProfiles returns a copy of the store. +func (st *Storage) GetProfiles() []Profile { + result := []Profile{} + err := st.db.Find(&result, &badgerhold.Query{}) + if err != nil { + // fmt.Printf("Failed to find profiles: %v\n", err) + } + return result +} + +// GetProfileKey returns the value stored in the store's key. +func (st *Storage) GetProfileKey(k string) (Profile, bool) { + result := Profile{} + err := st.db.Get(k, &result) + ok := true + if err != nil { + // fmt.Printf("Failed to find profile: %v\n", err) + ok = false + } + return result, ok +} + +// SetProfileKey stores value v in key k. +func (st *Storage) SetProfileKey(k string, v Profile) { + v.Name = k + v.Admin = v.Policy.IsAdministrator + if v.Policy.EnabledFolders != nil { + if len(v.Policy.EnabledFolders) == 0 { + v.LibraryAccess = "All" + } else { + v.LibraryAccess = strconv.Itoa(len(v.Policy.EnabledFolders)) + } + } + if v.FromUser == "" { + v.FromUser = "Unknown" + } + err := st.db.Upsert(k, v) + if err != nil { + // fmt.Printf("Failed to set profile: %v\n", err) + } +} + +// DeleteProfileKey deletes value at key k. +func (st *Storage) DeleteProfileKey(k string) { + st.db.Delete(k, Profile{}) +} + +// GetDefaultProfile returns the first profile set as default, or anything available if there isn't one. +func (st *Storage) GetDefaultProfile() Profile { + defaultProfile := Profile{} + err := st.db.FindOne(&defaultProfile, badgerhold.Where("Default").Eq(true)) + if err != nil { + st.db.FindOne(&defaultProfile, &badgerhold.Query{}) + } + return defaultProfile +} + +// GetCustomContent returns a copy of the store. +func (st *Storage) GetCustomContent() []CustomContent { + result := []CustomContent{} + err := st.db.Find(&result, &badgerhold.Query{}) + if err != nil { + // fmt.Printf("Failed to find custom content: %v\n", err) + } + return result +} + +// GetCustomContentKey returns the value stored in the store's key. +func (st *Storage) GetCustomContentKey(k string) (CustomContent, bool) { + result := CustomContent{} + err := st.db.Get(k, &result) + ok := true + if err != nil { + // fmt.Printf("Failed to find custom content: %v\n", err) + ok = false + } + return result, ok +} + +// MustGetCustomContentKey returns the value stored in the store's key, or an empty value. +func (st *Storage) MustGetCustomContentKey(k string) CustomContent { + result := CustomContent{} + st.db.Get(k, &result) + return result +} + +// SetCustomContentKey stores value v in key k. +func (st *Storage) SetCustomContentKey(k string, v CustomContent) { + v.Name = k + err := st.db.Upsert(k, v) + if err != nil { + // fmt.Printf("Failed to set custom content: %v\n", err) + } +} + +// DeleteCustomContentKey deletes value at key k. +func (st *Storage) DeleteCustomContentKey(k string) { + st.db.Delete(k, CustomContent{}) } type TelegramUser struct { - ChatID int64 - Username string - Lang string - Contact bool // Whether to contact through telegram or not + JellyfinID string `badgerhold:"key"` + ChatID int64 `badgerhold:"index"` + Username string `badgerhold:"index"` + Lang string + Contact bool // Whether to contact through telegram or not } type DiscordUser struct { ChannelID string - ID string - Username string + ID string `badgerhold:"index"` + Username string `badgerhold:"index"` Discriminator string Lang string Contact bool - JellyfinID string `json:"-"` // Used internally in discord.go + JellyfinID string `json:"-" badgerhold:"key"` // Used internally in discord.go } type EmailAddress struct { - Addr string - Label string // User Label. - Contact bool - Admin bool // Whether or not user is jfa-go admin. + Addr string `badgerhold:"index"` + Label string // User Label. + Contact bool + Admin bool // Whether or not user is jfa-go admin. + JellyfinID string `badgerhold:"key"` } type customEmails struct { - UserCreated customContent `json:"userCreated"` - InviteExpiry customContent `json:"inviteExpiry"` - PasswordReset customContent `json:"passwordReset"` - UserDeleted customContent `json:"userDeleted"` - UserDisabled customContent `json:"userDisabled"` - UserEnabled customContent `json:"userEnabled"` - InviteEmail customContent `json:"inviteEmail"` - WelcomeEmail customContent `json:"welcomeEmail"` - EmailConfirmation customContent `json:"emailConfirmation"` - UserExpired customContent `json:"userExpired"` + UserCreated CustomContent `json:"userCreated"` + InviteExpiry CustomContent `json:"inviteExpiry"` + PasswordReset CustomContent `json:"passwordReset"` + UserDeleted CustomContent `json:"userDeleted"` + UserDisabled CustomContent `json:"userDisabled"` + UserEnabled CustomContent `json:"userEnabled"` + InviteEmail CustomContent `json:"inviteEmail"` + WelcomeEmail CustomContent `json:"welcomeEmail"` + EmailConfirmation CustomContent `json:"emailConfirmation"` + UserExpired CustomContent `json:"userExpired"` } -type customContent struct { +// CustomContent stores customized versions of jfa-go content, including emails and user messages. +type CustomContent struct { + Name string `json:"name" badgerhold:"key"` Enabled bool `json:"enabled,omitempty"` Content string `json:"content"` Variables []string `json:"variables,omitempty"` @@ -245,14 +460,15 @@ type customContent struct { } type userPageContent struct { - Login customContent `json:"login"` - Page customContent `json:"page"` + Login CustomContent `json:"login"` + Page CustomContent `json:"page"` } // timePattern: %Y-%m-%dT%H:%M:%S.%f type Profile struct { - Admin bool `json:"admin,omitempty"` + Name string `badgerhold:"key"` + Admin bool `json:"admin,omitempty" badgerhold:"index"` LibraryAccess string `json:"libraries,omitempty"` FromUser string `json:"fromUser,omitempty"` Policy mediabrowser.Policy `json:"policy,omitempty"` @@ -263,6 +479,7 @@ type Profile struct { } type Invite struct { + Code string `badgerhold:"key"` Created time.Time `json:"created"` NoLimit bool `json:"no-limit"` RemainingUses int `json:"remaining-uses"` @@ -964,18 +1181,16 @@ func (st *Storage) loadLangTelegram(filesystems ...fs.FS) error { type Invites map[string]Invite func (st *Storage) loadInvites() error { - return loadJSON(st.invite_path, &st.invites) + return loadJSON(st.invite_path, &st.deprecatedInvites) } func (st *Storage) storeInvites() error { - return storeJSON(st.invite_path, st.invites) + return storeJSON(st.invite_path, st.deprecatedInvites) } -func (st *Storage) loadUsers() error { - st.usersLock.Lock() - defer st.usersLock.Unlock() - if st.users == nil { - st.users = map[string]time.Time{} +func (st *Storage) loadUserExpiries() error { + if st.deprecatedUserExpiries == nil { + st.deprecatedUserExpiries = map[string]time.Time{} } temp := map[string]time.Time{} err := loadJSON(st.users_path, &temp) @@ -983,111 +1198,111 @@ func (st *Storage) loadUsers() error { return err } for id, t1 := range temp { - if _, ok := st.users[id]; !ok { - st.users[id] = t1 + if _, ok := st.deprecatedUserExpiries[id]; !ok { + st.deprecatedUserExpiries[id] = t1 } } return nil } -func (st *Storage) storeUsers() error { - return storeJSON(st.users_path, st.users) +func (st *Storage) storeUserExpiries() error { + return storeJSON(st.users_path, st.deprecatedUserExpiries) } func (st *Storage) loadEmails() error { - return loadJSON(st.emails_path, &st.emails) + return loadJSON(st.emails_path, &st.deprecatedEmails) } func (st *Storage) storeEmails() error { - return storeJSON(st.emails_path, st.emails) + return storeJSON(st.emails_path, st.deprecatedEmails) } func (st *Storage) loadTelegramUsers() error { - return loadJSON(st.telegram_path, &st.telegram) + return loadJSON(st.telegram_path, &st.deprecatedTelegram) } func (st *Storage) storeTelegramUsers() error { - return storeJSON(st.telegram_path, st.telegram) + return storeJSON(st.telegram_path, st.deprecatedTelegram) } func (st *Storage) loadDiscordUsers() error { - return loadJSON(st.discord_path, &st.discord) + return loadJSON(st.discord_path, &st.deprecatedDiscord) } func (st *Storage) storeDiscordUsers() error { - return storeJSON(st.discord_path, st.discord) + return storeJSON(st.discord_path, st.deprecatedDiscord) } func (st *Storage) loadMatrixUsers() error { - return loadJSON(st.matrix_path, &st.matrix) + return loadJSON(st.matrix_path, &st.deprecatedMatrix) } func (st *Storage) storeMatrixUsers() error { - return storeJSON(st.matrix_path, st.matrix) + return storeJSON(st.matrix_path, st.deprecatedMatrix) } func (st *Storage) loadCustomEmails() error { - return loadJSON(st.customEmails_path, &st.customEmails) + return loadJSON(st.customEmails_path, &st.deprecatedCustomEmails) } func (st *Storage) storeCustomEmails() error { - return storeJSON(st.customEmails_path, st.customEmails) + return storeJSON(st.customEmails_path, st.deprecatedCustomEmails) } func (st *Storage) loadUserPageContent() error { - return loadJSON(st.userPage_path, &st.userPage) + return loadJSON(st.userPage_path, &st.deprecatedUserPageContent) } func (st *Storage) storeUserPageContent() error { - return storeJSON(st.userPage_path, st.userPage) + return storeJSON(st.userPage_path, st.deprecatedUserPageContent) } func (st *Storage) loadPolicy() error { - return loadJSON(st.policy_path, &st.policy) + return loadJSON(st.policy_path, &st.deprecatedPolicy) } func (st *Storage) storePolicy() error { - return storeJSON(st.policy_path, st.policy) + return storeJSON(st.policy_path, st.deprecatedPolicy) } func (st *Storage) loadConfiguration() error { - return loadJSON(st.configuration_path, &st.configuration) + return loadJSON(st.configuration_path, &st.deprecatedConfiguration) } func (st *Storage) storeConfiguration() error { - return storeJSON(st.configuration_path, st.configuration) + return storeJSON(st.configuration_path, st.deprecatedConfiguration) } func (st *Storage) loadDisplayprefs() error { - return loadJSON(st.displayprefs_path, &st.displayprefs) + return loadJSON(st.displayprefs_path, &st.deprecatedDisplayprefs) } func (st *Storage) storeDisplayprefs() error { - return storeJSON(st.displayprefs_path, st.displayprefs) + return storeJSON(st.displayprefs_path, st.deprecatedDisplayprefs) } func (st *Storage) loadOmbiTemplate() error { - return loadJSON(st.ombi_path, &st.ombi_template) + return loadJSON(st.ombi_path, &st.deprecatedOmbiTemplate) } func (st *Storage) storeOmbiTemplate() error { - return storeJSON(st.ombi_path, st.ombi_template) + return storeJSON(st.ombi_path, st.deprecatedOmbiTemplate) } func (st *Storage) loadAnnouncements() error { - return loadJSON(st.announcements_path, &st.announcements) + return loadJSON(st.announcements_path, &st.deprecatedAnnouncements) } func (st *Storage) storeAnnouncements() error { - return storeJSON(st.announcements_path, st.announcements) + return storeJSON(st.announcements_path, st.deprecatedAnnouncements) } func (st *Storage) loadProfiles() error { - err := loadJSON(st.profiles_path, &st.profiles) - for name, profile := range st.profiles { - if profile.Default { - st.defaultProfile = name - } + err := loadJSON(st.profiles_path, &st.deprecatedProfiles) + for name, profile := range st.deprecatedProfiles { + // if profile.Default { + // st.defaultProfile = name + // } change := false if profile.Policy.IsAdministrator != profile.Admin { change = true @@ -1107,19 +1322,19 @@ func (st *Storage) loadProfiles() error { change = true } if change { - st.profiles[name] = profile - } - } - if st.defaultProfile == "" { - for n := range st.profiles { - st.defaultProfile = n + st.deprecatedProfiles[name] = profile } } + // if st.defaultProfile == "" { + // for n := range st.deprecatedProfiles { + // st.defaultProfile = n + // } + // } return err } func (st *Storage) storeProfiles() error { - return storeJSON(st.profiles_path, st.profiles) + return storeJSON(st.profiles_path, st.deprecatedProfiles) } func (st *Storage) migrateToProfile() error { @@ -1127,10 +1342,10 @@ func (st *Storage) migrateToProfile() error { st.loadConfiguration() st.loadDisplayprefs() st.loadProfiles() - st.profiles["Default"] = Profile{ - Policy: st.policy, - Configuration: st.configuration, - Displayprefs: st.displayprefs, + st.deprecatedProfiles["Default"] = Profile{ + Policy: st.deprecatedPolicy, + Configuration: st.deprecatedConfiguration, + Displayprefs: st.deprecatedDisplayprefs, } return st.storeProfiles() } diff --git a/telegram.go b/telegram.go index a50292c..d5f5a67 100644 --- a/telegram.go +++ b/telegram.go @@ -7,6 +7,7 @@ import ( "time" tg "github.com/go-telegram-bot-api/telegram-bot-api" + "github.com/timshannon/badgerhold/v4" ) const ( @@ -216,13 +217,10 @@ func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string } if _, ok := t.app.storage.lang.Telegram[sects[1]]; ok { t.languages[upd.Message.Chat.ID] = sects[1] - for jfID, user := range t.app.storage.GetTelegram() { + for _, user := range t.app.storage.GetTelegram() { if user.ChatID == upd.Message.Chat.ID { user.Lang = sects[1] - t.app.storage.SetTelegramKey(jfID, user) - if err := t.app.storage.storeTelegramUsers(); err != nil { - t.app.err.Printf("Failed to store Telegram users: %v", err) - } + t.app.storage.SetTelegramKey(user.JellyfinID, user) break } } @@ -270,15 +268,9 @@ func (t *TelegramDaemon) AssignedTokenVerified(pin string, jfID string) (token T } // UserExists returns whether or not a user with the given username exists. -func (t *TelegramDaemon) UserExists(username string) (ok bool) { - ok = false - for _, u := range t.app.storage.GetTelegram() { - if u.Username == username { - ok = true - break - } - } - return +func (t *TelegramDaemon) UserExists(username string) bool { + c, err := t.app.storage.db.Count(&TelegramUser{}, badgerhold.Where("Username").Eq(username)) + return err != nil || c > 0 } // DeleteVerifiedToken removes the token with the given PIN. diff --git a/userdaemon.go b/userdaemon.go index 4db091f..de0e0ce 100644 --- a/userdaemon.go +++ b/userdaemon.go @@ -50,13 +50,7 @@ func (rt *userDaemon) shutdown() { } func (app *appContext) checkUsers() { - if err := app.storage.loadUsers(); err != nil { - app.err.Printf("Failed to load user expiries: %v", err) - return - } - app.storage.usersLock.Lock() - defer app.storage.usersLock.Unlock() - if len(app.storage.users) == 0 { + if len(app.storage.GetUserExpiries()) == 0 { return } app.info.Println("Daemon: Checking for user expiry") @@ -80,11 +74,12 @@ func (app *appContext) checkUsers() { for _, user := range users { userExists[user.ID] = true } - for id, expiry := range app.storage.users { + for _, expiry := range app.storage.GetUserExpiries() { + id := expiry.JellyfinID if _, ok := userExists[id]; !ok { app.info.Printf("Deleting expiry for non-existent user \"%s\"", id) - delete(app.storage.users, id) - } else if time.Now().After(expiry) { + app.storage.DeleteUserExpiryKey(expiry.JellyfinID) + } else if time.Now().After(expiry.Expiry) { found := false var user mediabrowser.User for _, u := range users { @@ -96,7 +91,7 @@ func (app *appContext) checkUsers() { } if !found { app.info.Printf("Expired user already deleted, ignoring.") - delete(app.storage.users, id) + app.storage.DeleteUserExpiryKey(expiry.JellyfinID) continue } app.info.Printf("%s expired user \"%s\"", termPlural, user.Name) @@ -112,7 +107,7 @@ func (app *appContext) checkUsers() { app.err.Printf("Failed to %s \"%s\" (%d): %s", mode, user.Name, status, err) continue } - delete(app.storage.users, id) + app.storage.DeleteUserExpiryKey(expiry.JellyfinID) app.jf.CacheExpiry = time.Now() if contact { if !ok { @@ -130,8 +125,4 @@ func (app *appContext) checkUsers() { } } } - err = app.storage.storeUsers() - if err != nil { - app.err.Printf("Failed to store user expiries: %s", err) - } } diff --git a/views.go b/views.go index b9df9a3..f2e2dbc 100644 --- a/views.go +++ b/views.go @@ -224,13 +224,13 @@ func (app *appContext) MyUserPage(gc *gin.Context) { data["discordInviteLink"] = app.discord.inviteChannelName != "" } - pageMessages := map[string]*customContent{ - "Login": app.getCustomMessage("UserLogin"), - "Page": app.getCustomMessage("UserPage"), - } + pageMessagesExist := map[string]bool{} + pageMessages := map[string]CustomContent{} + pageMessages["Login"], pageMessagesExist["Login"] = app.storage.GetCustomContentKey("UserLogin") + pageMessages["Page"], pageMessagesExist["Page"] = app.storage.GetCustomContentKey("UserPage") for name, msg := range pageMessages { - if msg == nil { + if !pageMessagesExist[name] { continue } data[name+"MessageEnabled"] = msg.Enabled