add email notify enable/disable; remove (de)hyphening

hyphen/dehyphen conflicted with new migration for email contact
preference, and it's been a while since this has been an issue so i've
just commented it out for now.
This commit is contained in:
Harvey Tindall 2021-05-21 22:46:46 +01:00
parent 3bf722c5fe
commit a6447165b7
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
8 changed files with 229 additions and 175 deletions

46
api.go
View File

@ -217,7 +217,7 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
username := jfUser.Name username := jfUser.Name
email := "" email := ""
if e, ok := app.storage.emails[jfID]; ok { if e, ok := app.storage.emails[jfID]; ok {
email = e.(string) email = e.Addr
} }
for _, ombiUser := range ombiUsers { for _, ombiUser := range ombiUsers {
ombiAddr := "" ombiAddr := ""
@ -283,7 +283,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
} }
app.jf.CacheExpiry = time.Now() app.jf.CacheExpiry = time.Now()
if emailEnabled { if emailEnabled {
app.storage.emails[id] = req.Email app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
app.storage.storeEmails() app.storage.storeEmails()
} }
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
@ -478,7 +478,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
} }
// if app.config.Section("password_resets").Key("enabled").MustBool(false) { // if app.config.Section("password_resets").Key("enabled").MustBool(false) {
if req.Email != "" { if req.Email != "" {
app.storage.emails[id] = req.Email app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
app.storage.storeEmails() app.storage.storeEmails()
} }
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {
@ -908,8 +908,8 @@ func (app *appContext) GetInvites(gc *gin.Context) {
var address string var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
app.storage.loadEmails() app.storage.loadEmails()
if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil { if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
address = addr.(string) address = addr.Addr
} }
} else { } else {
address = app.config.Section("ui").Key("email").String() address = app.config.Section("ui").Key("email").String()
@ -1108,14 +1108,14 @@ func (app *appContext) SetNotify(gc *gin.Context) {
} }
var address string var address string
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) { if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
var ok bool addr, ok := app.storage.emails[gc.GetString("jfId")]
address, ok = app.storage.emails[gc.GetString("jfId")].(string)
if !ok { if !ok {
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code) app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId")) app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
respond(500, "Missing user email", gc) respond(500, "Missing user email", gc)
return return
} }
address = addr.Addr
} else { } else {
address = app.config.Section("ui").Key("email").String() address = app.config.Section("ui").Key("email").String()
} }
@ -1202,7 +1202,7 @@ func (app *appContext) GetUsers(gc *gin.Context) {
user.LastActive = jfUser.LastActivityDate.Unix() user.LastActive = jfUser.LastActivityDate.Unix()
} }
if email, ok := app.storage.emails[jfUser.ID]; ok { if email, ok := app.storage.emails[jfUser.ID]; ok {
user.Email = email.(string) user.Email = email.Addr
user.NotifyThroughEmail = user.Email != "" user.NotifyThroughEmail = user.Email != ""
} }
expiry, ok := app.storage.users[jfUser.ID] expiry, ok := app.storage.users[jfUser.ID]
@ -1293,7 +1293,11 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
for _, jfUser := range users { for _, jfUser := range users {
id := jfUser.ID id := jfUser.ID
if address, ok := req[id]; ok { if address, ok := req[id]; ok {
app.storage.emails[id] = address contact := true
if oldAddr, ok := app.storage.emails[id]; ok {
contact = oldAddr.Contact
}
app.storage.emails[id] = EmailAddress{Addr: address, Contact: contact}
if ombiEnabled { if ombiEnabled {
ombiUser, code, err := app.getOmbiUser(id) ombiUser, code, err := app.getOmbiUser(id)
if code == 200 && err == nil { if code == 200 && err == nil {
@ -2052,7 +2056,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
// @Security Bearer // @Security Bearer
// @tags Other // @tags Other
func (app *appContext) SetContactMethods(gc *gin.Context) { func (app *appContext) SetContactMethods(gc *gin.Context) {
var req telegramNotifyDTO var req SetContactMethodsDTO
gc.BindJSON(&req) gc.BindJSON(&req)
if req.ID == "" { if req.ID == "" {
respondBool(400, false, gc) respondBool(400, false, gc)
@ -2068,9 +2072,9 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
} }
msg := "" msg := ""
if !req.Telegram { if !req.Telegram {
msg = "not" msg = " not"
} }
app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg) app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
} }
if dcUser, ok := app.storage.discord[req.ID]; ok { if dcUser, ok := app.storage.discord[req.ID]; ok {
dcUser.Contact = req.Discord dcUser.Contact = req.Discord
@ -2082,9 +2086,23 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
} }
msg := "" msg := ""
if !req.Discord { if !req.Discord {
msg = "not" msg = " not"
} }
app.debug.Printf("Discord: User \"%s\" will %s be notified through Discord.", dcUser.Username, msg) app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
}
if email, ok := app.storage.emails[req.ID]; ok {
email.Contact = req.Email
app.storage.emails[req.ID] = email
if err := app.storage.storeEmails(); err != nil {
respondBool(500, false, gc)
app.err.Printf("Failed to store emails: %v", err)
return
}
msg := ""
if !req.Email {
msg = " not"
}
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
} }
respondBool(200, true, gc) respondBool(200, true, gc)
} }

View File

@ -171,3 +171,28 @@ func (app *appContext) migrateEmailConfig() {
} }
app.loadConfig() app.loadConfig()
} }
func (app *appContext) migrateEmailStorage() error {
var emails map[string]interface{}
err := loadJSON(app.storage.emails_path, &emails)
if err != nil {
return err
}
newEmails := map[string]EmailAddress{}
for jfID, addr := range emails {
newEmails[jfID] = EmailAddress{
Addr: addr.(string),
Contact: true,
}
}
err = storeJSON(app.storage.emails_path+".bak", emails)
if err != nil {
return err
}
err = storeJSON(app.storage.emails_path, newEmails)
if err != nil {
return err
}
app.info.Println("Migrated to new email format. A backup has also been made.")
return nil
}

