Files
jfa-go/storage.go
T
Harvey Tindall eb406ef951 Implement email template generation
Variables are surrounded by {}, and initial (default) templates are
generated on demand from the plaintext version of emails. The custom
emails are intended to only be used if the user actually changes them,
as they lose the features of the default ones, such as tables.
2021-02-19 21:38:20 +00:00

569 lines
15 KiB
Go

package main
import (
"encoding/json"
"io/fs"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/hrfee/jfa-go/mediabrowser"
)
type Storage struct {
timePattern string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path string
invites Invites
profiles map[string]Profile
defaultProfile string
emails, displayprefs, ombi_template map[string]interface{}
customEmails customEmails
policy mediabrowser.Policy
configuration mediabrowser.Configuration
lang Lang
}
type customEmails struct {
UserCreated string `json:"userCreated"`
InviteExpiry string `json:"inviteExpiry"`
PasswordReset string `json:"passwordReset"`
UserDeleted string `json:"userDeleted"`
InviteEmail string `json:"inviteEmail"`
WelcomeEmail string `json:"welcomeEmail"`
EmailConfirmation string `json:"emailConfirmation"`
}
// timePattern: %Y-%m-%dT%H:%M:%S.%f
type Profile struct {
Admin bool `json:"admin,omitempty"`
LibraryAccess string `json:"libraries,omitempty"`
FromUser string `json:"fromUser,omitempty"`
Policy mediabrowser.Policy `json:"policy,omitempty"`
Configuration mediabrowser.Configuration `json:"configuration,omitempty"`
Displayprefs map[string]interface{} `json:"displayprefs,omitempty"`
Default bool `json:"default,omitempty"`
}
type Invite struct {
Created time.Time `json:"created"`
NoLimit bool `json:"no-limit"`
RemainingUses int `json:"remaining-uses"`
ValidTill time.Time `json:"valid_till"`
Email string `json:"email"`
UsedBy [][]string `json:"used-by"`
Notify map[string]map[string]bool `json:"notify"`
Profile string `json:"profile"`
Label string `json:"label,omitempty"`
Keys []string `json"keys,omitempty"`
}
type Lang struct {
chosenFormLang string
chosenAdminLang string
chosenEmailLang string
AdminPath string
Admin adminLangs
AdminJSON map[string]string
FormPath string
Form formLangs
EmailPath string
Email emailLangs
CommonPath string
Common commonLangs
SetupPath string
Setup setupLangs
}
func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
err = st.loadLangCommon(filesystems...)
if err != nil {
return
}
err = st.loadLangAdmin(filesystems...)
if err != nil {
return
}
err = st.loadLangForm(filesystems...)
if err != nil {
return
}
err = st.loadLangEmail(filesystems...)
return
}
func (common *commonLangs) patchCommon(lang string, other *langSection) {
if *other == nil {
*other = langSection{}
}
if _, ok := (*common)[lang]; !ok {
lang = "en-us"
}
for n, ev := range (*common)[lang].Strings {
if v, ok := (*other)[n]; !ok || v == "" {
(*other)[n] = ev
}
}
}
// If a given language has missing values, fill it in with the english value.
func patchLang(english, other *langSection) {
if *other == nil {
*other = langSection{}
}
for n, ev := range *english {
if v, ok := (*other)[n]; !ok || v == "" {
(*other)[n] = ev
}
}
}
func patchQuantityStrings(english, other *map[string]quantityString) {
for n, ev := range *english {
qs, ok := (*other)[n]
if !ok {
(*other)[n] = ev
return
} else if qs.Singular == "" {
qs.Singular = ev.Singular
} else if (*other)[n].Plural == "" {
qs.Plural = ev.Plural
}
(*other)[n] = qs
}
}
func (st *Storage) loadLangCommon(filesystems ...fs.FS) error {
st.lang.Common = map[string]commonLang{}
var english commonLang
load := func(filesystem fs.FS, fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := commonLang{}
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.CommonPath, fname))
if err != nil {
return err
}
if substituteStrings != "" {
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
}
err = json.Unmarshal(f, &lang)
if err != nil {
return err
}
if fname != "en-us.json" {
patchLang(&english.Strings, &lang.Strings)
}
st.lang.Common[index] = lang
return nil
}
engFound := false
var err error
for _, filesystem := range filesystems {
err = load(filesystem, "en-us.json")
if err == nil {
engFound = true
}
}
if !engFound {
return err
}
english = st.lang.Common["en-us"]
commonLoaded := false
for _, filesystem := range filesystems {
files, err := fs.ReadDir(filesystem, st.lang.CommonPath)
if err != nil {
continue
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(filesystem, f.Name())
if err == nil {
commonLoaded = true
}
}
}
}
if !commonLoaded {
return err
}
return nil
}
func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
st.lang.Admin = map[string]adminLang{}
var english adminLang
load := func(filesystem fs.FS, fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := adminLang{}
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.AdminPath, fname))
if err != nil {
return err
}
if substituteStrings != "" {
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
}
err = json.Unmarshal(f, &lang)
if err != nil {
return err
}
st.lang.Common.patchCommon(index, &lang.Strings)
if fname != "en-us.json" {
patchLang(&english.Strings, &lang.Strings)
patchLang(&english.Notifications, &lang.Notifications)
patchQuantityStrings(&english.QuantityStrings, &lang.QuantityStrings)
}
stringAdmin, err := json.Marshal(lang)
if err != nil {
return err
}
lang.JSON = string(stringAdmin)
st.lang.Admin[index] = lang
return nil
}
engFound := false
var err error
for _, filesystem := range filesystems {
err = load(filesystem, "en-us.json")
if err == nil {
engFound = true
}
}
if !engFound {
return err
}
english = st.lang.Admin["en-us"]
adminLoaded := false
for _, filesystem := range filesystems {
files, err := fs.ReadDir(filesystem, st.lang.AdminPath)
if err != nil {
continue
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(filesystem, f.Name())
if err == nil {
adminLoaded = true
}
}
}
}
if !adminLoaded {
return err
}
return nil
}
func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
st.lang.Form = map[string]formLang{}
var english formLang
load := func(filesystem fs.FS, fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := formLang{}
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.FormPath, fname))
if err != nil {
return err
}
if substituteStrings != "" {
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
}
err = json.Unmarshal(f, &lang)
if err != nil {
return err
}
st.lang.Common.patchCommon(index, &lang.Strings)
if fname != "en-us.json" {
patchLang(&english.Strings, &lang.Strings)
patchLang(&english.Notifications, &lang.Notifications)
patchQuantityStrings(&english.ValidationStrings, &lang.ValidationStrings)
}
notifications, err := json.Marshal(lang.Notifications)
if err != nil {
return err
}
validationStrings, err := json.Marshal(lang.ValidationStrings)
if err != nil {
return err
}
lang.notificationsJSON = string(notifications)
lang.validationStringsJSON = string(validationStrings)
st.lang.Form[index] = lang
return nil
}
engFound := false
var err error
for _, filesystem := range filesystems {
err = load(filesystem, "en-us.json")
if err == nil {
engFound = true
}
}
if !engFound {
return err
}
english = st.lang.Form["en-us"]
formLoaded := false
for _, filesystem := range filesystems {
files, err := fs.ReadDir(filesystem, st.lang.FormPath)
if err != nil {
continue
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(filesystem, f.Name())
if err == nil {
formLoaded = true
}
}
}
}
if !formLoaded {
return err
}
return nil
}
func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
st.lang.Email = map[string]emailLang{}
var english emailLang
load := func(filesystem fs.FS, fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := emailLang{}
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.EmailPath, fname))
if err != nil {
return err
}
if substituteStrings != "" {
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
}
err = json.Unmarshal(f, &lang)
if err != nil {
return err
}
st.lang.Common.patchCommon(index, &lang.Strings)
if fname != "en-us.json" {
patchLang(&english.UserCreated, &lang.UserCreated)
patchLang(&english.InviteExpiry, &lang.InviteExpiry)
patchLang(&english.PasswordReset, &lang.PasswordReset)
patchLang(&english.UserDeleted, &lang.UserDeleted)
patchLang(&english.InviteEmail, &lang.InviteEmail)
patchLang(&english.WelcomeEmail, &lang.WelcomeEmail)
}
st.lang.Email[index] = lang
return nil
}
engFound := false
var err error
for _, filesystem := range filesystems {
err = load(filesystem, "en-us.json")
if err == nil {
engFound = true
}
}
if !engFound {
return err
}
english = st.lang.Email["en-us"]
emailLoaded := false
for _, filesystem := range filesystems {
files, err := fs.ReadDir(filesystem, st.lang.EmailPath)
if err != nil {
continue
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(filesystem, f.Name())
if err == nil {
emailLoaded = true
}
}
}
}
if !emailLoaded {
return err
}
return nil
}
type Invites map[string]Invite
func (st *Storage) loadInvites() error {
return loadJSON(st.invite_path, &st.invites)
}
func (st *Storage) storeInvites() error {
return storeJSON(st.invite_path, st.invites)
}
func (st *Storage) loadEmails() error {
return loadJSON(st.emails_path, &st.emails)
}
func (st *Storage) storeEmails() error {
return storeJSON(st.emails_path, st.emails)
}
func (st *Storage) loadCustomEmails() error {
return loadJSON(st.customEmails_path, &st.customEmails)
}
func (st *Storage) storeCustomEmails() error {
return storeJSON(st.customEmails_path, st.customEmails)
}
func (st *Storage) loadPolicy() error {
return loadJSON(st.policy_path, &st.policy)
}
func (st *Storage) storePolicy() error {
return storeJSON(st.policy_path, st.policy)
}
func (st *Storage) loadConfiguration() error {
return loadJSON(st.configuration_path, &st.configuration)
}
func (st *Storage) storeConfiguration() error {
return storeJSON(st.configuration_path, st.configuration)
}
func (st *Storage) loadDisplayprefs() error {
return loadJSON(st.displayprefs_path, &st.displayprefs)
}
func (st *Storage) storeDisplayprefs() error {
return storeJSON(st.displayprefs_path, st.displayprefs)
}
func (st *Storage) loadOmbiTemplate() error {
return loadJSON(st.ombi_path, &st.ombi_template)
}
func (st *Storage) storeOmbiTemplate() error {
return storeJSON(st.ombi_path, st.ombi_template)
}
func (st *Storage) loadProfiles() error {
err := loadJSON(st.profiles_path, &st.profiles)
for name, profile := range st.profiles {
if profile.Default {
st.defaultProfile = name
}
change := false
if profile.Policy.IsAdministrator != profile.Admin {
change = true
}
profile.Admin = profile.Policy.IsAdministrator
if profile.Policy.EnabledFolders != nil {
length := len(profile.Policy.EnabledFolders)
if length == 0 {
profile.LibraryAccess = "All"
} else {
profile.LibraryAccess = strconv.Itoa(length)
}
change = true
}
if profile.FromUser == "" {
profile.FromUser = "Unknown"
change = true
}
if change {
st.profiles[name] = profile
}
}
if st.defaultProfile == "" {
for n := range st.profiles {
st.defaultProfile = n
}
}
return err
}
func (st *Storage) storeProfiles() error {
return storeJSON(st.profiles_path, st.profiles)
}
func (st *Storage) migrateToProfile() error {
st.loadPolicy()
st.loadConfiguration()
st.loadDisplayprefs()
st.loadProfiles()
st.profiles["Default"] = Profile{
Policy: st.policy,
Configuration: st.configuration,
Displayprefs: st.displayprefs,
}
return st.storeProfiles()
}
func loadJSON(path string, obj interface{}) error {
var file []byte
var err error
file, err = os.ReadFile(path)
if err != nil {
file = []byte("{}")
}
err = json.Unmarshal(file, &obj)
if err != nil {
log.Printf("ERROR: Failed to read \"%s\": %s", path, err)
}
return err
}
func storeJSON(path string, obj interface{}) error {
data, err := json.Marshal(obj)
if err != nil {
return err
}
err = os.WriteFile(path, data, 0644)
if err != nil {
log.Printf("ERROR: Failed to write to \"%s\": %s", path, 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.
func hyphenate(userID string) string {
if userID[8] == '-' {
return userID
}
return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
}
func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
jfUsers, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil {
return nil, status, err
}
newEmails := map[string]interface{}{}
for _, user := range jfUsers {
unHyphenated := user.ID
hyphenated := hyphenate(unHyphenated)
email, ok := old[hyphenated]
if ok {
newEmails[unHyphenated] = email
}
}
return newEmails, status, err
}
func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
jfUsers, status, err := app.jf.GetUsers(false)
if status != 200 || err != nil {
return nil, status, err
}
newEmails := map[string]interface{}{}
for _, user := range jfUsers {
unstripped := user.ID
stripped := strings.ReplaceAll(unstripped, "-", "")
email, ok := old[stripped]
if ok {
newEmails[unstripped] = email
}
}
return newEmails, status, err
}