2020-07-29 21:11:28 +00:00
package main
import (
"encoding/json"
2024-08-04 17:09:25 +00:00
"fmt"
2021-02-01 17:39:19 +00:00
"io/fs"
2020-11-02 23:20:06 +00:00
"log"
2021-01-31 23:12:50 +00:00
"os"
2021-01-11 19:17:43 +00:00
"path/filepath"
2020-09-22 23:01:07 +00:00
"strconv"
2020-11-29 18:01:10 +00:00
"strings"
2020-07-29 21:11:28 +00:00
"time"
2021-02-19 00:47:01 +00:00
2023-12-23 21:47:41 +00:00
"github.com/gin-gonic/gin"
2024-07-30 15:36:59 +00:00
"github.com/hrfee/jfa-go/jellyseerr"
2023-10-12 17:12:18 +00:00
"github.com/hrfee/jfa-go/logger"
2024-08-01 19:17:05 +00:00
lm "github.com/hrfee/jfa-go/logmessages"
2021-03-29 20:49:46 +00:00
"github.com/hrfee/mediabrowser"
2023-06-24 16:01:52 +00:00
"github.com/timshannon/badgerhold/v4"
2023-10-12 17:12:18 +00:00
"gopkg.in/ini.v1"
2020-07-29 21:11:28 +00:00
)
2023-06-20 11:19:24 +00:00
type discordStore map [ string ] DiscordUser
type telegramStore map [ string ] TelegramUser
type matrixStore map [ string ] MatrixUser
type emailStore map [ string ] EmailAddress
2023-10-19 16:59:34 +00:00
type ActivityType int
const (
2023-10-19 17:14:40 +00:00
ActivityCreation ActivityType = iota
ActivityDeletion
ActivityDisabled
ActivityEnabled
2023-10-19 20:13:00 +00:00
ActivityContactLinked
ActivityContactUnlinked
2023-10-19 17:14:40 +00:00
ActivityChangePassword
ActivityResetPassword
ActivityCreateInvite
ActivityDeleteInvite
2023-10-19 21:10:42 +00:00
ActivityUnknown
2023-10-19 16:59:34 +00:00
)
type ActivitySource int
const (
2023-10-19 17:56:35 +00:00
ActivityUser ActivitySource = iota // Source = UserID. For ActivityCreation, this would mean the referrer.
ActivityAdmin // Source = Admin's UserID, or blank if jellyfin login isn't on.
ActivityAnon // Source = Blank, or potentially browser info. For ActivityCreation, this would be via an invite
ActivityDaemon // Source = Blank, was deleted/disabled due to expiry by daemon
2023-10-19 16:59:34 +00:00
)
type Activity struct {
2023-10-19 21:10:42 +00:00
ID string ` badgerhold:"key" `
2023-10-19 16:59:34 +00:00
Type ActivityType ` badgerhold:"index" `
2023-10-19 17:56:35 +00:00
UserID string // ID of target user. For account creation, this will be the newly created account
2023-10-19 16:59:34 +00:00
SourceType ActivitySource
Source string
2023-10-20 21:16:40 +00:00
InviteCode string // Set for ActivityCreation, create/deleteInvite
2023-10-21 11:53:53 +00:00
Value string // Used for ActivityContactLinked where it's "email/discord/telegram/matrix", Create/DeleteInvite, where it's the label, and Creation/Deletion, where it's the Username.
2023-10-19 16:59:34 +00:00
Time time . Time
2023-12-23 21:47:41 +00:00
IP string
2023-10-19 16:59:34 +00:00
}
2023-06-24 18:13:05 +00:00
type UserExpiry struct {
2024-08-05 12:56:34 +00:00
JellyfinID string ` badgerhold:"key" `
Expiry time . Time
DeleteAfterPeriod bool // Whether or not to further disable the user later on
2023-06-24 18:13:05 +00:00
}
2023-10-12 17:12:18 +00:00
type DebugLogAction int
const (
NoLog DebugLogAction = iota
LogAll
LogDeletion // Logs deletion, and wiping of main field in new data, e.g. setting email.addr to "".
)
2020-07-29 21:11:28 +00:00
type Storage struct {
2023-10-12 17:12:18 +00:00
debug * logger . Logger
logActions map [ string ] DebugLogAction
2023-06-24 16:01:52 +00:00
timePattern string
db_path string
db * badgerhold . Store
2023-06-20 20:43:25 +00:00
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
2023-06-24 18:13:05 +00:00
deprecatedUserExpiries map [ string ] time . Time // Map of Jellyfin User IDs to their expiry times.
deprecatedInvites Invites
2023-06-24 20:31:22 +00:00
deprecatedProfiles map [ string ] Profile
2023-06-25 17:52:27 +00:00
deprecatedDisplayprefs , deprecatedOmbiTemplate map [ string ] interface { }
2023-06-24 18:13:05 +00:00
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.
2023-06-25 17:52:27 +00:00
deprecatedPolicy mediabrowser . Policy
deprecatedConfiguration mediabrowser . Configuration
2023-06-24 18:13:05 +00:00
deprecatedAnnouncements map [ string ] announcementTemplate
2023-06-25 18:40:54 +00:00
deprecatedCustomEmails customEmails
deprecatedUserPageContent userPageContent
lang Lang
2023-06-20 11:19:24 +00:00
}
2023-10-12 17:12:18 +00:00
type StoreType int
// Used for debug logging of storage.
const (
StoredEmails StoreType = iota
StoredDiscord
StoredTelegram
StoredMatrix
StoredInvites
StoredAnnouncements
StoredExpiries
StoredProfiles
StoredCustomContent
)
// DebugWatch logs database writes according on the advanced debugging settings in the Advanced section
func ( st * Storage ) DebugWatch ( storeType StoreType , key , mainData string ) {
if st . debug == nil {
return
}
actionKey := ""
switch storeType {
case StoredEmails :
actionKey = "emails"
case StoredDiscord :
actionKey = "discord"
case StoredTelegram :
actionKey = "telegram"
case StoredMatrix :
actionKey = "matrix"
case StoredInvites :
actionKey = "invites"
case StoredAnnouncements :
actionKey = "announcements"
case StoredExpiries :
actionKey = "expiries"
case StoredProfiles :
actionKey = "profiles"
case StoredCustomContent :
actionKey = "custom_content"
}
logAction := st . logActions [ actionKey ]
if logAction == NoLog {
return
}
actionString := "WRITE"
if mainData == "" {
actionString = "DELETE"
}
if logAction == LogAll || mainData == "" {
2023-10-12 17:21:47 +00:00
st . debug . Printf ( "%s @ %s %s[%s] = \"%s\"\n" , actionString , logger . Lshortfile ( 3 ) , actionKey , key , mainData )
2023-10-12 17:12:18 +00:00
}
}
func generateLogActions ( c * ini . File ) map [ string ] DebugLogAction {
m := map [ string ] DebugLogAction { }
for _ , v := range [ ] string { "emails" , "discord" , "telegram" , "matrix" , "invites" , "announcements" , "expirires" , "profiles" , "custom_content" } {
switch c . Section ( "advanced" ) . Key ( "debug_log_" + v ) . MustString ( "none" ) {
case "none" :
m [ v ] = NoLog
case "all" :
m [ v ] = LogAll
case "deletion" :
m [ v ] = LogDeletion
}
}
return m
}
2023-06-24 16:01:52 +00:00
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 {
2024-08-01 19:17:05 +00:00
app . err . Fatalf ( lm . FailedConnectDB , app . storage . db_path , err )
2023-06-24 16:01:52 +00:00
}
app . storage . db = db
2024-08-01 19:17:05 +00:00
app . info . Printf ( lm . ConnectDB , app . storage . db_path )
2023-06-24 16:01:52 +00:00
}
2023-06-20 11:19:24 +00:00
// GetEmails returns a copy of the store.
2023-06-24 16:01:52 +00:00
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
2023-06-20 11:19:24 +00:00
}
// GetEmailsKey returns the value stored in the store's key.
func ( st * Storage ) GetEmailsKey ( k string ) ( EmailAddress , bool ) {
2023-06-24 16:01:52 +00:00
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
2023-06-20 11:19:24 +00:00
}
// SetEmailsKey stores value v in key k.
func ( st * Storage ) SetEmailsKey ( k string , v EmailAddress ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredEmails , k , v . Addr )
2023-06-24 16:01:52 +00:00
v . JellyfinID = k
err := st . db . Upsert ( k , v )
if err != nil {
// fmt.Printf("Failed to set email: %v\n", err)
}
2023-06-20 11:19:24 +00:00
}
// DeleteEmailKey deletes value at key k.
func ( st * Storage ) DeleteEmailsKey ( k string ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredEmails , k , "" )
2023-06-24 16:01:52 +00:00
st . db . Delete ( k , EmailAddress { } )
2023-06-20 11:19:24 +00:00
}
// GetDiscord returns a copy of the store.
2023-06-24 16:01:52 +00:00
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)
2023-06-21 16:59:58 +00:00
}
2023-06-24 16:01:52 +00:00
return result
2023-06-20 11:19:24 +00:00
}
// GetDiscordKey returns the value stored in the store's key.
func ( st * Storage ) GetDiscordKey ( k string ) ( DiscordUser , bool ) {
2023-06-24 16:01:52 +00:00
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
2023-06-20 11:19:24 +00:00
}
// SetDiscordKey stores value v in key k.
func ( st * Storage ) SetDiscordKey ( k string , v DiscordUser ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredDiscord , k , v . Username )
2023-06-24 16:01:52 +00:00
v . JellyfinID = k
err := st . db . Upsert ( k , v )
if err != nil {
// fmt.Printf("Failed to set user: %v\n", err)
2023-06-21 16:59:58 +00:00
}
2023-06-20 11:19:24 +00:00
}
// DeleteDiscordKey deletes value at key k.
func ( st * Storage ) DeleteDiscordKey ( k string ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredDiscord , k , "" )
2023-06-24 16:01:52 +00:00
st . db . Delete ( k , DiscordUser { } )
2023-06-20 11:19:24 +00:00
}
// GetTelegram returns a copy of the store.
2023-06-24 16:01:52 +00:00
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)
2023-06-21 16:59:58 +00:00
}
2023-06-24 16:01:52 +00:00
return result
2023-06-20 11:19:24 +00:00
}
// GetTelegramKey returns the value stored in the store's key.
func ( st * Storage ) GetTelegramKey ( k string ) ( TelegramUser , bool ) {
2023-06-24 16:01:52 +00:00
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
2023-06-20 11:19:24 +00:00
}
// SetTelegramKey stores value v in key k.
func ( st * Storage ) SetTelegramKey ( k string , v TelegramUser ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredTelegram , k , v . Username )
2023-06-24 16:01:52 +00:00
v . JellyfinID = k
err := st . db . Upsert ( k , v )
if err != nil {
// fmt.Printf("Failed to set user: %v\n", err)
2023-06-21 16:59:58 +00:00
}
2023-06-20 11:19:24 +00:00
}
// DeleteTelegramKey deletes value at key k.
func ( st * Storage ) DeleteTelegramKey ( k string ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredTelegram , k , "" )
2023-06-24 16:01:52 +00:00
st . db . Delete ( k , TelegramUser { } )
2023-06-20 11:19:24 +00:00
}
// GetMatrix returns a copy of the store.
2023-06-24 16:01:52 +00:00
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)
2023-06-21 16:59:58 +00:00
}
2023-06-24 16:01:52 +00:00
return result
2023-06-20 11:19:24 +00:00
}
// GetMatrixKey returns the value stored in the store's key.
func ( st * Storage ) GetMatrixKey ( k string ) ( MatrixUser , bool ) {
2023-06-24 16:01:52 +00:00
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
2023-06-20 11:19:24 +00:00
}
// SetMatrixKey stores value v in key k.
func ( st * Storage ) SetMatrixKey ( k string , v MatrixUser ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredMatrix , k , v . UserID )
2023-06-24 16:01:52 +00:00
v . JellyfinID = k
err := st . db . Upsert ( k , v )
if err != nil {
// fmt.Printf("Failed to set user: %v\n", err)
2023-06-21 16:59:58 +00:00
}
2023-06-20 11:19:24 +00:00
}
// DeleteMatrixKey deletes value at key k.
func ( st * Storage ) DeleteMatrixKey ( k string ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredMatrix , k , "" )
2023-06-24 16:01:52 +00:00
st . db . Delete ( k , MatrixUser { } )
2021-05-07 00:08:12 +00:00
}
2023-06-21 19:39:16 +00:00
// GetInvites returns a copy of the store.
2023-06-24 18:13:05 +00:00
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)
2023-06-21 19:39:16 +00:00
}
2023-06-24 18:13:05 +00:00
return result
2023-06-21 19:39:16 +00:00
}
// GetInvitesKey returns the value stored in the store's key.
func ( st * Storage ) GetInvitesKey ( k string ) ( Invite , bool ) {
2023-06-24 18:13:05 +00:00
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
2023-06-21 19:39:16 +00:00
}
// SetInvitesKey stores value v in key k.
func ( st * Storage ) SetInvitesKey ( k string , v Invite ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredInvites , k , "changed" ) // Not sure what the main data from this would be
2023-06-24 18:13:05 +00:00
v . Code = k
err := st . db . Upsert ( k , v )
if err != nil {
// fmt.Printf("Failed to set invite: %v\n", err)
2023-06-21 19:39:16 +00:00
}
}
// DeleteInvitesKey deletes value at key k.
func ( st * Storage ) DeleteInvitesKey ( k string ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredInvites , k , "" )
2023-06-24 18:13:05 +00:00
st . db . Delete ( k , Invite { } )
2023-06-21 19:39:16 +00:00
}
2023-06-24 16:01:52 +00:00
// 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 ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredAnnouncements , k , v . Subject )
2023-06-24 16:01:52 +00:00
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 ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredAnnouncements , k , "" )
2023-06-24 16:01:52 +00:00
st . db . Delete ( k , announcementTemplate { } )
}
2023-06-24 18:13:05 +00:00
// 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 ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredExpiries , k , v . Expiry . String ( ) )
2023-06-24 18:13:05 +00:00
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 ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredExpiries , k , "" )
2023-06-24 18:13:05 +00:00
st . db . Delete ( k , UserExpiry { } )
}
2023-06-24 20:29:16 +00:00
// 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
}
2023-06-26 11:57:16 +00:00
if result . Policy . BlockedTags == nil {
result . Policy . BlockedTags = [ ] interface { } { }
}
2023-06-24 20:29:16 +00:00
return result , ok
}
// SetProfileKey stores value v in key k.
func ( st * Storage ) SetProfileKey ( k string , v Profile ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredProfiles , k , "changed" )
2023-06-24 20:29:16 +00:00
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 ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredProfiles , k , "" )
2023-06-24 20:29:16 +00:00
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
}
2024-08-03 20:23:59 +00:00
// MustGetProfileKey returns the profile at key k, or if missing, the default profile.
func ( st * Storage ) MustGetProfileKey ( k string ) Profile {
p , ok := st . GetProfileKey ( k )
if ! ok {
p = st . GetDefaultProfile ( )
}
return p
}
2023-06-25 18:40:54 +00:00
// 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 ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredCustomContent , k , "changed" )
2023-06-25 18:40:54 +00:00
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 ) {
2023-10-12 17:12:18 +00:00
st . DebugWatch ( StoredCustomContent , k , "" )
2023-06-25 18:40:54 +00:00
st . db . Delete ( k , CustomContent { } )
}
2023-10-19 17:14:40 +00:00
// GetActivityKey returns the value stored in the store's key.
func ( st * Storage ) GetActivityKey ( k string ) ( Activity , bool ) {
result := Activity { }
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
}
// SetActivityKey stores value v in key k.
2023-12-23 21:47:41 +00:00
// If the IP should be logged, pass "gc", and whether or not the action is of a user
func ( st * Storage ) SetActivityKey ( k string , v Activity , gc * gin . Context , user bool ) {
2023-10-19 21:10:42 +00:00
v . ID = k
2023-12-23 21:47:41 +00:00
if gc != nil && ( ( LOGIPU && user ) || ( LOGIP && ! user ) ) {
v . IP = gc . ClientIP ( )
}
2023-10-19 17:14:40 +00:00
err := st . db . Upsert ( k , v )
if err != nil {
// fmt.Printf("Failed to set custom content: %v\n", err)
}
}
// DeleteActivityKey deletes value at key k.
func ( st * Storage ) DeleteActivityKey ( k string ) {
st . db . Delete ( k , Activity { } )
}
2024-08-03 20:23:59 +00:00
type ThirdPartyService interface {
// ok implies user imported, err can be any issue that occurs during
ImportUser ( jellyfinID string , req newUserDTO , profile Profile ) ( err error , ok bool )
AddContactMethods ( jellyfinID string , req newUserDTO , discord * DiscordUser , telegram * TelegramUser ) ( err error )
Enabled ( app * appContext , profile * Profile ) bool
Name ( ) string
}
type ContactMethodLinker interface {
PIN ( req newUserDTO ) string
Name ( ) string
Required ( ) bool
UniqueRequired ( ) bool
UserVerified ( PIN string ) ( ContactMethodUser , bool )
PostVerificationTasks ( PIN string , u ContactMethodUser ) error
DeleteVerifiedToken ( PIN string )
Exists ( ContactMethodUser ) bool
}
type ContactMethodUser interface {
SetMethodID ( id any )
MethodID ( ) any
Name ( ) string
SetJellyfin ( id string )
Jellyfin ( ) string
SetAllowContactFromDTO ( req newUserDTO )
SetAllowContact ( contact bool )
AllowContact ( ) bool
Store ( st * Storage )
2021-02-19 21:38:20 +00:00
}
2021-05-17 22:42:33 +00:00
type DiscordUser struct {
2021-05-21 20:35:25 +00:00
ChannelID string
2023-06-24 16:01:52 +00:00
ID string ` badgerhold:"index" `
Username string ` badgerhold:"index" `
2021-05-21 20:35:25 +00:00
Discriminator string
Lang string
Contact bool
2023-06-28 15:05:24 +00:00
JellyfinID string ` json:"-" badgerhold:"key" `
2021-05-17 22:42:33 +00:00
}
2024-08-03 20:23:59 +00:00
type TelegramUser struct {
TelegramVerifiedToken
JellyfinID string ` badgerhold:"key" `
ChatID int64 ` badgerhold:"index" `
Username string ` badgerhold:"index" `
Lang string
Contact bool // Whether to contact through telegram or not
}
type MatrixUser struct {
RoomID string
Encrypted bool
UserID string
Lang string
Contact bool
JellyfinID string ` badgerhold:"key" `
}
2021-05-21 21:46:46 +00:00
type EmailAddress struct {
2023-06-28 15:05:24 +00:00
Addr string ` badgerhold:"index" `
Label string // User Label.
Contact bool
Admin bool // Whether or not user is jfa-go admin.
JellyfinID string ` badgerhold:"key" `
ReferralTemplateKey string
2021-05-21 21:46:46 +00:00
}
2021-02-19 21:38:20 +00:00
type customEmails struct {
2024-07-28 15:02:47 +00:00
UserCreated CustomContent ` json:"userCreated" `
InviteExpiry CustomContent ` json:"inviteExpiry" `
PasswordReset CustomContent ` json:"passwordReset" `
UserDeleted CustomContent ` json:"userDeleted" `
UserDisabled CustomContent ` json:"userDisabled" `
UserEnabled CustomContent ` json:"userEnabled" `
UserExpiryAdjusted CustomContent ` json:"userExpiryAdjusted" `
InviteEmail CustomContent ` json:"inviteEmail" `
WelcomeEmail CustomContent ` json:"welcomeEmail" `
EmailConfirmation CustomContent ` json:"emailConfirmation" `
UserExpired CustomContent ` json:"userExpired" `
2023-06-25 18:40:54 +00:00
}
// CustomContent stores customized versions of jfa-go content, including emails and user messages.
type CustomContent struct {
Name string ` json:"name" badgerhold:"key" `
2021-04-15 14:34:17 +00:00
Enabled bool ` json:"enabled,omitempty" `
Content string ` json:"content" `
Variables [ ] string ` json:"variables,omitempty" `
Conditionals [ ] string ` json:"conditionals,omitempty" `
2020-10-30 22:51:47 +00:00
}
2023-06-20 20:43:25 +00:00
type userPageContent struct {
2023-06-25 18:40:54 +00:00
Login CustomContent ` json:"login" `
Page CustomContent ` json:"page" `
2023-06-20 20:43:25 +00:00
}
2020-07-29 21:11:28 +00:00
// timePattern: %Y-%m-%dT%H:%M:%S.%f
2020-09-20 10:21:04 +00:00
type Profile struct {
2023-06-28 15:05:24 +00:00
Name string ` badgerhold:"key" `
Admin bool ` json:"admin,omitempty" badgerhold:"index" `
LibraryAccess string ` json:"libraries,omitempty" `
FromUser string ` json:"fromUser,omitempty" `
Homescreen bool ` json:"homescreen" `
Policy mediabrowser . Policy ` json:"policy,omitempty" `
Configuration mediabrowser . Configuration ` json:"configuration,omitempty" `
Displayprefs map [ string ] interface { } ` json:"displayprefs,omitempty" `
Default bool ` json:"default,omitempty" `
Ombi map [ string ] interface { } ` json:"ombi,omitempty" `
2024-07-30 15:36:59 +00:00
Jellyseerr JellyseerrTemplate ` json:"jellyseerr,omitempty" `
2023-06-28 15:05:24 +00:00
ReferralTemplateKey string
2020-09-20 10:21:04 +00:00
}
2024-07-30 15:36:59 +00:00
type JellyseerrTemplate struct {
Enabled bool ` json:"enabled,omitempty" `
User jellyseerr . UserTemplate ` json:"user,omitempty" `
Notifications jellyseerr . NotificationsTemplate ` json:"notifications,omitempty" `
}
2020-07-29 21:11:28 +00:00
type Invite struct {
2023-06-24 18:13:05 +00:00
Code string ` badgerhold:"key" `
2021-04-06 20:25:44 +00:00
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" `
2021-04-08 19:43:01 +00:00
UserMonths int ` json:"user-months,omitempty" `
2021-04-06 20:25:44 +00:00
UserDays int ` json:"user-days,omitempty" `
UserHours int ` json:"user-hours,omitempty" `
UserMinutes int ` json:"user-minutes,omitempty" `
2021-05-23 15:16:31 +00:00
SendTo string ` json:"email" `
2021-04-06 20:25:44 +00:00
// Used to be stored as formatted time, now as Unix.
2023-11-10 15:07:29 +00:00
UsedBy [ ] [ ] string ` json:"used-by" `
Notify map [ string ] map [ string ] bool ` json:"notify" `
Profile string ` json:"profile" `
Label string ` json:"label,omitempty" `
UserLabel string ` json:"user_label,omitempty" example:"Friend" ` // Label to apply to users created w/ this invite.
Captchas map [ string ] Captcha // Map of Captcha IDs to images & answers
IsReferral bool ` json:"is_referral" badgerhold:"index" `
ReferrerJellyfinID string ` json:"referrer_id" `
UseReferralExpiry bool ` json:"use_referral_expiry" `
2020-07-29 21:11:28 +00:00
}
2024-08-03 20:23:59 +00:00
func ( invite Invite ) Source ( ) ( ActivitySource , string ) {
sourceType := ActivityAnon
source := ""
if invite . ReferrerJellyfinID != "" {
sourceType = ActivityUser
source = invite . ReferrerJellyfinID
}
return sourceType , source
}
2023-09-07 21:38:23 +00:00
type Captcha struct {
Answer string
Image [ ] byte // image/png
Generated time . Time
}
2021-01-19 00:29:29 +00:00
type Lang struct {
2021-05-23 16:31:20 +00:00
AdminPath string
chosenAdminLang string
Admin adminLangs
AdminJSON map [ string ] string
2023-06-16 13:43:37 +00:00
UserPath string
chosenUserLang string
User userLangs
2021-05-23 16:31:20 +00:00
PasswordResetPath string
chosenPWRLang string
PasswordReset pwrLangs
EmailPath string
chosenEmailLang string
Email emailLangs
CommonPath string
Common commonLangs
SetupPath string
Setup setupLangs
// Telegram translations are also used for Discord bots (and likely future ones).
2021-05-07 00:08:12 +00:00
chosenTelegramLang string
TelegramPath string
Telegram telegramLangs
2021-01-19 00:29:29 +00:00
}
2020-07-29 21:11:28 +00:00
2021-02-01 17:39:19 +00:00
func ( st * Storage ) loadLang ( filesystems ... fs . FS ) ( err error ) {
err = st . loadLangCommon ( filesystems ... )
2021-01-25 21:26:54 +00:00
if err != nil {
2024-08-04 17:09:25 +00:00
err = fmt . Errorf ( "common: %v" , err )
2021-01-25 21:26:54 +00:00
return
}
2021-02-01 17:39:19 +00:00
err = st . loadLangAdmin ( filesystems ... )
2021-01-19 00:29:29 +00:00
if err != nil {
2024-08-04 17:09:25 +00:00
err = fmt . Errorf ( "admin: %v" , err )
2021-01-19 00:29:29 +00:00
return
}
2023-06-17 16:26:36 +00:00
err = st . loadLangEmail ( filesystems ... )
2021-01-19 00:29:29 +00:00
if err != nil {
2024-08-04 17:09:25 +00:00
err = fmt . Errorf ( "email: %v" , err )
2021-01-19 00:29:29 +00:00
return
}
2023-06-17 16:26:36 +00:00
err = st . loadLangUser ( filesystems ... )
2021-03-30 21:41:28 +00:00
if err != nil {
2024-08-04 17:09:25 +00:00
err = fmt . Errorf ( "user: %v" , err )
2021-03-30 21:41:28 +00:00
return
}
2023-06-17 16:26:36 +00:00
err = st . loadLangPWR ( filesystems ... )
2021-05-07 00:08:12 +00:00
if err != nil {
2024-08-04 17:09:25 +00:00
err = fmt . Errorf ( "pwr: %v" , err )
2021-05-07 00:08:12 +00:00
return
}
err = st . loadLangTelegram ( filesystems ... )
2021-01-19 00:29:29 +00:00
return
2020-07-29 21:11:28 +00:00
}
2021-04-13 17:34:13 +00:00
// 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].
2023-06-16 17:29:49 +00:00
func ( common * commonLangs ) patchCommonStrings ( to * langSection , from ... string ) {
2021-04-13 17:34:13 +00:00
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
2021-01-25 21:26:54 +00:00
}
}
}
2023-06-16 17:29:49 +00:00
func ( common * commonLangs ) patchCommonNotifications ( to * langSection , from ... string ) {
if * to == nil {
* to = langSection { }
}
for n , ev := range ( * common ) [ from [ len ( from ) - 1 ] ] . Notifications {
if v , ok := ( * to ) [ n ] ; ! ok || v == "" {
i := 0
for i < len ( from ) - 1 {
ev , ok = ( * common ) [ from [ i ] ] . Notifications [ n ]
if ok && ev != "" {
break
}
i ++
}
( * to ) [ n ] = ev
}
}
}
2023-06-18 11:27:18 +00:00
func ( common * commonLangs ) patchCommonQuantityStrings ( to * map [ string ] quantityString , from ... string ) {
if * to == nil {
* to = map [ string ] quantityString { }
}
for n , ev := range ( * common ) [ from [ len ( from ) - 1 ] ] . QuantityStrings {
if v , ok := ( * to ) [ n ] ; ! ok || ( v . Singular == "" && v . Plural == "" ) {
i := 0
for i < len ( from ) - 1 {
ev , ok = ( * common ) [ from [ i ] ] . QuantityStrings [ n ]
if ok && ev . Singular != "" && ev . Plural != "" {
break
}
i ++
}
( * to ) [ n ] = ev
}
}
}
2021-04-13 17:34:13 +00:00
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
2021-01-19 00:29:29 +00:00
}
}
2020-07-29 21:11:28 +00:00
}
2021-04-13 17:34:13 +00:00
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
2021-01-19 00:29:29 +00:00
}
}
}
2021-04-13 17:34:13 +00:00
type loadLangFunc func ( fsIndex int , name string ) error
2021-02-01 17:39:19 +00:00
func ( st * Storage ) loadLangCommon ( filesystems ... fs . FS ) error {
2021-01-25 21:26:54 +00:00
st . lang . Common = map [ string ] commonLang { }
var english commonLang
2021-04-13 17:34:13 +00:00
loadedLangs := make ( [ ] map [ string ] bool , len ( filesystems ) )
var load loadLangFunc
load = func ( fsIndex int , fname string ) error {
filesystem := filesystems [ fsIndex ]
2021-01-25 21:26:54 +00:00
index := strings . TrimSuffix ( fname , filepath . Ext ( fname ) )
lang := commonLang { }
2021-02-08 12:03:22 +00:00
f , err := fs . ReadFile ( filesystem , FSJoin ( st . lang . CommonPath , fname ) )
2021-01-25 21:26:54 +00:00
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" {
2021-04-13 17:34:13 +00:00
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 )
2023-06-18 11:27:18 +00:00
patchLang ( & lang . Notifications , & fallback . Notifications , & english . Notifications )
patchQuantityStrings ( & lang . QuantityStrings , & fallback . QuantityStrings , & english . QuantityStrings )
2021-04-13 17:34:13 +00:00
}
}
if ( lang . Meta . Fallback != "" && err != nil ) || lang . Meta . Fallback == "" {
patchLang ( & lang . Strings , & english . Strings )
2023-06-18 11:27:18 +00:00
patchLang ( & lang . Notifications , & english . Notifications )
patchQuantityStrings ( & lang . QuantityStrings , & english . QuantityStrings )
2021-04-13 17:34:13 +00:00
}
2021-01-25 21:26:54 +00:00
}
st . lang . Common [ index ] = lang
return nil
}
2021-02-01 17:39:19 +00:00
engFound := false
var err error
2021-04-13 17:34:13 +00:00
for i := range filesystems {
loadedLangs [ i ] = map [ string ] bool { }
err = load ( i , "en-us.json" )
2021-02-01 17:39:19 +00:00
if err == nil {
engFound = true
}
2021-04-13 17:34:13 +00:00
loadedLangs [ i ] [ "en-us.json" ] = true
2021-01-25 21:26:54 +00:00
}
2021-02-01 17:39:19 +00:00
if ! engFound {
2021-01-25 21:26:54 +00:00
return err
}
2021-02-01 17:39:19 +00:00
english = st . lang . Common [ "en-us" ]
commonLoaded := false
2021-04-13 17:34:13 +00:00
for i := range filesystems {
files , err := fs . ReadDir ( filesystems [ i ] , st . lang . CommonPath )
2021-02-01 17:39:19 +00:00
if err != nil {
continue
}
for _ , f := range files {
2021-04-13 17:34:13 +00:00
if ! loadedLangs [ i ] [ f . Name ( ) ] {
err = load ( i , f . Name ( ) )
2021-02-01 17:39:19 +00:00
if err == nil {
commonLoaded = true
2021-04-13 17:34:13 +00:00
loadedLangs [ i ] [ f . Name ( ) ] = true
2021-02-01 17:39:19 +00:00
}
2021-01-25 21:26:54 +00:00
}
}
}
2021-02-01 17:39:19 +00:00
if ! commonLoaded {
return err
}
2021-01-25 21:26:54 +00:00
return nil
}
2021-02-01 17:39:19 +00:00
func ( st * Storage ) loadLangAdmin ( filesystems ... fs . FS ) error {
2021-01-19 00:29:29 +00:00
st . lang . Admin = map [ string ] adminLang { }
var english adminLang
2021-04-13 17:34:13 +00:00
loadedLangs := make ( [ ] map [ string ] bool , len ( filesystems ) )
var load loadLangFunc
load = func ( fsIndex int , fname string ) error {
filesystem := filesystems [ fsIndex ]
2021-01-19 00:29:29 +00:00
index := strings . TrimSuffix ( fname , filepath . Ext ( fname ) )
lang := adminLang { }
2021-02-08 12:03:22 +00:00
f , err := fs . ReadFile ( filesystem , FSJoin ( st . lang . AdminPath , fname ) )
2021-01-12 00:11:40 +00:00
if err != nil {
2021-01-19 00:29:29 +00:00
return err
}
if substituteStrings != "" {
f = [ ] byte ( strings . ReplaceAll ( string ( f ) , "Jellyfin" , substituteStrings ) )
}
err = json . Unmarshal ( f , & lang )
if err != nil {
return err
}
2023-06-16 17:29:49 +00:00
st . lang . Common . patchCommonStrings ( & lang . Strings , index )
st . lang . Common . patchCommonNotifications ( & lang . Notifications , index )
2021-01-19 00:29:29 +00:00
if fname != "en-us.json" {
2021-04-13 17:34:13 +00:00
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 )
}
2021-01-19 00:29:29 +00:00
}
stringAdmin , err := json . Marshal ( lang )
if err != nil {
return err
}
lang . JSON = string ( stringAdmin )
st . lang . Admin [ index ] = lang
return nil
}
2021-02-01 17:39:19 +00:00
engFound := false
var err error
2021-04-13 17:34:13 +00:00
for i := range filesystems {
loadedLangs [ i ] = map [ string ] bool { }
err = load ( i , "en-us.json" )
2021-02-01 17:39:19 +00:00
if err == nil {
engFound = true
}
2021-04-13 17:34:13 +00:00
loadedLangs [ i ] [ "en-us.json" ] = true
2021-01-19 00:29:29 +00:00
}
2021-02-01 17:39:19 +00:00
if ! engFound {
2021-01-19 00:29:29 +00:00
return err
}
2021-02-01 17:39:19 +00:00
english = st . lang . Admin [ "en-us" ]
adminLoaded := false
2021-04-13 17:34:13 +00:00
for i := range filesystems {
files , err := fs . ReadDir ( filesystems [ i ] , st . lang . AdminPath )
2021-02-01 17:39:19 +00:00
if err != nil {
continue
}
for _ , f := range files {
2021-04-13 17:34:13 +00:00
if ! loadedLangs [ i ] [ f . Name ( ) ] {
err = load ( i , f . Name ( ) )
2021-02-01 17:39:19 +00:00
if err == nil {
adminLoaded = true
2021-04-13 17:34:13 +00:00
loadedLangs [ i ] [ f . Name ( ) ] = true
2021-02-01 17:39:19 +00:00
}
2021-01-12 00:11:40 +00:00
}
2021-01-19 00:29:29 +00:00
}
}
2021-02-01 17:39:19 +00:00
if ! adminLoaded {
return err
}
2021-01-19 00:29:29 +00:00
return nil
}
2021-01-12 23:15:12 +00:00
2023-06-16 13:43:37 +00:00
func ( st * Storage ) loadLangUser ( filesystems ... fs . FS ) error {
st . lang . User = map [ string ] userLang { }
var english userLang
2021-04-13 17:34:13 +00:00
loadedLangs := make ( [ ] map [ string ] bool , len ( filesystems ) )
var load loadLangFunc
load = func ( fsIndex int , fname string ) error {
filesystem := filesystems [ fsIndex ]
2021-01-19 00:29:29 +00:00
index := strings . TrimSuffix ( fname , filepath . Ext ( fname ) )
2023-06-16 13:43:37 +00:00
lang := userLang { }
f , err := fs . ReadFile ( filesystem , FSJoin ( st . lang . UserPath , fname ) )
2021-01-19 00:29:29 +00:00
if err != nil {
return err
}
if substituteStrings != "" {
f = [ ] byte ( strings . ReplaceAll ( string ( f ) , "Jellyfin" , substituteStrings ) )
2021-01-12 00:11:40 +00:00
}
2021-01-19 00:29:29 +00:00
err = json . Unmarshal ( f , & lang )
if err != nil {
return err
}
2023-06-16 17:29:49 +00:00
st . lang . Common . patchCommonStrings ( & lang . Strings , index )
st . lang . Common . patchCommonNotifications ( & lang . Notifications , index )
2023-06-18 11:27:18 +00:00
st . lang . Common . patchCommonQuantityStrings ( & lang . QuantityStrings , index )
2023-06-17 16:26:36 +00:00
// turns out, a lot of email strings are useful on the user page.
emailLang := [ ] langSection { st . lang . Email [ index ] . WelcomeEmail , st . lang . Email [ index ] . UserDisabled , st . lang . Email [ index ] . UserExpired }
for _ , v := range emailLang {
patchLang ( & lang . Strings , & v )
}
2021-01-19 00:29:29 +00:00
if fname != "en-us.json" {
2021-04-13 17:34:13 +00:00
if lang . Meta . Fallback != "" {
2023-06-16 13:43:37 +00:00
fallback , ok := st . lang . User [ lang . Meta . Fallback ]
2021-04-13 17:34:13 +00:00
err = nil
if ! ok {
err = load ( fsIndex , lang . Meta . Fallback + ".json" )
2023-06-16 13:43:37 +00:00
fallback = st . lang . User [ lang . Meta . Fallback ]
2021-04-13 17:34:13 +00:00
}
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 )
}
2021-01-19 00:29:29 +00:00
}
2021-01-25 18:01:18 +00:00
notifications , err := json . Marshal ( lang . Notifications )
if err != nil {
return err
}
2021-01-19 00:29:29 +00:00
validationStrings , err := json . Marshal ( lang . ValidationStrings )
if err != nil {
return err
}
2023-06-16 17:29:49 +00:00
userJSON , err := json . Marshal ( lang )
if err != nil {
return err
}
2021-01-25 18:01:18 +00:00
lang . notificationsJSON = string ( notifications )
2021-01-19 00:29:29 +00:00
lang . validationStringsJSON = string ( validationStrings )
2023-06-16 17:29:49 +00:00
lang . JSON = string ( userJSON )
2023-06-16 13:43:37 +00:00
st . lang . User [ index ] = lang
2021-01-19 00:29:29 +00:00
return nil
2021-01-12 00:11:40 +00:00
}
2021-02-01 17:39:19 +00:00
engFound := false
var err error
2021-04-13 17:34:13 +00:00
for i := range filesystems {
loadedLangs [ i ] = map [ string ] bool { }
err = load ( i , "en-us.json" )
2021-02-01 17:39:19 +00:00
if err == nil {
engFound = true
}
2021-04-13 17:34:13 +00:00
loadedLangs [ i ] [ "en-us.json" ] = true
2020-10-30 22:51:47 +00:00
}
2021-02-01 17:39:19 +00:00
if ! engFound {
2021-01-19 00:29:29 +00:00
return err
}
2023-06-16 13:43:37 +00:00
english = st . lang . User [ "en-us" ]
userLoaded := false
2021-04-13 17:34:13 +00:00
for i := range filesystems {
2023-06-16 13:43:37 +00:00
files , err := fs . ReadDir ( filesystems [ i ] , st . lang . UserPath )
2021-02-01 17:39:19 +00:00
if err != nil {
continue
}
for _ , f := range files {
2021-04-13 17:34:13 +00:00
if ! loadedLangs [ i ] [ f . Name ( ) ] {
err = load ( i , f . Name ( ) )
2021-02-01 17:39:19 +00:00
if err == nil {
2023-06-16 13:43:37 +00:00
userLoaded = true
2021-04-13 17:34:13 +00:00
loadedLangs [ i ] [ f . Name ( ) ] = true
2021-02-01 17:39:19 +00:00
}
2021-01-19 00:29:29 +00:00
}
}
}
2023-06-16 13:43:37 +00:00
if ! userLoaded {
2021-02-01 17:39:19 +00:00
return err
}
2021-01-19 00:29:29 +00:00
return nil
}
2021-03-30 21:41:28 +00:00
func ( st * Storage ) loadLangPWR ( filesystems ... fs . FS ) error {
st . lang . PasswordReset = map [ string ] pwrLang { }
var english pwrLang
2021-04-13 17:34:13 +00:00
loadedLangs := make ( [ ] map [ string ] bool , len ( filesystems ) )
var load loadLangFunc
load = func ( fsIndex int , fname string ) error {
filesystem := filesystems [ fsIndex ]
2021-03-30 21:41:28 +00:00
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
}
2023-06-16 17:29:49 +00:00
st . lang . Common . patchCommonStrings ( & lang . Strings , index )
2021-03-30 21:41:28 +00:00
if fname != "en-us.json" {
2021-04-13 17:34:13 +00:00
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 )
}
2021-03-30 21:41:28 +00:00
}
st . lang . PasswordReset [ index ] = lang
return nil
}
engFound := false
var err error
2021-04-13 17:34:13 +00:00
for i := range filesystems {
loadedLangs [ i ] = map [ string ] bool { }
err = load ( i , "en-us.json" )
2021-03-30 21:41:28 +00:00
if err == nil {
engFound = true
}
2021-04-13 17:34:13 +00:00
loadedLangs [ i ] [ "en-us.json" ] = true
2021-03-30 21:41:28 +00:00
}
if ! engFound {
return err
}
english = st . lang . PasswordReset [ "en-us" ]
2023-06-16 13:43:37 +00:00
userLoaded := false
2021-04-13 17:34:13 +00:00
for i := range filesystems {
files , err := fs . ReadDir ( filesystems [ i ] , st . lang . PasswordResetPath )
2021-03-30 21:41:28 +00:00
if err != nil {
continue
}
for _ , f := range files {
2021-04-13 17:34:13 +00:00
if ! loadedLangs [ i ] [ f . Name ( ) ] {
err = load ( i , f . Name ( ) )
2021-03-30 21:41:28 +00:00
if err == nil {
2023-06-16 13:43:37 +00:00
userLoaded = true
2021-04-13 17:34:13 +00:00
loadedLangs [ i ] [ f . Name ( ) ] = true
2021-03-30 21:41:28 +00:00
}
}
}
}
2023-06-16 13:43:37 +00:00
if ! userLoaded {
2021-03-30 21:41:28 +00:00
return err
}
return nil
}
2021-02-01 17:39:19 +00:00
func ( st * Storage ) loadLangEmail ( filesystems ... fs . FS ) error {
2021-01-19 00:29:29 +00:00
st . lang . Email = map [ string ] emailLang { }
var english emailLang
2021-04-13 17:34:13 +00:00
loadedLangs := make ( [ ] map [ string ] bool , len ( filesystems ) )
var load loadLangFunc
load = func ( fsIndex int , fname string ) error {
filesystem := filesystems [ fsIndex ]
2021-01-19 00:29:29 +00:00
index := strings . TrimSuffix ( fname , filepath . Ext ( fname ) )
lang := emailLang { }
2021-02-08 12:03:22 +00:00
f , err := fs . ReadFile ( filesystem , FSJoin ( st . lang . EmailPath , fname ) )
2021-01-11 19:17:43 +00:00
if err != nil {
return err
}
2021-01-19 00:29:29 +00:00
if substituteStrings != "" {
f = [ ] byte ( strings . ReplaceAll ( string ( f ) , "Jellyfin" , substituteStrings ) )
}
err = json . Unmarshal ( f , & lang )
if err != nil {
return err
}
2023-06-16 17:29:49 +00:00
st . lang . Common . patchCommonStrings ( & lang . Strings , index )
2021-01-19 00:29:29 +00:00
if fname != "en-us.json" {
2021-04-13 17:34:13 +00:00
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 )
2024-07-28 15:02:47 +00:00
patchLang ( & lang . UserExpiryAdjusted , & fallback . UserExpiryAdjusted , & english . UserExpiryAdjusted )
2021-04-13 17:34:13 +00:00
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 )
2024-07-28 15:02:47 +00:00
patchLang ( & lang . UserExpiryAdjusted , & english . UserExpiryAdjusted )
2021-04-13 17:34:13 +00:00
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 )
}
2021-01-19 00:29:29 +00:00
}
st . lang . Email [ index ] = lang
return nil
2020-10-30 22:51:47 +00:00
}
2021-02-01 17:39:19 +00:00
engFound := false
var err error
2021-04-13 17:34:13 +00:00
for i := range filesystems {
loadedLangs [ i ] = map [ string ] bool { }
err = load ( i , "en-us.json" )
2021-02-01 17:39:19 +00:00
if err == nil {
engFound = true
}
2021-04-13 17:34:13 +00:00
loadedLangs [ i ] [ "en-us.json" ] = true
2021-01-19 00:29:29 +00:00
}
2021-02-01 17:39:19 +00:00
if ! engFound {
2021-01-19 00:29:29 +00:00
return err
}
2021-02-01 17:39:19 +00:00
english = st . lang . Email [ "en-us" ]
emailLoaded := false
2021-04-13 17:34:13 +00:00
for i := range filesystems {
files , err := fs . ReadDir ( filesystems [ i ] , st . lang . EmailPath )
2021-02-01 17:39:19 +00:00
if err != nil {
continue
}
for _ , f := range files {
2021-04-13 17:34:13 +00:00
if ! loadedLangs [ i ] [ f . Name ( ) ] {
err = load ( i , f . Name ( ) )
2021-02-01 17:39:19 +00:00
if err == nil {
emailLoaded = true
2021-04-13 17:34:13 +00:00
loadedLangs [ i ] [ f . Name ( ) ] = true
2021-02-01 17:39:19 +00:00
}
2021-01-19 00:29:29 +00:00
}
2021-01-14 17:51:12 +00:00
}
}
2021-02-01 17:39:19 +00:00
if ! emailLoaded {
return err
}
2021-01-19 00:29:29 +00:00
return nil
2020-10-30 22:51:47 +00:00
}
2021-05-07 00:08:12 +00:00
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
}
2023-06-16 17:29:49 +00:00
st . lang . Common . patchCommonStrings ( & lang . Strings , index )
2021-05-07 00:08:12 +00:00
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
}
2021-01-19 00:29:29 +00:00
type Invites map [ string ] Invite
func ( st * Storage ) loadInvites ( ) error {
2023-06-24 18:13:05 +00:00
return loadJSON ( st . invite_path , & st . deprecatedInvites )
2021-01-19 00:29:29 +00:00
}
func ( st * Storage ) storeInvites ( ) error {
2023-06-24 18:13:05 +00:00
return storeJSON ( st . invite_path , st . deprecatedInvites )
2021-01-19 00:29:29 +00:00
}
2023-06-24 18:13:05 +00:00
func ( st * Storage ) loadUserExpiries ( ) error {
if st . deprecatedUserExpiries == nil {
st . deprecatedUserExpiries = map [ string ] time . Time { }
2021-04-06 13:00:32 +00:00
}
2021-04-06 12:53:07 +00:00
temp := map [ string ] time . Time { }
err := loadJSON ( st . users_path , & temp )
if err != nil {
return err
}
for id , t1 := range temp {
2023-06-24 18:13:05 +00:00
if _ , ok := st . deprecatedUserExpiries [ id ] ; ! ok {
st . deprecatedUserExpiries [ id ] = t1
2021-04-06 12:53:07 +00:00
}
}
return nil
2021-02-28 15:41:06 +00:00
}
2023-06-24 18:13:05 +00:00
func ( st * Storage ) storeUserExpiries ( ) error {
return storeJSON ( st . users_path , st . deprecatedUserExpiries )
2021-02-28 15:41:06 +00:00
}
2020-07-29 21:11:28 +00:00
func ( st * Storage ) loadEmails ( ) error {
2023-06-24 18:13:05 +00:00
return loadJSON ( st . emails_path , & st . deprecatedEmails )
2020-07-29 21:11:28 +00:00
}
func ( st * Storage ) storeEmails ( ) error {
2023-06-24 18:13:05 +00:00
return storeJSON ( st . emails_path , st . deprecatedEmails )
2020-07-29 21:11:28 +00:00
}
2021-05-07 00:08:12 +00:00
func ( st * Storage ) loadTelegramUsers ( ) error {
2023-06-24 18:13:05 +00:00
return loadJSON ( st . telegram_path , & st . deprecatedTelegram )
2021-05-07 00:08:12 +00:00
}
func ( st * Storage ) storeTelegramUsers ( ) error {
2023-06-24 18:13:05 +00:00
return storeJSON ( st . telegram_path , st . deprecatedTelegram )
2021-05-07 00:08:12 +00:00
}
2021-05-17 22:42:33 +00:00
func ( st * Storage ) loadDiscordUsers ( ) error {
2023-06-24 18:13:05 +00:00
return loadJSON ( st . discord_path , & st . deprecatedDiscord )
2021-05-17 22:42:33 +00:00
}
func ( st * Storage ) storeDiscordUsers ( ) error {
2023-06-24 18:13:05 +00:00
return storeJSON ( st . discord_path , st . deprecatedDiscord )
2021-05-17 22:42:33 +00:00
}
2021-05-29 16:43:11 +00:00
func ( st * Storage ) loadMatrixUsers ( ) error {
2023-06-24 18:13:05 +00:00
return loadJSON ( st . matrix_path , & st . deprecatedMatrix )
2021-05-29 16:43:11 +00:00
}
func ( st * Storage ) storeMatrixUsers ( ) error {
2023-06-24 18:13:05 +00:00
return storeJSON ( st . matrix_path , st . deprecatedMatrix )
2021-05-29 16:43:11 +00:00
}
2021-02-19 21:38:20 +00:00
func ( st * Storage ) loadCustomEmails ( ) error {
2023-06-25 18:40:54 +00:00
return loadJSON ( st . customEmails_path , & st . deprecatedCustomEmails )
2021-02-19 21:38:20 +00:00
}
func ( st * Storage ) storeCustomEmails ( ) error {
2023-06-25 18:40:54 +00:00
return storeJSON ( st . customEmails_path , st . deprecatedCustomEmails )
2021-02-19 21:38:20 +00:00
}
2023-06-20 20:43:25 +00:00
func ( st * Storage ) loadUserPageContent ( ) error {
2023-06-25 18:40:54 +00:00
return loadJSON ( st . userPage_path , & st . deprecatedUserPageContent )
2023-06-20 20:43:25 +00:00
}
func ( st * Storage ) storeUserPageContent ( ) error {
2023-06-25 18:40:54 +00:00
return storeJSON ( st . userPage_path , st . deprecatedUserPageContent )
2023-06-20 20:43:25 +00:00
}
2020-07-29 21:11:28 +00:00
func ( st * Storage ) loadPolicy ( ) error {
2023-06-25 17:52:27 +00:00
return loadJSON ( st . policy_path , & st . deprecatedPolicy )
2020-07-29 21:11:28 +00:00
}
func ( st * Storage ) storePolicy ( ) error {
2023-06-25 17:52:27 +00:00
return storeJSON ( st . policy_path , st . deprecatedPolicy )
2020-07-29 21:11:28 +00:00
}
func ( st * Storage ) loadConfiguration ( ) error {
2023-06-25 17:52:27 +00:00
return loadJSON ( st . configuration_path , & st . deprecatedConfiguration )
2020-07-29 21:11:28 +00:00
}
func ( st * Storage ) storeConfiguration ( ) error {
2023-06-25 17:52:27 +00:00
return storeJSON ( st . configuration_path , st . deprecatedConfiguration )
2020-07-29 21:11:28 +00:00
}
func ( st * Storage ) loadDisplayprefs ( ) error {
2023-06-25 17:52:27 +00:00
return loadJSON ( st . displayprefs_path , & st . deprecatedDisplayprefs )
2020-07-29 21:11:28 +00:00
}
func ( st * Storage ) storeDisplayprefs ( ) error {
2023-06-25 17:52:27 +00:00
return storeJSON ( st . displayprefs_path , st . deprecatedDisplayprefs )
2020-07-29 21:11:28 +00:00
}
2020-09-05 16:32:49 +00:00
func ( st * Storage ) loadOmbiTemplate ( ) error {
2023-06-25 17:52:27 +00:00
return loadJSON ( st . ombi_path , & st . deprecatedOmbiTemplate )
2020-09-05 16:32:49 +00:00
}
func ( st * Storage ) storeOmbiTemplate ( ) error {
2023-06-25 17:52:27 +00:00
return storeJSON ( st . ombi_path , st . deprecatedOmbiTemplate )
2020-09-05 16:32:49 +00:00
}
2021-07-10 15:43:27 +00:00
func ( st * Storage ) loadAnnouncements ( ) error {
2023-06-24 18:13:05 +00:00
return loadJSON ( st . announcements_path , & st . deprecatedAnnouncements )
2021-07-10 15:43:27 +00:00
}
func ( st * Storage ) storeAnnouncements ( ) error {
2023-06-24 18:13:05 +00:00
return storeJSON ( st . announcements_path , st . deprecatedAnnouncements )
2021-07-10 15:43:27 +00:00
}
2020-09-20 10:21:04 +00:00
func ( st * Storage ) loadProfiles ( ) error {
2023-06-24 20:31:22 +00:00
err := loadJSON ( st . profiles_path , & st . deprecatedProfiles )
for name , profile := range st . deprecatedProfiles {
// if profile.Default {
// st.defaultProfile = name
// }
2020-09-22 23:01:07 +00:00
change := false
2021-02-19 00:47:01 +00:00
if profile . Policy . IsAdministrator != profile . Admin {
2020-09-22 23:01:07 +00:00
change = true
}
2021-02-19 00:47:01 +00:00
profile . Admin = profile . Policy . IsAdministrator
if profile . Policy . EnabledFolders != nil {
length := len ( profile . Policy . EnabledFolders )
2020-09-22 23:01:07 +00:00
if length == 0 {
profile . LibraryAccess = "All"
} else {
profile . LibraryAccess = strconv . Itoa ( length )
}
change = true
}
if profile . FromUser == "" {
profile . FromUser = "Unknown"
change = true
}
if change {
2023-06-24 20:31:22 +00:00
st . deprecatedProfiles [ name ] = profile
2020-09-23 17:48:00 +00:00
}
2020-09-22 23:01:07 +00:00
}
2023-06-24 20:31:22 +00:00
// if st.defaultProfile == "" {
// for n := range st.deprecatedProfiles {
// st.defaultProfile = n
// }
// }
2020-09-22 23:01:07 +00:00
return err
2020-09-20 10:21:04 +00:00
}
func ( st * Storage ) storeProfiles ( ) error {
2023-06-24 20:31:22 +00:00
return storeJSON ( st . profiles_path , st . deprecatedProfiles )
2020-09-20 10:21:04 +00:00
}
func ( st * Storage ) migrateToProfile ( ) error {
st . loadPolicy ( )
st . loadConfiguration ( )
st . loadDisplayprefs ( )
st . loadProfiles ( )
2023-06-24 20:31:22 +00:00
st . deprecatedProfiles [ "Default" ] = Profile {
2023-06-25 17:52:27 +00:00
Policy : st . deprecatedPolicy ,
Configuration : st . deprecatedConfiguration ,
Displayprefs : st . deprecatedDisplayprefs ,
2020-09-20 10:21:04 +00:00
}
return st . storeProfiles ( )
}
2020-07-29 21:11:28 +00:00
func loadJSON ( path string , obj interface { } ) error {
2020-08-01 20:20:02 +00:00
var file [ ] byte
var err error
2021-01-31 23:12:50 +00:00
file , err = os . ReadFile ( path )
2020-07-29 21:11:28 +00:00
if err != nil {
2020-08-01 20:20:02 +00:00
file = [ ] byte ( "{}" )
2020-07-29 21:11:28 +00:00
}
err = json . Unmarshal ( file , & obj )
2020-11-02 23:20:06 +00:00
if err != nil {
log . Printf ( "ERROR: Failed to read \"%s\": %s" , path , err )
}
2020-07-29 21:11:28 +00:00
return err
}
func storeJSON ( path string , obj interface { } ) error {
data , err := json . Marshal ( obj )
if err != nil {
return err
}
2021-01-31 23:12:50 +00:00
err = os . WriteFile ( path , data , 0644 )
2020-11-02 23:20:06 +00:00
if err != nil {
log . Printf ( "ERROR: Failed to write to \"%s\": %s" , path , err )
}
2020-07-29 21:11:28 +00:00
return err
}