View File

@ -817,8 +817,8 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
return err return err
} }
} }
if address, ok := app.storage.emails[id]; ok { if address, ok := app.storage.emails[id]; ok && address.Contact && emailEnabled {
err = app.email.send(email, address.(string)) err = app.email.send(email, address.Addr)
if err != nil { if err != nil {
return err return err
} }
@ -838,7 +838,7 @@ func (app *appContext) getAddressOrName(jfID string) string {
return "@" + tgChat.Username return "@" + tgChat.Username
} }
if addr, ok := app.storage.emails[jfID]; ok { if addr, ok := app.storage.emails[jfID]; ok {
return addr.(string) return addr.Addr
} }
return "" return ""
} }

143
main.go
View File

@ -16,7 +16,6 @@ import (
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "time"
@ -321,6 +320,10 @@ func start(asDaemon, firstCall bool) {
app.storage.emails_path = app.config.Section("files").Key("emails").String() app.storage.emails_path = app.config.Section("files").Key("emails").String()
if err := app.storage.loadEmails(); err != nil { if err := app.storage.loadEmails(); err != nil {
app.err.Printf("Failed to load Emails: %v", err) app.err.Printf("Failed to load Emails: %v", err)
err := app.migrateEmailStorage()
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() app.storage.policy_path = app.config.Section("files").Key("user_template").String()
if err := app.storage.loadPolicy(); err != nil { if err := app.storage.loadPolicy(); err != nil {
@ -434,76 +437,76 @@ func start(asDaemon, firstCall bool) {
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err) app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err)
} }
app.info.Printf("Authenticated with %s", server) app.info.Printf("Authenticated with %s", server)
/* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs. // /* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs.
This checks if the version is equal or higher. */ // This checks if the version is equal or higher. */
checkVersion := func(version string) int { // checkVersion := func(version string) int {
numberStrings := strings.Split(version, ".") // numberStrings := strings.Split(version, ".")
n := 0 // n := 0
for _, s := range numberStrings { // for _, s := range numberStrings {
num, err := strconv.Atoi(s) // num, err := strconv.Atoi(s)
if err == nil { // if err == nil {
n += num // n += num
} // }
} // }
return n // return n
} // }
if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") { // if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") {
// Get users to check if server uses hyphenated userIDs // // Get users to check if server uses hyphenated userIDs
app.jf.GetUsers(false) // app.jf.GetUsers(false)
noHyphens := true // noHyphens := true
for id := range app.storage.emails { // for id := range app.storage.emails {
if strings.Contains(id, "-") { // if strings.Contains(id, "-") {
noHyphens = false // noHyphens = false
break // break
} // }
} // }
if noHyphens == app.jf.Hyphens { // if noHyphens == app.jf.Hyphens {
var newEmails map[string]interface{} // var newEmails map[string]interface{}
var newUsers map[string]time.Time // var newUsers map[string]time.Time
var status, status2 int // var status, status2 int
var err, err2 error // var err, err2 error
if app.jf.Hyphens { // if app.jf.Hyphens {
app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match.")) // app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
time.Sleep(time.Second * time.Duration(3)) // time.Sleep(time.Second * time.Duration(3))
newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails) // newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users) // newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
} else { // } else {
app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified.")) // app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
time.Sleep(time.Second * time.Duration(3)) // time.Sleep(time.Second * time.Duration(3))
newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails) // newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users) // newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
} // }
if status != 200 || err != nil { // if status != 200 || err != nil {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) // app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
app.err.Fatalf("Couldn't upgrade emails.json") // app.err.Fatalf("Couldn't upgrade emails.json")
} // }
if status2 != 200 || err2 != nil { // if status2 != 200 || err2 != nil {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err) // app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
app.err.Fatalf("Couldn't upgrade users.json") // app.err.Fatalf("Couldn't upgrade users.json")
} // }
emailBakFile := app.storage.emails_path + ".bak" // emailBakFile := app.storage.emails_path + ".bak"
usersBakFile := app.storage.users_path + ".bak" // usersBakFile := app.storage.users_path + ".bak"
err = storeJSON(emailBakFile, app.storage.emails) // err = storeJSON(emailBakFile, app.storage.emails)
err2 = storeJSON(usersBakFile, app.storage.users) // err2 = storeJSON(usersBakFile, app.storage.users)
if err != nil { // if err != nil {
app.err.Fatalf("couldn't store emails.json backup: %v", err) // app.err.Fatalf("couldn't store emails.json backup: %v", err)
} // }
if err2 != nil { // if err2 != nil {
app.err.Fatalf("couldn't store users.json backup: %v", err) // app.err.Fatalf("couldn't store users.json backup: %v", err)
} // }
app.storage.emails = newEmails // app.storage.emails = newEmails
app.storage.users = newUsers // app.storage.users = newUsers
err = app.storage.storeEmails() // err = app.storage.storeEmails()
err2 = app.storage.storeUsers() // err2 = app.storage.storeUsers()
if err != nil { // if err != nil {
app.err.Fatalf("couldn't store emails.json: %v", err) // app.err.Fatalf("couldn't store emails.json: %v", err)
} // }
if err2 != nil { // if err2 != nil {
app.err.Fatalf("couldn't store users.json: %v", err) // app.err.Fatalf("couldn't store users.json: %v", err)
} // }
} // }
} // }
// Auth (manual user/pass or jellyfin) // Auth (manual user/pass or jellyfin)
app.jellyfinLogin = true app.jellyfinLogin = true

