mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-06-28 12:27:47 +02:00
Harvey Tindall
99875b9176
When signing up, the user is given a pin code which they send to a telegram bot. This provides user verification, but more importantly allows the bot to message the user, as the Telegram API requires the user to interact with the bot before it can do the opposite. The bot should recognize the correct language, but a /lang command is also provided to change it. The verification process is pretty much functional but ui is still broken, and it isn't properly integrated yet.
968 lines
27 KiB
Go
968 lines
27 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hrfee/mediabrowser"
|
|
)
|
|
|
|
type Storage struct {
|
|
timePattern string
|
|
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path string
|
|
users map[string]time.Time
|
|
invites Invites
|
|
profiles map[string]Profile
|
|
defaultProfile string
|
|
emails, displayprefs, ombi_template map[string]interface{}
|
|
telegram map[int64]TelegramUser
|
|
customEmails customEmails
|
|
policy mediabrowser.Policy
|
|
configuration mediabrowser.Configuration
|
|
lang Lang
|
|
invitesLock, usersLock sync.Mutex
|
|
}
|
|
|
|
type TelegramUser struct {
|
|
ChatID int64
|
|
Username string
|
|
Lang string
|
|
}
|
|
|
|
type customEmails struct {
|
|
UserCreated customEmail `json:"userCreated"`
|
|
InviteExpiry customEmail `json:"inviteExpiry"`
|
|
PasswordReset customEmail `json:"passwordReset"`
|
|
UserDeleted customEmail `json:"userDeleted"`
|
|
UserDisabled customEmail `json:"userDisabled"`
|
|
UserEnabled customEmail `json:"userEnabled"`
|
|
InviteEmail customEmail `json:"inviteEmail"`
|
|
WelcomeEmail customEmail `json:"welcomeEmail"`
|
|
EmailConfirmation customEmail `json:"emailConfirmation"`
|
|
UserExpired customEmail `json:"userExpired"`
|
|
}
|
|
|
|
type customEmail struct {
|
|
Enabled bool `json:"enabled,omitempty"`
|
|
Content string `json:"content"`
|
|
Variables []string `json:"variables,omitempty"`
|
|
Conditionals []string `json:"conditionals,omitempty"`
|
|
}
|
|
|
|
// 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"`
|
|
UserExpiry bool `json:"user-duration"`
|
|
UserMonths int `json:"user-months,omitempty"`
|
|
UserDays int `json:"user-days,omitempty"`
|
|
UserHours int `json:"user-hours,omitempty"`
|
|
UserMinutes int `json:"user-minutes,omitempty"`
|
|
Email string `json:"email"`
|
|
// Used to be stored as formatted time, now as Unix.
|
|
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 {
|
|
AdminPath string
|
|
chosenAdminLang string
|
|
Admin adminLangs
|
|
AdminJSON map[string]string
|
|
FormPath string
|
|
chosenFormLang string
|
|
Form formLangs
|
|
PasswordResetPath string
|
|
chosenPWRLang string
|
|
PasswordReset pwrLangs
|
|
EmailPath string
|
|
chosenEmailLang string
|
|
Email emailLangs
|
|
CommonPath string
|
|
Common commonLangs
|
|
SetupPath string
|
|
Setup setupLangs
|
|
chosenTelegramLang string
|
|
TelegramPath string
|
|
Telegram telegramLangs
|
|
}
|
|
|
|
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.loadLangPWR(filesystems...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = st.loadLangEmail(filesystems...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = st.loadLangTelegram(filesystems...)
|
|
return
|
|
}
|
|
|
|
// The following patch* functions fill in a language with missing values
|
|
// from a list of other sources in a preferred order.
|
|
// languages to patch from should be in decreasing priority,
|
|
// E.g: If to = fr-be, from = [fr-fr, en-us].
|
|
func (common *commonLangs) patchCommon(to *langSection, from ...string) {
|
|
if *to == nil {
|
|
*to = langSection{}
|
|
}
|
|
for n, ev := range (*common)[from[len(from)-1]].Strings {
|
|
if v, ok := (*to)[n]; !ok || v == "" {
|
|
i := 0
|
|
for i < len(from)-1 {
|
|
ev, ok = (*common)[from[i]].Strings[n]
|
|
if ok && ev != "" {
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
(*to)[n] = ev
|
|
}
|
|
}
|
|
}
|
|
|
|
func patchLang(to *langSection, from ...*langSection) {
|
|
if *to == nil {
|
|
*to = langSection{}
|
|
}
|
|
for n, ev := range *from[len(from)-1] {
|
|
if v, ok := (*to)[n]; !ok || v == "" {
|
|
i := 0
|
|
for i < len(from)-1 {
|
|
ev, ok = (*from[i])[n]
|
|
if ok && ev != "" {
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
(*to)[n] = ev
|
|
}
|
|
}
|
|
}
|
|
|
|
func patchQuantityStrings(to *map[string]quantityString, from ...*map[string]quantityString) {
|
|
if *to == nil {
|
|
*to = map[string]quantityString{}
|
|
}
|
|
for n, ev := range *from[len(from)-1] {
|
|
qs, ok := (*to)[n]
|
|
if !ok || qs.Singular == "" || qs.Plural == "" {
|
|
i := 0
|
|
subOk := false
|
|
for i < len(from)-1 {
|
|
ev, subOk = (*from[i])[n]
|
|
if subOk && ev.Singular != "" && ev.Plural != "" {
|
|
break
|
|
}
|
|
i++
|
|
}
|
|
if !ok {
|
|
(*to)[n] = ev
|
|
continue
|
|
} else if qs.Singular == "" {
|
|
qs.Singular = ev.Singular
|
|
} else if qs.Plural == "" {
|
|
qs.Plural = ev.Plural
|
|
}
|
|
(*to)[n] = qs
|
|
}
|
|
}
|
|
}
|
|
|
|
type loadLangFunc func(fsIndex int, name string) error
|
|
|
|
func (st *Storage) loadLangCommon(filesystems ...fs.FS) error {
|
|
st.lang.Common = map[string]commonLang{}
|
|
var english commonLang
|
|
loadedLangs := make([]map[string]bool, len(filesystems))
|
|
var load loadLangFunc
|
|
load = func(fsIndex int, fname string) error {
|
|
filesystem := filesystems[fsIndex]
|
|
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" {
|
|
if lang.Meta.Fallback != "" {
|
|
fallback, ok := st.lang.Common[lang.Meta.Fallback]
|
|
err = nil
|
|
if !ok {
|
|
err = load(fsIndex, lang.Meta.Fallback+".json")
|
|
fallback = st.lang.Common[lang.Meta.Fallback]
|
|
}
|
|
if err == nil {
|
|
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
|
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
|
}
|
|
}
|
|
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
|
patchLang(&lang.Strings, &english.Strings)
|
|
}
|
|
}
|
|
st.lang.Common[index] = lang
|
|
return nil
|
|
}
|
|
engFound := false
|
|
var err error
|
|
for i := range filesystems {
|
|
loadedLangs[i] = map[string]bool{}
|
|
err = load(i, "en-us.json")
|
|
if err == nil {
|
|
engFound = true
|
|
}
|
|
loadedLangs[i]["en-us.json"] = true
|
|
}
|
|
if !engFound {
|
|
return err
|
|
}
|
|
english = st.lang.Common["en-us"]
|
|
commonLoaded := false
|
|
for i := range filesystems {
|
|
files, err := fs.ReadDir(filesystems[i], st.lang.CommonPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, f := range files {
|
|
if !loadedLangs[i][f.Name()] {
|
|
err = load(i, f.Name())
|
|
if err == nil {
|
|
commonLoaded = true
|
|
loadedLangs[i][f.Name()] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !commonLoaded {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
|
|
st.lang.Admin = map[string]adminLang{}
|
|
var english adminLang
|
|
loadedLangs := make([]map[string]bool, len(filesystems))
|
|
var load loadLangFunc
|
|
load = func(fsIndex int, fname string) error {
|
|
filesystem := filesystems[fsIndex]
|
|
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(&lang.Strings, index)
|
|
if fname != "en-us.json" {
|
|
if lang.Meta.Fallback != "" {
|
|
fallback, ok := st.lang.Admin[lang.Meta.Fallback]
|
|
err = nil
|
|
if !ok {
|
|
err = load(fsIndex, lang.Meta.Fallback+".json")
|
|
fallback = st.lang.Admin[lang.Meta.Fallback]
|
|
}
|
|
if err == nil {
|
|
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
|
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
|
patchLang(&lang.Notifications, &fallback.Notifications, &english.Notifications)
|
|
patchQuantityStrings(&lang.QuantityStrings, &fallback.QuantityStrings, &english.QuantityStrings)
|
|
}
|
|
}
|
|
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
|
patchLang(&lang.Strings, &english.Strings)
|
|
patchLang(&lang.Notifications, &english.Notifications)
|
|
patchQuantityStrings(&lang.QuantityStrings, &english.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 i := range filesystems {
|
|
loadedLangs[i] = map[string]bool{}
|
|
err = load(i, "en-us.json")
|
|
if err == nil {
|
|
engFound = true
|
|
}
|
|
loadedLangs[i]["en-us.json"] = true
|
|
}
|
|
if !engFound {
|
|
return err
|
|
}
|
|
english = st.lang.Admin["en-us"]
|
|
adminLoaded := false
|
|
for i := range filesystems {
|
|
files, err := fs.ReadDir(filesystems[i], st.lang.AdminPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, f := range files {
|
|
if !loadedLangs[i][f.Name()] {
|
|
err = load(i, f.Name())
|
|
if err == nil {
|
|
adminLoaded = true
|
|
loadedLangs[i][f.Name()] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !adminLoaded {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
|
|
st.lang.Form = map[string]formLang{}
|
|
var english formLang
|
|
loadedLangs := make([]map[string]bool, len(filesystems))
|
|
var load loadLangFunc
|
|
load = func(fsIndex int, fname string) error {
|
|
filesystem := filesystems[fsIndex]
|
|
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(&lang.Strings, index)
|
|
if fname != "en-us.json" {
|
|
if lang.Meta.Fallback != "" {
|
|
fallback, ok := st.lang.Form[lang.Meta.Fallback]
|
|
err = nil
|
|
if !ok {
|
|
err = load(fsIndex, lang.Meta.Fallback+".json")
|
|
fallback = st.lang.Form[lang.Meta.Fallback]
|
|
}
|
|
if err == nil {
|
|
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
|
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
|
patchLang(&lang.Notifications, &fallback.Notifications, &english.Notifications)
|
|
patchQuantityStrings(&lang.ValidationStrings, &fallback.ValidationStrings, &english.ValidationStrings)
|
|
}
|
|
}
|
|
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
|
patchLang(&lang.Strings, &english.Strings)
|
|
patchLang(&lang.Notifications, &english.Notifications)
|
|
patchQuantityStrings(&lang.ValidationStrings, &english.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 i := range filesystems {
|
|
loadedLangs[i] = map[string]bool{}
|
|
err = load(i, "en-us.json")
|
|
if err == nil {
|
|
engFound = true
|
|
}
|
|
loadedLangs[i]["en-us.json"] = true
|
|
}
|
|
if !engFound {
|
|
return err
|
|
}
|
|
english = st.lang.Form["en-us"]
|
|
formLoaded := false
|
|
for i := range filesystems {
|
|
files, err := fs.ReadDir(filesystems[i], st.lang.FormPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, f := range files {
|
|
if !loadedLangs[i][f.Name()] {
|
|
err = load(i, f.Name())
|
|
if err == nil {
|
|
formLoaded = true
|
|
loadedLangs[i][f.Name()] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !formLoaded {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
|
|
st.lang.PasswordReset = map[string]pwrLang{}
|
|
var english pwrLang
|
|
loadedLangs := make([]map[string]bool, len(filesystems))
|
|
var load loadLangFunc
|
|
load = func(fsIndex int, fname string) error {
|
|
filesystem := filesystems[fsIndex]
|
|
index := strings.TrimSuffix(fname, filepath.Ext(fname))
|
|
lang := pwrLang{}
|
|
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.PasswordResetPath, 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(&lang.Strings, index)
|
|
if fname != "en-us.json" {
|
|
if lang.Meta.Fallback != "" {
|
|
fallback, ok := st.lang.PasswordReset[lang.Meta.Fallback]
|
|
err = nil
|
|
if !ok {
|
|
err = load(fsIndex, lang.Meta.Fallback+".json")
|
|
fallback = st.lang.PasswordReset[lang.Meta.Fallback]
|
|
}
|
|
if err == nil {
|
|
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
|
}
|
|
}
|
|
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
|
patchLang(&lang.Strings, &english.Strings)
|
|
}
|
|
}
|
|
st.lang.PasswordReset[index] = lang
|
|
return nil
|
|
}
|
|
engFound := false
|
|
var err error
|
|
for i := range filesystems {
|
|
loadedLangs[i] = map[string]bool{}
|
|
err = load(i, "en-us.json")
|
|
if err == nil {
|
|
engFound = true
|
|
}
|
|
loadedLangs[i]["en-us.json"] = true
|
|
}
|
|
if !engFound {
|
|
return err
|
|
}
|
|
english = st.lang.PasswordReset["en-us"]
|
|
formLoaded := false
|
|
for i := range filesystems {
|
|
files, err := fs.ReadDir(filesystems[i], st.lang.PasswordResetPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, f := range files {
|
|
if !loadedLangs[i][f.Name()] {
|
|
err = load(i, f.Name())
|
|
if err == nil {
|
|
formLoaded = true
|
|
loadedLangs[i][f.Name()] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !formLoaded {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
|
|
st.lang.Email = map[string]emailLang{}
|
|
var english emailLang
|
|
loadedLangs := make([]map[string]bool, len(filesystems))
|
|
var load loadLangFunc
|
|
load = func(fsIndex int, fname string) error {
|
|
filesystem := filesystems[fsIndex]
|
|
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(&lang.Strings, index)
|
|
if fname != "en-us.json" {
|
|
if lang.Meta.Fallback != "" {
|
|
fallback, ok := st.lang.Email[lang.Meta.Fallback]
|
|
err = nil
|
|
if !ok {
|
|
err = load(fsIndex, lang.Meta.Fallback+".json")
|
|
fallback = st.lang.Email[lang.Meta.Fallback]
|
|
}
|
|
if err == nil {
|
|
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
|
patchLang(&lang.UserCreated, &fallback.UserCreated, &english.UserCreated)
|
|
patchLang(&lang.InviteExpiry, &fallback.InviteExpiry, &english.InviteExpiry)
|
|
patchLang(&lang.PasswordReset, &fallback.PasswordReset, &english.PasswordReset)
|
|
patchLang(&lang.UserDeleted, &fallback.UserDeleted, &english.UserDeleted)
|
|
patchLang(&lang.UserDisabled, &fallback.UserDisabled, &english.UserDisabled)
|
|
patchLang(&lang.UserEnabled, &fallback.UserEnabled, &english.UserEnabled)
|
|
patchLang(&lang.InviteEmail, &fallback.InviteEmail, &english.InviteEmail)
|
|
patchLang(&lang.WelcomeEmail, &fallback.WelcomeEmail, &english.WelcomeEmail)
|
|
patchLang(&lang.EmailConfirmation, &fallback.EmailConfirmation, &english.EmailConfirmation)
|
|
patchLang(&lang.UserExpired, &fallback.UserExpired, &english.UserExpired)
|
|
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
|
}
|
|
}
|
|
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
|
patchLang(&lang.UserCreated, &english.UserCreated)
|
|
patchLang(&lang.InviteExpiry, &english.InviteExpiry)
|
|
patchLang(&lang.PasswordReset, &english.PasswordReset)
|
|
patchLang(&lang.UserDeleted, &english.UserDeleted)
|
|
patchLang(&lang.UserDisabled, &english.UserDisabled)
|
|
patchLang(&lang.UserEnabled, &english.UserEnabled)
|
|
patchLang(&lang.InviteEmail, &english.InviteEmail)
|
|
patchLang(&lang.WelcomeEmail, &english.WelcomeEmail)
|
|
patchLang(&lang.EmailConfirmation, &english.EmailConfirmation)
|
|
patchLang(&lang.UserExpired, &english.UserExpired)
|
|
patchLang(&lang.Strings, &english.Strings)
|
|
}
|
|
}
|
|
st.lang.Email[index] = lang
|
|
return nil
|
|
}
|
|
engFound := false
|
|
var err error
|
|
for i := range filesystems {
|
|
loadedLangs[i] = map[string]bool{}
|
|
err = load(i, "en-us.json")
|
|
if err == nil {
|
|
engFound = true
|
|
}
|
|
loadedLangs[i]["en-us.json"] = true
|
|
}
|
|
if !engFound {
|
|
return err
|
|
}
|
|
english = st.lang.Email["en-us"]
|
|
emailLoaded := false
|
|
for i := range filesystems {
|
|
files, err := fs.ReadDir(filesystems[i], st.lang.EmailPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, f := range files {
|
|
if !loadedLangs[i][f.Name()] {
|
|
err = load(i, f.Name())
|
|
if err == nil {
|
|
emailLoaded = true
|
|
loadedLangs[i][f.Name()] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !emailLoaded {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *Storage) loadLangTelegram(filesystems ...fs.FS) error {
|
|
st.lang.Telegram = map[string]telegramLang{}
|
|
var english telegramLang
|
|
loadedLangs := make([]map[string]bool, len(filesystems))
|
|
var load loadLangFunc
|
|
load = func(fsIndex int, fname string) error {
|
|
filesystem := filesystems[fsIndex]
|
|
index := strings.TrimSuffix(fname, filepath.Ext(fname))
|
|
lang := telegramLang{}
|
|
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.TelegramPath, 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(&lang.Strings, index)
|
|
if fname != "en-us.json" {
|
|
if lang.Meta.Fallback != "" {
|
|
fallback, ok := st.lang.Telegram[lang.Meta.Fallback]
|
|
err = nil
|
|
if !ok {
|
|
err = load(fsIndex, lang.Meta.Fallback+".json")
|
|
fallback = st.lang.Telegram[lang.Meta.Fallback]
|
|
}
|
|
if err == nil {
|
|
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
|
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
|
}
|
|
}
|
|
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
|
patchLang(&lang.Strings, &english.Strings)
|
|
}
|
|
}
|
|
st.lang.Telegram[index] = lang
|
|
return nil
|
|
}
|
|
engFound := false
|
|
var err error
|
|
for i := range filesystems {
|
|
loadedLangs[i] = map[string]bool{}
|
|
err = load(i, "en-us.json")
|
|
if err == nil {
|
|
engFound = true
|
|
}
|
|
loadedLangs[i]["en-us.json"] = true
|
|
}
|
|
if !engFound {
|
|
return err
|
|
}
|
|
english = st.lang.Telegram["en-us"]
|
|
telegramLoaded := false
|
|
for i := range filesystems {
|
|
files, err := fs.ReadDir(filesystems[i], st.lang.TelegramPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, f := range files {
|
|
if !loadedLangs[i][f.Name()] {
|
|
err = load(i, f.Name())
|
|
if err == nil {
|
|
telegramLoaded = true
|
|
loadedLangs[i][f.Name()] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !telegramLoaded {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Invites map[string]Invite
|
|
|
|
func (st *Storage) loadInvites() error {
|
|
st.invitesLock.Lock()
|
|
defer st.invitesLock.Unlock()
|
|
return loadJSON(st.invite_path, &st.invites)
|
|
}
|
|
|
|
func (st *Storage) storeInvites() error {
|
|
st.invitesLock.Lock()
|
|
defer st.invitesLock.Unlock()
|
|
return storeJSON(st.invite_path, st.invites)
|
|
}
|
|
|
|
func (st *Storage) loadUsers() error {
|
|
st.usersLock.Lock()
|
|
defer st.usersLock.Unlock()
|
|
if st.users == nil {
|
|
st.users = map[string]time.Time{}
|
|
}
|
|
temp := map[string]time.Time{}
|
|
err := loadJSON(st.users_path, &temp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for id, t1 := range temp {
|
|
if _, ok := st.users[id]; !ok {
|
|
st.users[id] = t1
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (st *Storage) storeUsers() error {
|
|
return storeJSON(st.users_path, st.users)
|
|
}
|
|
|
|
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) loadTelegramUsers() error {
|
|
return loadJSON(st.telegram_path, &st.telegram)
|
|
}
|
|
|
|
func (st *Storage) storeTelegramUsers() error {
|
|
return storeJSON(st.telegram_path, st.telegram)
|
|
}
|
|
|
|
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) deHyphenateStorage(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)
|
|
val, ok := old[hyphenated]
|
|
if ok {
|
|
newEmails[unHyphenated] = val
|
|
}
|
|
}
|
|
return newEmails, status, err
|
|
}
|
|
|
|
func (app *appContext) hyphenateStorage(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, "-", "")
|
|
val, ok := old[stripped]
|
|
if ok {
|
|
newEmails[unstripped] = val
|
|
}
|
|
}
|
|
return newEmails, status, err
|
|
}
|
|
|
|
func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
|
return app.hyphenateStorage(old)
|
|
}
|
|
|
|
func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
|
return app.deHyphenateStorage(old)
|
|
}
|
|
|
|
func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
|
|
asInterface := map[string]interface{}{}
|
|
for k, v := range old {
|
|
asInterface[k] = v
|
|
}
|
|
fixed, status, err := app.hyphenateStorage(asInterface)
|
|
if err != nil {
|
|
return nil, status, err
|
|
}
|
|
out := map[string]time.Time{}
|
|
for k, v := range fixed {
|
|
out[k] = v.(time.Time)
|
|
}
|
|
return out, status, err
|
|
}
|
|
|
|
func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
|
|
asInterface := map[string]interface{}{}
|
|
for k, v := range old {
|
|
asInterface[k] = v
|
|
}
|
|
fixed, status, err := app.deHyphenateStorage(asInterface)
|
|
if err != nil {
|
|
return nil, status, err
|
|
}
|
|
out := map[string]time.Time{}
|
|
for k, v := range fixed {
|
|
out[k] = v.(time.Time)
|
|
}
|
|
return out, status, err
|
|
}
|