View File

@ -255,7 +255,7 @@ type telegramSetDTO struct {
ID string `json:"id"` // Jellyfin ID of user. ID string `json:"id"` // Jellyfin ID of user.
} }
type telegramNotifyDTO struct { type SetContactMethodsDTO struct {
ID string `json:"id"` ID string `json:"id"`
Email bool `json:"email"` Email bool `json:"email"`
Discord bool `json:"discord"` Discord bool `json:"discord"`

View File

@ -161,10 +161,12 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/config", app.GetConfig) api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart) api.POST(p+"/restart", app.restart)
if telegramEnabled { if telegramEnabled || discordEnabled {
api.GET(p+"/telegram/pin", app.TelegramGetPin) api.GET(p+"/telegram/pin", app.TelegramGetPin)
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified) api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
api.POST(p+"/users/telegram", app.TelegramAddUser) api.POST(p+"/users/telegram", app.TelegramAddUser)
}
if discordEnabled || telegramEnabled {
api.POST(p+"/users/contact", app.SetContactMethods) api.POST(p+"/users/contact", app.SetContactMethods)
} }
if app.config.Section("ombi").Key("enabled").MustBool(false) { if app.config.Section("ombi").Key("enabled").MustBool(false) {

View File

@ -21,7 +21,8 @@ type Storage struct {
invites Invites invites Invites
profiles map[string]Profile profiles map[string]Profile
defaultProfile string defaultProfile string
emails, displayprefs, ombi_template map[string]interface{} displayprefs, ombi_template map[string]interface{}
emails map[string]EmailAddress
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users. telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users. discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
customEmails customEmails customEmails customEmails
@ -47,6 +48,11 @@ type DiscordUser struct {
Contact bool Contact bool
} }
type EmailAddress struct {
Addr string
Contact bool
}
type customEmails struct { type customEmails struct {
UserCreated customEmail `json:"userCreated"` UserCreated customEmail `json:"userCreated"`
InviteExpiry customEmail `json:"inviteExpiry"` InviteExpiry customEmail `json:"inviteExpiry"`
@ -902,85 +908,85 @@ func storeJSON(path string, obj interface{}) error {
return err return err
} }
// One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage. // // One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage.
//
func hyphenate(userID string) string { // func hyphenate(userID string) string {
if userID[8] == '-' { // if userID[8] == '-' {
return userID // return userID
} // }
return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:] // return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
} // }
//
func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) { // func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
jfUsers, status, err := app.jf.GetUsers(false) // jfUsers, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil { // if status != 200 || err != nil {
return nil, status, err // return nil, status, err
} // }
newEmails := map[string]interface{}{} // newEmails := map[string]interface{}{}
for _, user := range jfUsers { // for _, user := range jfUsers {
unHyphenated := user.ID // unHyphenated := user.ID
hyphenated := hyphenate(unHyphenated) // hyphenated := hyphenate(unHyphenated)
val, ok := old[hyphenated] // val, ok := old[hyphenated]
if ok { // if ok {
newEmails[unHyphenated] = val // newEmails[unHyphenated] = val
} // }
} // }
return newEmails, status, err // return newEmails, status, err
} // }
//
func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) { // func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
jfUsers, status, err := app.jf.GetUsers(false) // jfUsers, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil { // if status != 200 || err != nil {
return nil, status, err // return nil, status, err
} // }
newEmails := map[string]interface{}{} // newEmails := map[string]interface{}{}
for _, user := range jfUsers { // for _, user := range jfUsers {
unstripped := user.ID // unstripped := user.ID
stripped := strings.ReplaceAll(unstripped, "-", "") // stripped := strings.ReplaceAll(unstripped, "-", "")
val, ok := old[stripped] // val, ok := old[stripped]
if ok { // if ok {
newEmails[unstripped] = val // newEmails[unstripped] = val
} // }
} // }
return newEmails, status, err // return newEmails, status, err
} // }
//
func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) { // func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
return app.hyphenateStorage(old) // return app.hyphenateStorage(old)
} // }
//
func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) { // func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
return app.deHyphenateStorage(old) // return app.deHyphenateStorage(old)
} // }
//
func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) { // func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
asInterface := map[string]interface{}{} // asInterface := map[string]interface{}{}
for k, v := range old { // for k, v := range old {
asInterface[k] = v // asInterface[k] = v
} // }
fixed, status, err := app.hyphenateStorage(asInterface) // fixed, status, err := app.hyphenateStorage(asInterface)
if err != nil { // if err != nil {
return nil, status, err // return nil, status, err
} // }
out := map[string]time.Time{} // out := map[string]time.Time{}
for k, v := range fixed { // for k, v := range fixed {
out[k] = v.(time.Time) // out[k] = v.(time.Time)
} // }
return out, status, err // return out, status, err
} // }
//
func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) { // func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
asInterface := map[string]interface{}{} // asInterface := map[string]interface{}{}
for k, v := range old { // for k, v := range old {
asInterface[k] = v // asInterface[k] = v
} // }
fixed, status, err := app.deHyphenateStorage(asInterface) // fixed, status, err := app.deHyphenateStorage(asInterface)
if err != nil { // if err != nil {
return nil, status, err // return nil, status, err
} // }
out := map[string]time.Time{} // out := map[string]time.Time{}
for k, v := range fixed { // for k, v := range fixed {
out[k] = v.(time.Time) // out[k] = v.(time.Time)
} // }
return out, status, err // return out, status, err
} // }

View File

@ -228,18 +228,18 @@ class user implements User {
<div class="card ~neutral !low"> <div class="card ~neutral !low">
<span class="supra sm">${window.lang.strings("contactThrough")}</span> <span class="supra sm">${window.lang.strings("contactThrough")}</span>
<label class="switch pb-1 mt-half"> <label class="switch pb-1 mt-half">
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-email"> <input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
<span>Email</span> <span>Email</span>
</label> </label>
<label class="switch pb-1"> <label class="switch pb-1">
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-discord"> <input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
<span>Discord</span> <span>Discord</span>
</label> </label>
`; `;
if (window.telegramEnabled && this._telegramUsername != "") { if (window.telegramEnabled && this._telegramUsername != "") {
innerHTML += ` innerHTML += `
<label class="switch pb-1"> <label class="switch pb-1">
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-telegram"> <input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
<span>Telegram</span> <span>Telegram</span>
</label> </label>
`; `;