mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-01-20 10:07:23 +01:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c560ec0f9f | ||
|
|
71554e0c85 | ||
|
|
0efd7c5718 | ||
|
|
901ad7529e | ||
|
|
b64bcc9738 | ||
|
|
fddb7b7584 | ||
|
|
b91302ddf8 | ||
|
|
ea0293bd4e | ||
|
|
51f2f4cc6a | ||
|
|
2d93b3b7ee | ||
|
|
0f41d1e6cf | ||
|
|
36edd4ab0d | ||
|
|
716d6a931a | ||
|
|
72bf280e2d | ||
|
|
326c2cf70a | ||
|
|
2816c6277d | ||
|
|
99875b9176 | ||
|
|
0e21942cd6 | ||
|
|
b2b5083102 | ||
|
|
c0f316d049 | ||
|
|
2c6d08319b | ||
|
|
5d8f139356 | ||
|
|
87ef71b415 | ||
|
|
cf99ae880c | ||
|
|
8e86078394 | ||
|
|
beea903879 | ||
|
|
c5e4c5d509 | ||
|
|
fac951c733 | ||
|
|
83449f3332 | ||
|
|
2a9fc8c7a5 | ||
|
|
f8d4f79271 | ||
|
|
bc466d0c6f | ||
|
|
382a0f4c3c | ||
|
|
488c2f5df5 | ||
|
|
43effd0c32 | ||
|
|
af61549bf1 | ||
|
|
22a0d8925d | ||
|
|
59a014f681 | ||
|
|
9944cc2db9 | ||
|
|
570e3a1e54 | ||
|
|
a9bde40661 | ||
|
|
b03a185e88 | ||
|
|
e450587eea | ||
|
|
30a529baac | ||
|
|
adbb74f56b | ||
|
|
223b4df172 | ||
|
|
44dc315914 | ||
|
|
c959e2ce4d | ||
|
|
57b10dd514 | ||
|
|
9da0f89613 | ||
|
|
4104cb334e | ||
|
|
94067a1ec2 | ||
|
|
3e9da3baf7 | ||
|
|
6129305b2c | ||
|
|
7165eb1f59 | ||
|
|
a4820de423 | ||
|
|
0c09f3b05f | ||
|
|
269d67f071 | ||
|
|
bdc0c0ffa2 | ||
|
|
c00f5f4330 | ||
|
|
a2c344de83 | ||
|
|
886ae64feb | ||
|
|
90a2c1f2e7 | ||
|
|
d772e43e44 | ||
|
|
8fdab39b18 | ||
|
|
f7d2771263 | ||
|
|
e8b1cca9ca | ||
|
|
d4d7219801 | ||
|
|
3273607fc3 | ||
|
|
55e21f8be3 | ||
|
|
dafb439a7d | ||
|
|
ab94de2f95 | ||
|
|
3dc0df0ac2 | ||
|
|
d701c5f27d | ||
|
|
a8f71c83da | ||
|
|
7a3e0d60f9 | ||
|
|
2687af31ca |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ server.pem
|
||||
server.crt
|
||||
instructions-debian.txt
|
||||
cl.md
|
||||
./telegram/
|
||||
|
||||
6
Makefile
6
Makefile
@@ -31,11 +31,13 @@ DEBUG ?= off
|
||||
ifeq ($(DEBUG), on)
|
||||
LDFLAGS := -s -w $(LDFLAGS)
|
||||
SOURCEMAP := --sourcemap
|
||||
TYPECHECK := tsc -noEmit --project ts/tsconfig.json
|
||||
# jank
|
||||
COPYTS := rm -r $(DATA)/web/js/ts; cp -r ts $(DATA)/web/js
|
||||
else
|
||||
SOURCEMAP :=
|
||||
COPYTS :=
|
||||
TYPECHECK :=
|
||||
endif
|
||||
|
||||
npm:
|
||||
@@ -59,6 +61,7 @@ email:
|
||||
python3 scripts/compile_mjml.py -o $(DATA)/
|
||||
|
||||
typescript:
|
||||
$(TYPECHECK)
|
||||
$(info compiling typescript)
|
||||
-mkdir -p $(DATA)/web/js
|
||||
-$(ESBUILD) --bundle ts/admin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/admin.js --minify
|
||||
@@ -94,6 +97,8 @@ copy:
|
||||
$(info copying static data)
|
||||
-mkdir -p $(DATA)/web
|
||||
cp -r static/* $(DATA)/web/
|
||||
$(info copying systemd service)
|
||||
cp jfa-go.service $(DATA)/
|
||||
$(info copying language files)
|
||||
cp -r lang $(DATA)/
|
||||
cp LICENSE $(DATA)/
|
||||
@@ -114,7 +119,6 @@ clean:
|
||||
-rm -r $(DATA)
|
||||
-rm -r build
|
||||
-rm mail/*.html
|
||||
-rm embed.go
|
||||
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
|
||||
go clean
|
||||
|
||||
|
||||
18
README.md
18
README.md
@@ -8,8 +8,6 @@
|
||||
---
|
||||
jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) (and now [Emby](https://emby.media/)) that provides invite-based account creation as well as other features that make one's instance much easier to manage.
|
||||
|
||||
I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) in Go mainly as a learning experience, but also to slightly improve speeds and efficiency.
|
||||
|
||||
#### Features
|
||||
* 🧑 Invite based account creation: Sends invites to your friends or family, and let them choose their own username and password without relying on you.
|
||||
* Send invites via a link and/or email
|
||||
@@ -19,15 +17,16 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
|
||||
* ⌛ User expiry: Specify a validity period, and new users accounts will be disabled/deleted after it. The period can be manually extended too.
|
||||
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
|
||||
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
|
||||
* Telegram Integration: Verify users via telegram, and send Password Resets, Announcements, etc. through it.
|
||||
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
||||
* Email addresses can optionally be used instead of usernames
|
||||
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email.
|
||||
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email/telegram.
|
||||
* Notifications: Get notified when someone creates an account, or an invite expires.
|
||||
* 📣 Announcements: Bulk email your users with announcements about your server.
|
||||
* 📣 Announcements: Bulk message your users with announcements about your server.
|
||||
* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
|
||||
* Enables the usage of jfa-go by multiple people
|
||||
* 🌓 Customizable look
|
||||
* Edit emails with variables and markdown
|
||||
* 🌓 Customizations
|
||||
* Customize emails with variables and markdown
|
||||
* Specify contact and help messages to appear in emails and pages
|
||||
* Light and dark themes available
|
||||
|
||||
@@ -71,8 +70,6 @@ Otherwise, full build instructions can be found [here](https://github.com/hrfee/
|
||||
#### Usage
|
||||
Simply run `jfa-go` to start the application. A setup wizard will start on `localhost:8056` (or your own specified address). Upon completion, refresh the page.
|
||||
|
||||
Note: jfa-go does not run as a daemon by default. You'll need to figure this out yourself.
|
||||
|
||||
```
|
||||
Usage of ./jfa-go:
|
||||
-config string
|
||||
@@ -89,6 +86,11 @@ Usage of ./jfa-go:
|
||||
Enable swagger at /swagger/index.html
|
||||
```
|
||||
|
||||
#### Systemd
|
||||
jfa-go does not run as a daemon by default. Run `jfa-go systemd` to create a systemd `.service` file in your current directory, which you can copy into `~/.config/systemd/user` or somewhere else.
|
||||
|
||||
---
|
||||
|
||||
If you're switching from jellyfin-accounts, copy your existing `~/.jf-accounts` to:
|
||||
|
||||
* `XDG_CONFIG_DIR/jfa-go` (usually ~/.config/jfa-go) on \*nix systems,
|
||||
|
||||
154
args.go
Normal file
154
args.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (app *appContext) loadArgs(firstCall bool) {
|
||||
if firstCall {
|
||||
flag.Usage = helpFunc
|
||||
help := flag.Bool("help", false, "prints this message.")
|
||||
flag.BoolVar(help, "h", false, "SHORTHAND")
|
||||
|
||||
DATA = flag.String("data", app.dataPath, "alternate path to data directory.")
|
||||
flag.StringVar(DATA, "d", app.dataPath, "SHORTHAND")
|
||||
CONFIG = flag.String("config", app.configPath, "alternate path to config file.")
|
||||
flag.StringVar(CONFIG, "c", app.configPath, "SHORTHAND")
|
||||
HOST = flag.String("host", "", "alternate address to host web ui on.")
|
||||
PORT = flag.Int("port", 0, "alternate port to host web ui on.")
|
||||
flag.IntVar(PORT, "p", 0, "SHORTHAND")
|
||||
DEBUG = flag.Bool("debug", false, "Enables debug logging.")
|
||||
PPROF = flag.Bool("pprof", false, "Exposes pprof profiler on /debug/pprof.")
|
||||
SWAGGER = flag.Bool("swagger", false, "Enable swagger at /swagger/index.html")
|
||||
|
||||
flag.Parse()
|
||||
if *help {
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
if *SWAGGER {
|
||||
os.Setenv("SWAGGER", "1")
|
||||
}
|
||||
if *DEBUG {
|
||||
os.Setenv("DEBUG", "1")
|
||||
}
|
||||
if *PPROF {
|
||||
os.Setenv("PPROF", "1")
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("SWAGGER") == "1" {
|
||||
*SWAGGER = true
|
||||
}
|
||||
if os.Getenv("DEBUG") == "1" {
|
||||
*DEBUG = true
|
||||
}
|
||||
if os.Getenv("PPROF") == "1" {
|
||||
*PPROF = true
|
||||
}
|
||||
// attempt to apply command line flags correctly
|
||||
if app.configPath == *CONFIG && app.dataPath != *DATA {
|
||||
app.dataPath = *DATA
|
||||
app.configPath = filepath.Join(app.dataPath, "config.ini")
|
||||
} else if app.configPath != *CONFIG && app.dataPath == *DATA {
|
||||
app.configPath = *CONFIG
|
||||
} else {
|
||||
app.configPath = *CONFIG
|
||||
app.dataPath = *DATA
|
||||
}
|
||||
|
||||
// Previously used for self-restarts but leaving them here as they might be useful.
|
||||
if v := os.Getenv("JFA_CONFIGPATH"); v != "" {
|
||||
app.configPath = v
|
||||
}
|
||||
if v := os.Getenv("JFA_DATAPATH"); v != "" {
|
||||
app.dataPath = v
|
||||
}
|
||||
|
||||
os.Setenv("JFA_CONFIGPATH", app.configPath)
|
||||
os.Setenv("JFA_DATAPATH", app.dataPath)
|
||||
}
|
||||
|
||||
/* Adds start/stop/systemd to help message, and
|
||||
also gets rid of usage for shorthand flags, and merge them with the full-length one.
|
||||
implementation is 🤢, will clean this up eventually.
|
||||
-h SHORTHAND
|
||||
-help
|
||||
prints this message.
|
||||
becomes:
|
||||
-help, -h
|
||||
prints this message.
|
||||
*/
|
||||
func helpFunc() {
|
||||
fmt.Fprint(os.Stderr, `Usage of jfa-go:
|
||||
start
|
||||
start jfa-go as a daemon and run in the background.
|
||||
stop
|
||||
stop a daemonized instance of jfa-go.
|
||||
systemd
|
||||
generate a systemd .service file.
|
||||
`)
|
||||
shortHands := []string{"-help", "-data", "-config", "-port"}
|
||||
var b bytes.Buffer
|
||||
// Write defaults into buffer then remove any shorthands
|
||||
flag.CommandLine.SetOutput(&b)
|
||||
flag.PrintDefaults()
|
||||
flag.CommandLine.SetOutput(os.Stderr)
|
||||
scanner := bufio.NewScanner(&b)
|
||||
out := ""
|
||||
line := scanner.Text()
|
||||
eof := !scanner.Scan()
|
||||
lastLine := false
|
||||
for !eof || lastLine {
|
||||
nextline := scanner.Text()
|
||||
start := 0
|
||||
if len(nextline) != 0 {
|
||||
for nextline[start] == ' ' && start < len(nextline) {
|
||||
start++
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "SHORTHAND") || (len(nextline) != 0 && strings.Contains(nextline, "SHORTHAND") && nextline[start] != '-') {
|
||||
line = nextline
|
||||
if lastLine {
|
||||
break
|
||||
}
|
||||
eof := !scanner.Scan()
|
||||
if eof {
|
||||
lastLine = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
// if !strings.Contains(line, "SHORTHAND") && !(strings.Contains(nextline, "SHORTHAND") && !strings.Contains(nextline, "-")) {
|
||||
match := false
|
||||
for i, c := range line {
|
||||
if c != '-' {
|
||||
continue
|
||||
}
|
||||
for _, s := range shortHands {
|
||||
if i+len(s) <= len(line) && line[i:i+len(s)] == s {
|
||||
out += line[:i+len(s)] + ", " + s[:2] + line[i+len(s):] + "\n"
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
out += line + "\n"
|
||||
}
|
||||
line = nextline
|
||||
if lastLine {
|
||||
break
|
||||
}
|
||||
eof := !scanner.Scan()
|
||||
if eof {
|
||||
lastLine = true
|
||||
}
|
||||
}
|
||||
fmt.Fprint(os.Stderr, out)
|
||||
}
|
||||
47
config.go
47
config.go
@@ -12,6 +12,8 @@ import (
|
||||
)
|
||||
|
||||
var emailEnabled = false
|
||||
var messagesEnabled = false
|
||||
var telegramEnabled = false
|
||||
|
||||
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
||||
val := app.config.Section(sect).Key(key).MustString("")
|
||||
@@ -40,7 +42,7 @@ func (app *appContext) loadConfig() error {
|
||||
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users"} {
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users"} {
|
||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
||||
}
|
||||
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
|
||||
@@ -64,6 +66,12 @@ func (app *appContext) loadConfig() error {
|
||||
app.MustSetValue("deletion", "email_html", "jfa-go:"+"deleted.html")
|
||||
app.MustSetValue("deletion", "email_text", "jfa-go:"+"deleted.txt")
|
||||
|
||||
// Deletion template is good enough for these as well.
|
||||
app.MustSetValue("disable_enable", "disabled_html", "jfa-go:"+"deleted.html")
|
||||
app.MustSetValue("disable_enable", "disabled_text", "jfa-go:"+"deleted.txt")
|
||||
app.MustSetValue("disable_enable", "enabled_html", "jfa-go:"+"deleted.html")
|
||||
app.MustSetValue("disable_enable", "enabled_text", "jfa-go:"+"deleted.txt")
|
||||
|
||||
app.MustSetValue("welcome_email", "email_html", "jfa-go:"+"welcome.html")
|
||||
app.MustSetValue("welcome_email", "email_text", "jfa-go:"+"welcome.txt")
|
||||
|
||||
@@ -77,12 +85,19 @@ func (app *appContext) loadConfig() error {
|
||||
app.config.Section("jellyfin").Key("version").SetValue(version)
|
||||
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
|
||||
|
||||
if app.config.Section("email").Key("method").MustString("") == "" {
|
||||
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
|
||||
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
|
||||
if !messagesEnabled {
|
||||
emailEnabled = false
|
||||
telegramEnabled = false
|
||||
} else if app.config.Section("email").Key("method").MustString("") == "" {
|
||||
emailEnabled = false
|
||||
} else {
|
||||
emailEnabled = true
|
||||
}
|
||||
if !emailEnabled && !telegramEnabled {
|
||||
messagesEnabled = false
|
||||
}
|
||||
|
||||
app.MustSetValue("updates", "enabled", "true")
|
||||
releaseChannel := app.config.Section("updates").Key("channel").String()
|
||||
@@ -122,8 +137,34 @@ func (app *appContext) loadConfig() error {
|
||||
app.storage.lang.chosenAdminLang = app.config.Section("ui").Key("language-admin").MustString("en-us")
|
||||
app.storage.lang.chosenEmailLang = app.config.Section("email").Key("language").MustString("en-us")
|
||||
app.storage.lang.chosenPWRLang = app.config.Section("password_resets").Key("language").MustString("en-us")
|
||||
app.storage.lang.chosenTelegramLang = app.config.Section("telegram").Key("language").MustString("en-us")
|
||||
|
||||
app.email = NewEmailer(app)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *appContext) migrateEmailConfig() {
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
fmt.Println(warning("Part of your email configuration will be migrated to the new \"messages\" section.\nA backup will be made."))
|
||||
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to backup config: %v", err)
|
||||
return
|
||||
}
|
||||
for _, setting := range []string{"use_24h", "date_format", "message"} {
|
||||
if val := app.config.Section("email").Key(setting).Value(); val != "" {
|
||||
tempConfig.Section("email").Key(setting).SetValue("")
|
||||
tempConfig.Section("messages").Key(setting).SetValue(val)
|
||||
}
|
||||
}
|
||||
if app.config.Section("messages").Key("enabled").MustBool(false) || app.config.Section("telegram").Key("enabled").MustBool(false) {
|
||||
tempConfig.Section("messages").Key("enabled").SetValue("true")
|
||||
}
|
||||
err = tempConfig.SaveTo(app.configPath)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to save config: %v", err)
|
||||
return
|
||||
}
|
||||
app.loadConfig()
|
||||
}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
### fixconfig
|
||||
|
||||
Python's `json` library retains the order of data in a JSON file, which meant settings sent to the web page would be in the right order. Go's `encoding/json` and maps do not retain order, so this script opens the json file, and for each section, adds an "order" list which tells the web page in which order to display settings.
|
||||
|
||||
Python's `json` library retains the order of data in a JSON file, which meant settings sent to the web page would be in the right order. Go's `encoding/json` and maps do not retain order, so `enumerate/enumerate_config.py` opens the json file, and for each section, adds an "order" array which tells the web page in which order to display settings.
|
||||
Specify the input and output files with `-i` and `-o` respectively.
|
||||
|
||||
### jsontostruct
|
||||
|
||||
Generates a go struct from `config-base.json`. I wrote this because i was annoyed with the `ini` library, but i've since realised mapping the ini values onto it is painful.
|
||||
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default Account Form Language. See issue #12 on Github if you'd like to translate."
|
||||
"description": "Default Account Form Language. Visit weblate.hrfee.dev if you'd like to translate."
|
||||
},
|
||||
"language-admin": {
|
||||
"name": "Default Admin Language",
|
||||
@@ -135,7 +135,7 @@
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default Admin page Language. Settings has not been translated. Submit a PR on github if you'd like to translate."
|
||||
"description": "Default Admin page Language. Settings has not been translated. Visit weblate.hrfee.dev if you'd like to translate."
|
||||
},
|
||||
"theme": {
|
||||
"name": "Default Look",
|
||||
@@ -345,33 +345,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"messages": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Email",
|
||||
"description": "General email settings."
|
||||
"name": "Messages/Notifications",
|
||||
"description": "General settings for emails/messages."
|
||||
},
|
||||
"settings": {
|
||||
"language": {
|
||||
"name": "Email Language",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "select",
|
||||
"options": [
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default email language. Submit a PR on github if you'd like to translate."
|
||||
},
|
||||
"no_username": {
|
||||
"name": "Use email addresses as username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": true,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use email address from invite form as username on Jellyfin."
|
||||
"value": true,
|
||||
"description": "Enable the sending of emails/messages such as password resets, announcements, etc."
|
||||
},
|
||||
"use_24h": {
|
||||
"name": "Use 24h time",
|
||||
@@ -399,6 +386,37 @@
|
||||
"type": "text",
|
||||
"value": "Need help? contact me.",
|
||||
"description": "Message displayed at bottom of emails."
|
||||
}
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Email",
|
||||
"description": "General email settings.",
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"language": {
|
||||
"name": "Email Language",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "select",
|
||||
"options": [
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default email language. Submit a PR on github if you'd like to translate."
|
||||
},
|
||||
"no_username": {
|
||||
"name": "Use email addresses as username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "method",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Use email address from invite form as username on Jellyfin."
|
||||
},
|
||||
"method": {
|
||||
"name": "Email method",
|
||||
@@ -443,12 +461,143 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mailgun": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Mailgun (Email)",
|
||||
"description": "Mailgun API connection settings",
|
||||
"depends_true": "email|method"
|
||||
},
|
||||
"settings": {
|
||||
"api_url": {
|
||||
"name": "API URL",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "https://api.mailgun.net..."
|
||||
},
|
||||
"api_key": {
|
||||
"name": "API Key",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "your api key"
|
||||
}
|
||||
}
|
||||
},
|
||||
"smtp": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "SMTP (Email)",
|
||||
"description": "SMTP Server connection settings.",
|
||||
"depends_true": "email|method"
|
||||
},
|
||||
"settings": {
|
||||
"username": {
|
||||
"name": "Username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Username for SMTP. Leave blank to user send from address as username."
|
||||
},
|
||||
"encryption": {
|
||||
"name": "Encryption Method",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "select",
|
||||
"options": [
|
||||
["ssl_tls", "SSL/TLS"],
|
||||
["starttls", "STARTTLS"]
|
||||
],
|
||||
"value": "starttls",
|
||||
"description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls."
|
||||
},
|
||||
"server": {
|
||||
"name": "Server address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "smtp.jellyf.in",
|
||||
"description": "SMTP Server address."
|
||||
},
|
||||
"port": {
|
||||
"name": "Port",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "number",
|
||||
"value": 465
|
||||
},
|
||||
"password": {
|
||||
"name": "Password",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "password",
|
||||
"value": "smtp password"
|
||||
},
|
||||
"ssl_cert": {
|
||||
"name": "Path to custom SSL certificate",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Use if your SMTP server's SSL Certificate is not trusted by the system."
|
||||
}
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Telegram",
|
||||
"description": "Settings for Telegram signup/notifications"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enable signup verification through Telegram and the sending of notifications through it.\nSee the jfa-go wiki for setting up a bot."
|
||||
},
|
||||
"required": {
|
||||
"name": "Require on sign-up",
|
||||
"required": false,
|
||||
"required_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Require telegram connection on sign-up."
|
||||
},
|
||||
"token": {
|
||||
"name": "API Token",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Telegram Bot API Token."
|
||||
},
|
||||
"language": {
|
||||
"name": "Language",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "select",
|
||||
"options": [
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default telegram message language. Visit weblate if you'd like to translate."
|
||||
}
|
||||
}
|
||||
},
|
||||
"password_resets": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Password Resets",
|
||||
"description": "Settings for the password reset handler.",
|
||||
"depends_true": "email|method"
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
@@ -469,13 +618,13 @@
|
||||
"description": "Path to the folder Jellyfin puts password-reset files."
|
||||
},
|
||||
"link_reset": {
|
||||
"name": "Use reset link instead of PIN",
|
||||
"name": "Use reset link instead of PIN (Required for Ombi)",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Send users a link to reset their password instead of a PIN."
|
||||
"description": "Send users a link to reset their password instead of a PIN. Must be enabled to reset Ombi password at the same time as the Jellyfin password."
|
||||
},
|
||||
"language": {
|
||||
"name": "Default reset link language",
|
||||
@@ -580,7 +729,7 @@
|
||||
"meta": {
|
||||
"name": "Notifications",
|
||||
"description": "Notification related settings.",
|
||||
"depends_true": "email|method"
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
@@ -633,96 +782,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mailgun": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Mailgun (Email)",
|
||||
"description": "Mailgun API connection settings",
|
||||
"depends_true": "email|method"
|
||||
},
|
||||
"settings": {
|
||||
"api_url": {
|
||||
"name": "API URL",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "https://api.mailgun.net..."
|
||||
},
|
||||
"api_key": {
|
||||
"name": "API Key",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "your api key"
|
||||
}
|
||||
}
|
||||
},
|
||||
"smtp": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "SMTP (Email)",
|
||||
"description": "SMTP Server connection settings.",
|
||||
"depends_true": "email|method"
|
||||
},
|
||||
"settings": {
|
||||
"username": {
|
||||
"name": "Username",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Username for SMTP. Leave blank to user send from address as username."
|
||||
},
|
||||
"encryption": {
|
||||
"name": "Encryption Method",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "select",
|
||||
"options": [
|
||||
["ssl_tls", "SSL/TLS"],
|
||||
["starttls", "STARTTLS"]
|
||||
],
|
||||
"value": "starttls",
|
||||
"description": "Your email provider should provide different ports for each encryption method. Generally 465 for ssl_tls, 587 for starttls."
|
||||
},
|
||||
"server": {
|
||||
"name": "Server address",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "smtp.jellyf.in",
|
||||
"description": "SMTP Server address."
|
||||
},
|
||||
"port": {
|
||||
"name": "Port",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "number",
|
||||
"value": 465
|
||||
},
|
||||
"password": {
|
||||
"name": "Password",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "password",
|
||||
"value": "smtp password"
|
||||
},
|
||||
"ssl_cert": {
|
||||
"name": "Path to custom SSL certificate",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Use if your SMTP server's SSL Certificate is not trusted by the system."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ombi": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Ombi Integration",
|
||||
"description": "Connect to Ombi to automatically create both Ombi and Jellyfin accounts for new users. You'll need to create a user template for this to work. Once enabled, refresh to see an option in settings for this."
|
||||
"description": "Connect to Ombi to automatically create both Ombi and Jellyfin accounts for new users. You'll need to create a user template for this to work. Once enabled, refresh to see an option in settings for this. To handle password resets for Ombi & Jellyfin, enable \"Use reset link instead of PIN\"."
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
@@ -756,9 +820,9 @@
|
||||
"welcome_email": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Welcome Emails",
|
||||
"description": "Optionally send a welcome email to new users with the Jellyfin URL and their username.",
|
||||
"depends_true": "email|method"
|
||||
"name": "Welcome Message",
|
||||
"description": "Optionally send a welcome message to new users with the Jellyfin URL and their username.",
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
@@ -865,14 +929,14 @@
|
||||
"requires_restart": false,
|
||||
"type": "bool",
|
||||
"value": true,
|
||||
"depends_true": "email|method",
|
||||
"depends_true": "messages|enabled",
|
||||
"description": "Send an email when a user's account expires."
|
||||
},
|
||||
"subject": {
|
||||
"name": "Email subject",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "email|method",
|
||||
"depends_true": "messages|enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Subject of user expiry emails."
|
||||
@@ -882,7 +946,7 @@
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"depends_true": "email|method",
|
||||
"depends_true": "messages|enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email html"
|
||||
@@ -892,7 +956,69 @@
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"depends_true": "email|method",
|
||||
"depends_true": "messages|enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email in plain text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable_enable": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Account Disabling/Enabling",
|
||||
"description": "Subject/email files for account disabling/enabling emails.",
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"subject_disabled": {
|
||||
"name": "Email subject (Disabled)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Subject of account disabling emails."
|
||||
},
|
||||
"subject_enabled": {
|
||||
"name": "Email subject (Enabled)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Subject of account enabling emails."
|
||||
},
|
||||
"disabled_html": {
|
||||
"name": "Custom disabling email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email html"
|
||||
},
|
||||
"disabled_text": {
|
||||
"name": "Custom disabling email (plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email in plain text"
|
||||
},
|
||||
"enabled_html": {
|
||||
"name": "Custom enabling email (HTML)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email html"
|
||||
},
|
||||
"enabled_text": {
|
||||
"name": "Custom enabling email (plaintext)",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"advanced": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Path to custom email in plain text"
|
||||
@@ -904,7 +1030,7 @@
|
||||
"meta": {
|
||||
"name": "Account Deletion",
|
||||
"description": "Subject/email files for account deletion emails.",
|
||||
"depends_true": "email|method"
|
||||
"depends_true": "messages|enabled"
|
||||
},
|
||||
"settings": {
|
||||
"subject": {
|
||||
@@ -1006,6 +1132,14 @@
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "JSON file generated by program in settings, different from email_html/email_text. See wiki for more info."
|
||||
},
|
||||
"telegram_users": {
|
||||
"name": "Telegram users",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Stores telegram user IDs and language preferences."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,541 +0,0 @@
|
||||
package main
|
||||
|
||||
type Metadata struct{
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type Config struct{
|
||||
Order []string `json:"order"`
|
||||
Jellyfin struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Username struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"username"`
|
||||
} `json:"username" cfg:"username"`
|
||||
Password struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"password"`
|
||||
} `json:"password" cfg:"password"`
|
||||
Server struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"server"`
|
||||
} `json:"server" cfg:"server"`
|
||||
PublicServer struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"public_server"`
|
||||
} `json:"public_server" cfg:"public_server"`
|
||||
Client struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"client"`
|
||||
} `json:"client" cfg:"client"`
|
||||
Version struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"version"`
|
||||
} `json:"version" cfg:"version"`
|
||||
Device struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"device"`
|
||||
} `json:"device" cfg:"device"`
|
||||
DeviceId struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"device_id"`
|
||||
} `json:"device_id" cfg:"device_id"`
|
||||
} `json:"jellyfin"`
|
||||
Ui struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Theme struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Options []string `json:"options"`
|
||||
Value string `json:"value" cfg:"theme"`
|
||||
} `json:"theme" cfg:"theme"`
|
||||
Host struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"host"`
|
||||
} `json:"host" cfg:"host"`
|
||||
Port struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value int `json:"value" cfg:"port"`
|
||||
} `json:"port" cfg:"port"`
|
||||
JellyfinLogin struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"jellyfin_login"`
|
||||
} `json:"jellyfin_login" cfg:"jellyfin_login"`
|
||||
AdminOnly struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"admin_only"`
|
||||
} `json:"admin_only" cfg:"admin_only"`
|
||||
Username struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"username"`
|
||||
} `json:"username" cfg:"username"`
|
||||
Password struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"password"`
|
||||
} `json:"password" cfg:"password"`
|
||||
Email struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email"`
|
||||
} `json:"email" cfg:"email"`
|
||||
Debug struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"debug"`
|
||||
} `json:"debug" cfg:"debug"`
|
||||
ContactMessage struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"contact_message"`
|
||||
} `json:"contact_message" cfg:"contact_message"`
|
||||
HelpMessage struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"help_message"`
|
||||
} `json:"help_message" cfg:"help_message"`
|
||||
SuccessMessage struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"success_message"`
|
||||
} `json:"success_message" cfg:"success_message"`
|
||||
Bs5 struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"bs5"`
|
||||
} `json:"bs5" cfg:"bs5"`
|
||||
} `json:"ui"`
|
||||
PasswordValidation struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
MinLength struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"min_length"`
|
||||
} `json:"min_length" cfg:"min_length"`
|
||||
Upper struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"upper"`
|
||||
} `json:"upper" cfg:"upper"`
|
||||
Lower struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"lower"`
|
||||
} `json:"lower" cfg:"lower"`
|
||||
Number struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"number"`
|
||||
} `json:"number" cfg:"number"`
|
||||
Special struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"special"`
|
||||
} `json:"special" cfg:"special"`
|
||||
} `json:"password_validation"`
|
||||
Email struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
NoUsername struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"no_username"`
|
||||
} `json:"no_username" cfg:"no_username"`
|
||||
Use24H struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"use_24h"`
|
||||
} `json:"use_24h" cfg:"use_24h"`
|
||||
DateFormat struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"date_format"`
|
||||
} `json:"date_format" cfg:"date_format"`
|
||||
Message struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"message"`
|
||||
} `json:"message" cfg:"message"`
|
||||
Method struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Options []string `json:"options"`
|
||||
Value string `json:"value" cfg:"method"`
|
||||
} `json:"method" cfg:"method"`
|
||||
Address struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"address"`
|
||||
} `json:"address" cfg:"address"`
|
||||
From struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"from"`
|
||||
} `json:"from" cfg:"from"`
|
||||
} `json:"email"`
|
||||
PasswordResets struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
WatchDirectory struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"watch_directory"`
|
||||
} `json:"watch_directory" cfg:"watch_directory"`
|
||||
EmailHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_html"`
|
||||
} `json:"email_html" cfg:"email_html"`
|
||||
EmailText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_text"`
|
||||
} `json:"email_text" cfg:"email_text"`
|
||||
Subject struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"subject"`
|
||||
} `json:"subject" cfg:"subject"`
|
||||
} `json:"password_resets"`
|
||||
InviteEmails struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
EmailHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_html"`
|
||||
} `json:"email_html" cfg:"email_html"`
|
||||
EmailText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_text"`
|
||||
} `json:"email_text" cfg:"email_text"`
|
||||
Subject struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"subject"`
|
||||
} `json:"subject" cfg:"subject"`
|
||||
UrlBase struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"url_base"`
|
||||
} `json:"url_base" cfg:"url_base"`
|
||||
} `json:"invite_emails"`
|
||||
Notifications struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
ExpiryHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"expiry_html"`
|
||||
} `json:"expiry_html" cfg:"expiry_html"`
|
||||
ExpiryText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"expiry_text"`
|
||||
} `json:"expiry_text" cfg:"expiry_text"`
|
||||
CreatedHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"created_html"`
|
||||
} `json:"created_html" cfg:"created_html"`
|
||||
CreatedText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"created_text"`
|
||||
} `json:"created_text" cfg:"created_text"`
|
||||
} `json:"notifications"`
|
||||
Mailgun struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
ApiUrl struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"api_url"`
|
||||
} `json:"api_url" cfg:"api_url"`
|
||||
ApiKey struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"api_key"`
|
||||
} `json:"api_key" cfg:"api_key"`
|
||||
} `json:"mailgun"`
|
||||
Smtp struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Encryption struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Options []string `json:"options"`
|
||||
Value string `json:"value" cfg:"encryption"`
|
||||
} `json:"encryption" cfg:"encryption"`
|
||||
Server struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"server"`
|
||||
} `json:"server" cfg:"server"`
|
||||
Port struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value int `json:"value" cfg:"port"`
|
||||
} `json:"port" cfg:"port"`
|
||||
Password struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"password"`
|
||||
} `json:"password" cfg:"password"`
|
||||
} `json:"smtp"`
|
||||
Files struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Invites struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"invites"`
|
||||
} `json:"invites" cfg:"invites"`
|
||||
Emails struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"emails"`
|
||||
} `json:"emails" cfg:"emails"`
|
||||
UserTemplate struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"user_template"`
|
||||
} `json:"user_template" cfg:"user_template"`
|
||||
UserConfiguration struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"user_configuration"`
|
||||
} `json:"user_configuration" cfg:"user_configuration"`
|
||||
UserDisplayprefs struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"user_displayprefs"`
|
||||
} `json:"user_displayprefs" cfg:"user_displayprefs"`
|
||||
CustomCss struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"custom_css"`
|
||||
} `json:"custom_css" cfg:"custom_css"`
|
||||
} `json:"files"`
|
||||
}
|
||||
20
css/base.css
20
css/base.css
@@ -39,6 +39,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chip.btn:hover:not([disabled]):not(.textarea),
|
||||
.chip.btn:focus:not([disabled]):not(.textarea) {
|
||||
filter: brightness(var(--button-filter-brightness,95%));
|
||||
}
|
||||
|
||||
.banner {
|
||||
margin: calc(-1 * var(--spacing-4,1rem));
|
||||
}
|
||||
@@ -121,6 +126,10 @@ div.card:contains(section.banner.footer) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ac {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -162,6 +171,12 @@ div.card:contains(section.banner.footer) {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
p.sm,
|
||||
span.sm {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.col.sm {
|
||||
margin: .25rem;
|
||||
}
|
||||
@@ -459,6 +474,11 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
|
||||
color: var(--color-urge-200);
|
||||
}
|
||||
|
||||
.link-center {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.search {
|
||||
max-width: 15rem;
|
||||
min-width: 10rem;
|
||||
|
||||
406
email.go
406
email.go
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -25,8 +26,15 @@ import (
|
||||
)
|
||||
|
||||
// implements email sending, right now via smtp or mailgun.
|
||||
type emailClient interface {
|
||||
send(fromName, fromAddr string, email *Email, address ...string) error
|
||||
type EmailClient interface {
|
||||
Send(fromName, fromAddr string, message *Message, address ...string) error
|
||||
}
|
||||
|
||||
type DummyClient struct{}
|
||||
|
||||
func (dc *DummyClient) Send(fromName, fromAddr string, email *Message, address ...string) error {
|
||||
fmt.Printf("FROM: %s <%s>\nTO: %s\nTEXT: %s\n", fromName, fromAddr, strings.Join(address, ", "), email.Text)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mailgun client implements emailClient.
|
||||
@@ -34,7 +42,7 @@ type Mailgun struct {
|
||||
client *mailgun.MailgunImpl
|
||||
}
|
||||
|
||||
func (mg *Mailgun) send(fromName, fromAddr string, email *Email, address ...string) error {
|
||||
func (mg *Mailgun) Send(fromName, fromAddr string, email *Message, address ...string) error {
|
||||
message := mg.client.NewMessage(
|
||||
fmt.Sprintf("%s <%s>", fromName, fromAddr),
|
||||
email.Subject,
|
||||
@@ -60,7 +68,7 @@ type SMTP struct {
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
func (sm *SMTP) send(fromName, fromAddr string, email *Email, address ...string) error {
|
||||
func (sm *SMTP) Send(fromName, fromAddr string, email *Message, address ...string) error {
|
||||
server := fmt.Sprintf("%s:%d", sm.server, sm.port)
|
||||
from := fmt.Sprintf("%s <%s>", fromName, fromAddr)
|
||||
var wg sync.WaitGroup
|
||||
@@ -86,18 +94,19 @@ func (sm *SMTP) send(fromName, fromAddr string, email *Email, address ...string)
|
||||
return err
|
||||
}
|
||||
|
||||
// Emailer contains the email sender, email content, and methods to construct message content.
|
||||
// Emailer contains the email sender, translations, and methods to construct messages.
|
||||
type Emailer struct {
|
||||
fromAddr, fromName string
|
||||
lang emailLang
|
||||
sender emailClient
|
||||
sender EmailClient
|
||||
}
|
||||
|
||||
// Email stores content.
|
||||
type Email struct {
|
||||
Subject string `json:"subject"`
|
||||
HTML string `json:"html"`
|
||||
Text string `json:"text"`
|
||||
// Message stores content.
|
||||
type Message struct {
|
||||
Subject string `json:"subject"`
|
||||
HTML string `json:"html"`
|
||||
Text string `json:"text"`
|
||||
Markdown string `json:"markdown"`
|
||||
}
|
||||
|
||||
func (emailer *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern, timePattern string) (d, t, expiresIn string) {
|
||||
@@ -146,6 +155,8 @@ func NewEmailer(app *appContext) *Emailer {
|
||||
}
|
||||
} else if method == "mailgun" {
|
||||
emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String())
|
||||
} else if method == "dummy" {
|
||||
emailer.sender = &DummyClient{}
|
||||
}
|
||||
return emailer
|
||||
}
|
||||
@@ -195,7 +206,7 @@ type templ interface {
|
||||
Execute(wr io.Writer, data interface{}) error
|
||||
}
|
||||
|
||||
func (emailer *Emailer) construct(app *appContext, section, keyFragment string, data map[string]interface{}) (html, text string, err error) {
|
||||
func (emailer *Emailer) construct(app *appContext, section, keyFragment string, data map[string]interface{}) (html, text, markdown string, err error) {
|
||||
var tpl templ
|
||||
if substituteStrings == "" {
|
||||
data["jellyfin"] = "Jellyfin"
|
||||
@@ -203,14 +214,31 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
|
||||
data["jellyfin"] = substituteStrings
|
||||
}
|
||||
var keys []string
|
||||
if app.config.Section("email").Key("plaintext").MustBool(false) {
|
||||
keys = []string{"text"}
|
||||
text = ""
|
||||
plaintext := app.config.Section("email").Key("plaintext").MustBool(false)
|
||||
telegram := app.config.Section("telegram").Key("enabled").MustBool(false)
|
||||
if plaintext {
|
||||
if telegram {
|
||||
keys = []string{"text"}
|
||||
text, markdown = "", ""
|
||||
} else {
|
||||
keys = []string{"text"}
|
||||
text = ""
|
||||
}
|
||||
} else {
|
||||
keys = []string{"html", "text"}
|
||||
if telegram {
|
||||
keys = []string{"html", "text", "markdown"}
|
||||
} else {
|
||||
keys = []string{"html", "text"}
|
||||
}
|
||||
}
|
||||
for _, key := range keys {
|
||||
filesystem, fpath := app.GetPath(section, keyFragment+key)
|
||||
var filesystem fs.FS
|
||||
var fpath string
|
||||
if key == "markdown" {
|
||||
filesystem, fpath = app.GetPath(section, keyFragment+"text")
|
||||
} else {
|
||||
filesystem, fpath = app.GetPath(section, keyFragment+key)
|
||||
}
|
||||
if key == "html" {
|
||||
tpl, err = template.ParseFS(filesystem, fpath)
|
||||
} else {
|
||||
@@ -219,15 +247,28 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// For constructTemplate, if "md" is found in data it's used in stead of "text".
|
||||
foundMarkdown := false
|
||||
if key == "markdown" {
|
||||
_, foundMarkdown = data["md"]
|
||||
if foundMarkdown {
|
||||
data["plaintext"], data["md"] = data["md"], data["plaintext"]
|
||||
}
|
||||
}
|
||||
var tplData bytes.Buffer
|
||||
err = tpl.Execute(&tplData, data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if foundMarkdown {
|
||||
data["plaintext"], data["md"] = data["md"], data["plaintext"]
|
||||
}
|
||||
if key == "html" {
|
||||
html = tplData.String()
|
||||
} else {
|
||||
} else if key == "text" {
|
||||
text = tplData.String()
|
||||
} else {
|
||||
markdown = tplData.String()
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -248,7 +289,7 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
|
||||
template[v] = "{" + v + "}"
|
||||
}
|
||||
} else {
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
||||
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key)
|
||||
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username})
|
||||
@@ -258,23 +299,22 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")),
|
||||
}
|
||||
var err error
|
||||
template := emailer.confirmationValues(code, username, key, app, noSub)
|
||||
if app.storage.customEmails.EmailConfirmation.Enabled {
|
||||
content := app.storage.customEmails.EmailConfirmation.Content
|
||||
for _, v := range app.storage.customEmails.EmailConfirmation.Variables {
|
||||
replaceWith, ok := template[v[1:len(v)-1]]
|
||||
if ok {
|
||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||
}
|
||||
}
|
||||
content := templateEmail(
|
||||
app.storage.customEmails.EmailConfirmation.Content,
|
||||
app.storage.customEmails.EmailConfirmation.Variables,
|
||||
nil,
|
||||
template,
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "email_confirmation", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "email_confirmation", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -282,17 +322,18 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Email, error) {
|
||||
email := &Email{Subject: subject}
|
||||
func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Message, error) {
|
||||
email := &Message{Subject: subject}
|
||||
renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
||||
html := markdown.ToHTML([]byte(md), nil, renderer)
|
||||
text := stripMarkdown(md)
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
var err error
|
||||
email.HTML, email.Text, err = emailer.construct(app, "template_email", "email_", map[string]interface{}{
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "template_email", "email_", map[string]interface{}{
|
||||
"text": template.HTML(html),
|
||||
"plaintext": text,
|
||||
"message": message,
|
||||
"md": md,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -303,7 +344,7 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (
|
||||
func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext, noSub bool) map[string]interface{} {
|
||||
expiry := invite.ValidTill
|
||||
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
||||
inviteLink = fmt.Sprintf("%s/%s", inviteLink, code)
|
||||
template := map[string]interface{}{
|
||||
@@ -330,23 +371,22 @@ func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
|
||||
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("invite_emails").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
|
||||
}
|
||||
template := emailer.inviteValues(code, invite, app, noSub)
|
||||
var err error
|
||||
if app.storage.customEmails.InviteEmail.Enabled {
|
||||
content := app.storage.customEmails.InviteEmail.Content
|
||||
for _, v := range app.storage.customEmails.InviteEmail.Variables {
|
||||
replaceWith, ok := template[v[1:len(v)-1]]
|
||||
if ok {
|
||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||
}
|
||||
}
|
||||
content := templateEmail(
|
||||
app.storage.customEmails.InviteEmail.Content,
|
||||
app.storage.customEmails.InviteEmail.Variables,
|
||||
nil,
|
||||
template,
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "invite_emails", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "invite_emails", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -370,23 +410,22 @@ func (emailer *Emailer) expiryValues(code string, invite Invite, app *appContext
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: emailer.lang.InviteExpiry.get("title"),
|
||||
}
|
||||
var err error
|
||||
template := emailer.expiryValues(code, invite, app, noSub)
|
||||
if app.storage.customEmails.InviteExpiry.Enabled {
|
||||
content := app.storage.customEmails.InviteExpiry.Content
|
||||
for _, v := range app.storage.customEmails.InviteExpiry.Variables {
|
||||
replaceWith, ok := template[v[1:len(v)-1]]
|
||||
if ok {
|
||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||
}
|
||||
}
|
||||
content := templateEmail(
|
||||
app.storage.customEmails.InviteExpiry.Content,
|
||||
app.storage.customEmails.InviteExpiry.Variables,
|
||||
nil,
|
||||
template,
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "notifications", "expiry_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "notifications", "expiry_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -425,23 +464,22 @@ func (emailer *Emailer) createdValues(code, username, address string, invite Inv
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: emailer.lang.UserCreated.get("title"),
|
||||
}
|
||||
template := emailer.createdValues(code, username, address, invite, app, noSub)
|
||||
var err error
|
||||
if app.storage.customEmails.UserCreated.Enabled {
|
||||
content := app.storage.customEmails.UserCreated.Content
|
||||
for _, v := range app.storage.customEmails.UserCreated.Variables {
|
||||
replaceWith, ok := template[v[1:len(v)-1]]
|
||||
if ok {
|
||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||
}
|
||||
}
|
||||
content := templateEmail(
|
||||
app.storage.customEmails.UserCreated.Content,
|
||||
app.storage.customEmails.UserCreated.Variables,
|
||||
nil,
|
||||
template,
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "notifications", "created_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "notifications", "created_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -451,7 +489,7 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
|
||||
|
||||
func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bool) map[string]interface{} {
|
||||
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
template := map[string]interface{}{
|
||||
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
|
||||
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
|
||||
@@ -500,23 +538,22 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("password_resets").Key("subject").MustString(emailer.lang.PasswordReset.get("title")),
|
||||
}
|
||||
template := emailer.resetValues(pwr, app, noSub)
|
||||
var err error
|
||||
if app.storage.customEmails.PasswordReset.Enabled {
|
||||
content := app.storage.customEmails.PasswordReset.Content
|
||||
for _, v := range app.storage.customEmails.PasswordReset.Variables {
|
||||
replaceWith, ok := template[v[1:len(v)-1]]
|
||||
if ok {
|
||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||
}
|
||||
}
|
||||
content := templateEmail(
|
||||
app.storage.customEmails.PasswordReset.Content,
|
||||
app.storage.customEmails.PasswordReset.Variables,
|
||||
nil,
|
||||
template,
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "password_resets", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "password_resets", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -526,9 +563,9 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
|
||||
|
||||
func (emailer *Emailer) deletedValues(reason string, app *appContext, noSub bool) map[string]interface{} {
|
||||
template := map[string]interface{}{
|
||||
"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
|
||||
"reasonString": emailer.lang.UserDeleted.get("reason"),
|
||||
"message": "",
|
||||
"yourAccountWas": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
|
||||
"reasonString": emailer.lang.Strings.get("reason"),
|
||||
"message": "",
|
||||
}
|
||||
if noSub {
|
||||
empty := []string{"reason"}
|
||||
@@ -537,28 +574,27 @@ func (emailer *Emailer) deletedValues(reason string, app *appContext, noSub bool
|
||||
}
|
||||
} else {
|
||||
template["reason"] = reason
|
||||
template["message"] = app.config.Section("email").Key("message").String()
|
||||
template["message"] = app.config.Section("messages").Key("message").String()
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
|
||||
}
|
||||
var err error
|
||||
template := emailer.deletedValues(reason, app, noSub)
|
||||
if app.storage.customEmails.UserDeleted.Enabled {
|
||||
content := app.storage.customEmails.UserDeleted.Content
|
||||
for _, v := range app.storage.customEmails.UserDeleted.Variables {
|
||||
replaceWith, ok := template[v[1:len(v)-1]]
|
||||
if ok {
|
||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||
}
|
||||
}
|
||||
content := templateEmail(
|
||||
app.storage.customEmails.UserDeleted.Content,
|
||||
app.storage.customEmails.UserDeleted.Variables,
|
||||
nil,
|
||||
template,
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "deletion", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "deletion", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -566,44 +602,146 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func (emailer *Emailer) welcomeValues(username string, app *appContext, noSub bool) map[string]interface{} {
|
||||
func (emailer *Emailer) disabledValues(reason string, app *appContext, noSub bool) map[string]interface{} {
|
||||
template := map[string]interface{}{
|
||||
"welcome": emailer.lang.WelcomeEmail.get("welcome"),
|
||||
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"),
|
||||
"jellyfinURLString": emailer.lang.WelcomeEmail.get("jellyfinURL"),
|
||||
"usernameString": emailer.lang.Strings.get("username"),
|
||||
"message": "",
|
||||
"yourAccountWas": emailer.lang.UserDisabled.get("yourAccountWasDisabled"),
|
||||
"reasonString": emailer.lang.Strings.get("reason"),
|
||||
"message": "",
|
||||
}
|
||||
if noSub {
|
||||
empty := []string{"jellyfinURL", "username"}
|
||||
empty := []string{"reason"}
|
||||
for _, v := range empty {
|
||||
template[v] = "{" + v + "}"
|
||||
}
|
||||
} else {
|
||||
template["reason"] = reason
|
||||
template["message"] = app.config.Section("messages").Key("message").String()
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("disable_enable").Key("subject_disabled").MustString(emailer.lang.UserDisabled.get("title")),
|
||||
}
|
||||
var err error
|
||||
template := emailer.disabledValues(reason, app, noSub)
|
||||
if app.storage.customEmails.UserDisabled.Enabled {
|
||||
content := templateEmail(
|
||||
app.storage.customEmails.UserDisabled.Content,
|
||||
app.storage.customEmails.UserDisabled.Variables,
|
||||
nil,
|
||||
template,
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "disable_enable", "disabled_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func (emailer *Emailer) enabledValues(reason string, app *appContext, noSub bool) map[string]interface{} {
|
||||
template := map[string]interface{}{
|
||||
"yourAccountWas": emailer.lang.UserEnabled.get("yourAccountWasEnabled"),
|
||||
"reasonString": emailer.lang.Strings.get("reason"),
|
||||
"message": "",
|
||||
}
|
||||
if noSub {
|
||||
empty := []string{"reason"}
|
||||
for _, v := range empty {
|
||||
template[v] = "{" + v + "}"
|
||||
}
|
||||
} else {
|
||||
template["reason"] = reason
|
||||
template["message"] = app.config.Section("messages").Key("message").String()
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("disable_enable").Key("subject_enabled").MustString(emailer.lang.UserEnabled.get("title")),
|
||||
}
|
||||
var err error
|
||||
template := emailer.enabledValues(reason, app, noSub)
|
||||
if app.storage.customEmails.UserEnabled.Enabled {
|
||||
content := templateEmail(
|
||||
app.storage.customEmails.UserEnabled.Content,
|
||||
app.storage.customEmails.UserEnabled.Variables,
|
||||
nil,
|
||||
template,
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "disable_enable", "enabled_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func (emailer *Emailer) welcomeValues(username string, expiry time.Time, app *appContext, noSub bool, custom bool) map[string]interface{} {
|
||||
template := map[string]interface{}{
|
||||
"welcome": emailer.lang.WelcomeEmail.get("welcome"),
|
||||
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"),
|
||||
"jellyfinURLString": emailer.lang.WelcomeEmail.get("jellyfinURL"),
|
||||
"usernameString": emailer.lang.Strings.get("username"),
|
||||
"message": "",
|
||||
"yourAccountWillExpire": "",
|
||||
}
|
||||
if noSub {
|
||||
empty := []string{"jellyfinURL", "username", "yourAccountWillExpire"}
|
||||
for _, v := range empty {
|
||||
template[v] = "{" + v + "}"
|
||||
}
|
||||
} else {
|
||||
template["jellyfinURL"] = app.config.Section("jellyfin").Key("public_server").String()
|
||||
template["username"] = username
|
||||
template["message"] = app.config.Section("email").Key("message").String()
|
||||
template["message"] = app.config.Section("messages").Key("message").String()
|
||||
exp := app.formatDatetime(expiry)
|
||||
if !expiry.IsZero() {
|
||||
if custom {
|
||||
template["yourAccountWillExpire"] = exp
|
||||
} else if !expiry.IsZero() {
|
||||
template["yourAccountWillExpire"] = emailer.lang.WelcomeEmail.template("yourAccountWillExpire", tmpl{
|
||||
"date": exp,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")),
|
||||
}
|
||||
var err error
|
||||
template := emailer.welcomeValues(username, app, noSub)
|
||||
var template map[string]interface{}
|
||||
if app.storage.customEmails.WelcomeEmail.Enabled {
|
||||
content := app.storage.customEmails.WelcomeEmail.Content
|
||||
for _, v := range app.storage.customEmails.WelcomeEmail.Variables {
|
||||
replaceWith, ok := template[v[1:len(v)-1]]
|
||||
if ok {
|
||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||
}
|
||||
}
|
||||
template = emailer.welcomeValues(username, expiry, app, noSub, true)
|
||||
} else {
|
||||
template = emailer.welcomeValues(username, expiry, app, noSub, false)
|
||||
}
|
||||
if noSub {
|
||||
template["yourAccountWillExpire"] = emailer.lang.WelcomeEmail.template("yourAccountWillExpire", tmpl{
|
||||
"date": "{yourAccountWillExpire}",
|
||||
})
|
||||
}
|
||||
if app.storage.customEmails.WelcomeEmail.Enabled {
|
||||
content := templateEmail(
|
||||
app.storage.customEmails.WelcomeEmail.Content,
|
||||
app.storage.customEmails.WelcomeEmail.Variables,
|
||||
app.storage.customEmails.WelcomeEmail.Conditionals,
|
||||
template,
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "welcome_email", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "welcome_email", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -618,28 +756,27 @@ func (emailer *Emailer) userExpiredValues(app *appContext, noSub bool) map[strin
|
||||
"message": "",
|
||||
}
|
||||
if !noSub {
|
||||
template["message"] = app.config.Section("email").Key("message").String()
|
||||
template["message"] = app.config.Section("messages").Key("message").String()
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Message, error) {
|
||||
email := &Message{
|
||||
Subject: app.config.Section("user_expiry").Key("subject").MustString(emailer.lang.UserExpired.get("title")),
|
||||
}
|
||||
var err error
|
||||
template := emailer.userExpiredValues(app, noSub)
|
||||
if app.storage.customEmails.UserExpired.Enabled {
|
||||
content := app.storage.customEmails.UserExpired.Content
|
||||
for _, v := range app.storage.customEmails.UserExpired.Variables {
|
||||
replaceWith, ok := template[v[1:len(v)-1]]
|
||||
if ok {
|
||||
content = strings.ReplaceAll(content, v, replaceWith.(string))
|
||||
}
|
||||
}
|
||||
content := templateEmail(
|
||||
app.storage.customEmails.UserExpired.Content,
|
||||
app.storage.customEmails.UserExpired.Variables,
|
||||
nil,
|
||||
template,
|
||||
)
|
||||
email, err = emailer.constructTemplate(email.Subject, content, app)
|
||||
} else {
|
||||
email.HTML, email.Text, err = emailer.construct(app, "user_expiry", "email_", template)
|
||||
email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "user_expiry", "email_", template)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -648,6 +785,31 @@ func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Emai
|
||||
}
|
||||
|
||||
// calls the send method in the underlying emailClient.
|
||||
func (emailer *Emailer) send(email *Email, address ...string) error {
|
||||
return emailer.sender.send(emailer.fromName, emailer.fromAddr, email, address...)
|
||||
func (emailer *Emailer) send(email *Message, address ...string) error {
|
||||
return emailer.sender.Send(emailer.fromName, emailer.fromAddr, email, address...)
|
||||
}
|
||||
|
||||
func (app *appContext) sendByID(email *Message, ID ...string) error {
|
||||
for _, id := range ID {
|
||||
var err error
|
||||
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
|
||||
err = app.telegram.Send(email, tgChat.ChatID)
|
||||
} else if address, ok := app.storage.emails[id]; ok {
|
||||
err = app.email.send(email, address.(string))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *appContext) getAddressOrName(jfID string) string {
|
||||
if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled {
|
||||
return "@" + tgChat.Username
|
||||
}
|
||||
if addr, ok := app.storage.emails[jfID]; ok {
|
||||
return addr.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
13
go.mod
13
go.mod
@@ -8,6 +8,8 @@ replace github.com/hrfee/jfa-go/common => ./common
|
||||
|
||||
replace github.com/hrfee/jfa-go/ombi => ./ombi
|
||||
|
||||
replace github.com/hrfee/jfa-go/logger => ./logger
|
||||
|
||||
require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/fatih/color v1.10.0
|
||||
@@ -15,27 +17,28 @@ require (
|
||||
github.com/gin-contrib/pprof v1.3.0
|
||||
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-openapi/spec v0.20.3 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/golang/protobuf v1.4.3 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8
|
||||
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd
|
||||
github.com/google/uuid v1.1.2 // indirect
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20210105184019-fdc97b4e86cc
|
||||
github.com/hrfee/jfa-go/docs v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/jfa-go/logger v0.0.0-00010101000000-000000000000
|
||||
github.com/hrfee/jfa-go/ombi v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/mediabrowser v0.3.3
|
||||
github.com/itchyny/timefmt-go v0.1.2
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0
|
||||
github.com/mailgun/mailgun-go/v4 v4.5.1
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
|
||||
github.com/swaggo/gin-swagger v1.3.0
|
||||
github.com/swaggo/swag v1.7.0 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/ugorji/go v1.2.0 // indirect
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
|
||||
|
||||
24
go.sum
24
go.sum
@@ -58,9 +58,6 @@ github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmC
|
||||
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
|
||||
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
@@ -96,6 +93,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
||||
@@ -112,8 +111,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8 h1:nWU6p08f1VgIalT6iZyqXi4o5cZsz4X6qa87nusfcsc=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd h1:0b8AqsWQb6A0jjx80UXLG/uMTXQkGD0IGuXWqsrNz1M=
|
||||
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -127,12 +126,14 @@ github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/hrfee/mediabrowser v0.3.3 h1:7E05uiol8hh2ytKn3WVLrUIvHAyifYEIy3Y5qtuNh8I=
|
||||
github.com/hrfee/mediabrowser v0.3.3/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||
github.com/itchyny/timefmt-go v0.1.2 h1:q0Xa4P5it6K6D7ISsbLAMwx1PnWlixDcJL6/sFs93Hs=
|
||||
github.com/itchyny/timefmt-go v0.1.2/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible h1:CL0ooBNfbNyJTJATno+m0h+zM5bW6v7fKlboKUGP/dI=
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@@ -156,8 +157,8 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4 h1:uj4xhotfY92Y1Oa6n6HUiFn87CdoEHYUlTy0+IgbLrs=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw=
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0 h1:9nAF7LI3k6bfDPbMZQMMl63Q8/vs+dr1FUN8eR1XMhk=
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0/go.mod h1:fWuBI2iaS/pSSyo6+EBpHjatQO3lV8onwqcRy7joSJI=
|
||||
github.com/mailgun/mailgun-go/v4 v4.5.1 h1:XrQQ/ZgqFvINRKy+eBqowLl7k3pQO6OCLpKphliMOFs=
|
||||
github.com/mailgun/mailgun-go/v4 v4.5.1/go.mod h1:FJlF9rI5cQT+mrwujtJjPMbIVy3Ebor9bKTVsJ0QU40=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@@ -180,9 +181,8 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
||||
@@ -214,6 +214,8 @@ github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+t
|
||||
github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=
|
||||
github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E=
|
||||
github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
|
||||
226
html/admin.html
226
html/admin.html
@@ -6,6 +6,7 @@
|
||||
window.URLBase = "{{ .urlBase }}";
|
||||
window.notificationsEnabled = {{ .notifications }};
|
||||
window.emailEnabled = {{ .email_enabled }};
|
||||
window.telegramEnabled = {{ .telegram_enabled }};
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.usernameEnabled = {{ .username }};
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
@@ -45,6 +46,19 @@
|
||||
<p><i class="icon ri-github-fill"></i><a href="https://github.com/hrfee/jfa-go">jfa-go</a></p>
|
||||
<p>{{ .strings.version }} <span class="code monospace">{{ .version }}</span></p>
|
||||
<p>{{ .strings.commitNoun }} <span class="code monospace">{{ .commit }}</span></p>
|
||||
<div class="dropdown" tabindex="0">
|
||||
<span class="button ~info dropdown-button">
|
||||
<i class="ri-hand-heart-line mr-half"></i>
|
||||
{{ .strings.donate }}
|
||||
<span class="ml-1 chev"></span>
|
||||
</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral !low">
|
||||
<a href="https://github.com/sponsors/hrfee" target="_blank" class="button input ~neutral field mb-half lang-link">GitHub</a>
|
||||
<a href="https://ko-fi.com/hrfee" target="_blank" class="button input ~neutral field mb-half lang-link">Ko-fi</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p><a href="https://github.com/hrfee/jfa-go/blob/main/LICENSE">Available under the MIT License.</a></p>
|
||||
<pre class="monospace">{{ .license }}</pre>
|
||||
</div>
|
||||
@@ -99,23 +113,41 @@
|
||||
<form class="modal-content card" id="form-extend-expiry" href="">
|
||||
<span class="heading"><span id="header-extend-expiry"></span> <span class="modal-close">×</span></span>
|
||||
<div class="content mt-half">
|
||||
<label class="label supra" for="extend-expiry-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="extend-expiry-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="extend-expiry-months">{{ .strings.inviteMonths }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="extend-expiry-months">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="extend-expiry-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="extend-expiry-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="label supra" for="extend-expiry-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="extend-expiry-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="label supra" for="extend-expiry-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="extend-expiry-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="extend-expiry-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="extend-expiry-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="extend-expiry-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="extend-expiry-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
@@ -125,25 +157,31 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-announce" class="modal">
|
||||
<form class="modal-content card" id="form-announce" href="">
|
||||
<form class="modal-content wide card" id="form-announce" href="">
|
||||
<span class="heading"><span id="header-announce"></span> <span class="modal-close">×</span></span>
|
||||
<div class="content mt-half">
|
||||
<label class="label supra" for="announce-subject"> {{ .strings.subject }}</label>
|
||||
<input type="text" id="announce-subject" class="input ~neutral !normal mb-1 mt-half">
|
||||
<label class="label supra" for="textarea-announce">{{ .strings.message }}</label>
|
||||
<textarea id="textarea-announce" class="textarea full-width ~neutral !normal mt-half monospace"></textarea>
|
||||
<p class="support mt-half mb-1">{{ .strings.markdownSupported }}</p>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
<div class="row">
|
||||
<div class="col flex-col content mt-half">
|
||||
<label class="label supra" for="announce-subject"> {{ .strings.subject }}</label>
|
||||
<input type="text" id="announce-subject" class="input ~neutral !normal mb-1 mt-half">
|
||||
<label class="label supra" for="textarea-announce">{{ .strings.message }}</label>
|
||||
<textarea id="textarea-announce" class="textarea full-width ~neutral !normal mt-half monospace"></textarea>
|
||||
<p class="support mt-half mb-1">{{ .strings.markdownSupported }}</p>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col card ~neutral !low">
|
||||
<span class="subheading supra">{{ .strings.preview }}</span>
|
||||
<div class="mt-half" id="announce-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-customize" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading">{{ .strings.customizeEmails }} <span class="modal-close">×</span></span>
|
||||
<p class="content">{{ .strings.customizeEmailsDescription }}</p>
|
||||
<span class="heading">{{ .strings.customizeMessages }} <span class="modal-close">×</span></span>
|
||||
<p class="content">{{ .strings.customizeMessagesDescription }}</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
@@ -165,6 +203,8 @@
|
||||
<div class="col flex-col content mt-half">
|
||||
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span>
|
||||
<div id="editor-variables"></div>
|
||||
<span class="label supra" for="editor-conditionals" id="label-editor-conditionals">{{ .strings.conditionals }}</span>
|
||||
<div id="editor-conditionals"></div>
|
||||
<label class="label supra" for="textarea-editor">{{ .strings.message }}</label>
|
||||
<textarea id="textarea-editor" class="textarea full-width flex-auto ~neutral !normal mt-half monospace"></textarea>
|
||||
<p class="support mt-half mb-1">{{ .strings.markdownSupported }}</p>
|
||||
@@ -269,6 +309,24 @@
|
||||
<span class="button ~urge !normal full-width center" id="update-update">{{ .strings.update }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .telegram_enabled }}
|
||||
<div id="modal-telegram" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading mb-1">{{ .strings.linkTelegram }}</span>
|
||||
<p class="content mb-1">{{ .strings.sendPIN }}</p>
|
||||
<h1 class="ac" id="telegram-pin"></h1>
|
||||
<a class="subheading link-center" id="telegram-link" target="_blank">
|
||||
<span class="shield ~info mr-1">
|
||||
<span class="icon">
|
||||
<i class="ri-telegram-line"></i>
|
||||
</span>
|
||||
</span>
|
||||
@<span id="telegram-username">
|
||||
</a>
|
||||
<span class="button ~info !normal full-width center mt-1" id="telegram-waiting">{{ .strings.success }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div id="notification-box"></div>
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
@@ -325,23 +383,41 @@
|
||||
</label>
|
||||
</div>
|
||||
<div id="inv-duration">
|
||||
<label class="label supra" for="create-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="create-months">{{ .strings.inviteMonths }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-months">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="create-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="label supra" for="create-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="label supra" for="create-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="create-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="create-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="user-expiry" class="unfocused">
|
||||
@@ -352,27 +428,47 @@
|
||||
<span class="ml-half">{{ .strings.enabled }} </span>
|
||||
</label>
|
||||
</div>
|
||||
<label class="label supra" for="user-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="user-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="user-months">{{ .strings.inviteMonths }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="user-months">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="user-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="user-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="label supra" for="user-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="user-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="label supra" for="user-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="user-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="user-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="user-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="user-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="user-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="label supra" for="create-label"> {{ .strings.label }}</label>
|
||||
<input type="text" id="create-label" class="input ~neutral !normal mb-1 mt-half">
|
||||
<div class="col">
|
||||
<label class="label supra" for="create-label"> {{ .strings.label }}</label>
|
||||
<input type="text" id="create-label" class="input ~neutral !normal mb-1 mt-half">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card ~neutral !normal col">
|
||||
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
|
||||
@@ -415,6 +511,7 @@
|
||||
<span class="col sm button ~info !normal center mb-half" id="accounts-announce">{{ .strings.announce }}</span>
|
||||
<span class="col sm button ~urge !normal center mb-half" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
||||
<span class="col sm button ~warning !normal center mb-half" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
|
||||
<span class="col sm button ~positive !normal center mb-half" id="accounts-disable-enable">{{ .strings.disable }}</span>
|
||||
<span class="col sm button ~critical !normal center mb-half" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -425,6 +522,9 @@
|
||||
<th><input type="checkbox" value="" id="accounts-select-all"></th>
|
||||
<th>{{ .strings.username }}</th>
|
||||
<th>{{ .strings.emailAddress }}</th>
|
||||
{{ if .telegram_enabled }}
|
||||
<th>Telegram</th>
|
||||
{{ end }}
|
||||
<th>{{ .strings.expiry }}</th>
|
||||
<th>{{ .strings.lastActiveTime }}</th>
|
||||
</tr>
|
||||
|
||||
@@ -9,10 +9,14 @@
|
||||
window.messages = JSON.parse({{ .notifications }});
|
||||
window.confirmation = {{ .confirmation }};
|
||||
window.userExpiryEnabled = {{ .userExpiry }};
|
||||
window.userExpiryMonths = {{ .userExpiryMonths }};
|
||||
window.userExpiryDays = {{ .userExpiryDays }};
|
||||
window.userExpiryHours = {{ .userExpiryHours }};
|
||||
window.userExpiryMinutes = {{ .userExpiryMinutes }};
|
||||
window.userExpiryMessage = {{ .userExpiryMessage }};
|
||||
window.telegramEnabled = {{ .telegramEnabled }};
|
||||
window.telegramRequired = {{ .telegramRequired }};
|
||||
window.telegramPIN = "{{ .telegramPIN }}";
|
||||
</script>
|
||||
<script src="js/form.js" type="module"></script>
|
||||
{{ end }}
|
||||
|
||||
@@ -19,6 +19,24 @@
|
||||
<p class="content mb-1">{{ .strings.confirmationRequiredMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .telegramEnabled }}
|
||||
<div id="modal-telegram" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading mb-1">{{ .strings.linkTelegram }}</span>
|
||||
<p class="content mb-1">{{ .strings.sendPIN }}</p>
|
||||
<h1 class="ac">{{ .telegramPIN }}</h1>
|
||||
<a class="subheading link-center" href="{{ .telegramURL }}" target="_blank">
|
||||
<span class="shield ~info mr-1">
|
||||
<span class="icon">
|
||||
<i class="ri-telegram-line"></i>
|
||||
</span>
|
||||
</span>
|
||||
@{{ .telegramUsername }}
|
||||
</a>
|
||||
<span class="button ~info !normal full-width center mt-1" id="telegram-waiting">{{ .strings.success }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
@@ -29,6 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<div id="notification-box"></div>
|
||||
<div class="page-container">
|
||||
<div class="card ~neutral !low">
|
||||
<div class="row baseline">
|
||||
@@ -48,7 +67,17 @@
|
||||
|
||||
<label class="label supra" for="create-email">{{ .strings.emailAddress }}</label>
|
||||
<input type="email" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.emailAddress }}" id="create-email" aria-label="{{ .strings.emailAddress }}" value="{{ .email }}">
|
||||
|
||||
{{ if .telegramEnabled }}
|
||||
<span class="button ~info !normal full-width center mb-1" id="link-telegram">{{ .strings.linkTelegram }}</span>
|
||||
<div id="contact-via" class="unfocused">
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="contact-via" value="email"><span>Contact through Email</span>
|
||||
</label>
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="contact-via" value="telegram" id="contact-via-telegram"><span>Contact through Telegram</span>
|
||||
</label>
|
||||
</div>
|
||||
{{ end }}
|
||||
<label class="label supra" for="create-password">{{ .strings.password }}</label>
|
||||
<input type="password" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-password" aria-label="{{ .strings.password }}">
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
</span>
|
||||
<p class="content mb-1">
|
||||
{{ if .success }}
|
||||
{{ if .ombiEnabled }}
|
||||
{{ .strings.youCanLoginOmbi }}
|
||||
{{ else }}
|
||||
{{ .strings.youCanLogin }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ .strings.tryAgain }}
|
||||
{{ end }}
|
||||
|
||||
@@ -376,7 +376,18 @@
|
||||
<input type="text" class="input ~neutral !normal mt-half" id="password_resets-watch_directory" placeholder="/config/jellyfin">
|
||||
<p class="support mb-1">{{ .lang.PasswordResets.pathToJellyfinNotice }}</p>
|
||||
</label>
|
||||
<label class="label">
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="password_resets-link_reset"><span>{{ .lang.PasswordResets.resetLinks }}</span>
|
||||
<p class="support mb-1">{{ .lang.PasswordResets.resetLinksNotice }}</p>
|
||||
</label>
|
||||
<label class="row label">
|
||||
<p class="mt-half">{{ .lang.PasswordResets.resetLinksLanguage }}</p>
|
||||
<div class="select ~neutral !normal mt-half mb-1">
|
||||
<select id="password_resets-language">
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label class="row label">
|
||||
<span class="mt-half">{{ .lang.Strings.emailSubject }}</span>
|
||||
<input type="text" class="input ~neutral !normal mt-half mb-1" id="password_resets-subject" placeholder="{{ .emailLang.PasswordReset.title }}">
|
||||
</label>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 66 KiB |
@@ -13,7 +13,7 @@ const binaryType = "internal"
|
||||
//go:embed data data/html data/web data/web/css data/web/js
|
||||
var loFS embed.FS
|
||||
|
||||
//go:embed lang/common lang/admin lang/email lang/form lang/setup lang/pwreset
|
||||
//go:embed lang/common lang/admin lang/email lang/form lang/setup lang/pwreset lang/telegram
|
||||
var laFS embed.FS
|
||||
|
||||
var langFS rewriteFS
|
||||
|
||||
21
lang.go
21
lang.go
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
type langMeta struct {
|
||||
Name string `json:"name"`
|
||||
// Language to fall back on if strings are missing. Defaults to en-us.
|
||||
Fallback string `json:"fallback,omitempty"`
|
||||
}
|
||||
|
||||
type quantityString struct {
|
||||
@@ -93,6 +95,8 @@ type emailLang struct {
|
||||
InviteExpiry langSection `json:"inviteExpiry"`
|
||||
PasswordReset langSection `json:"passwordReset"`
|
||||
UserDeleted langSection `json:"userDeleted"`
|
||||
UserDisabled langSection `json:"userDisabled"`
|
||||
UserEnabled langSection `json:"userEnabled"`
|
||||
InviteEmail langSection `json:"inviteEmail"`
|
||||
WelcomeEmail langSection `json:"welcomeEmail"`
|
||||
EmailConfirmation langSection `json:"emailConfirmation"`
|
||||
@@ -132,6 +136,23 @@ func (ls *setupLangs) getOptions() [][2]string {
|
||||
return opts
|
||||
}
|
||||
|
||||
type telegramLangs map[string]telegramLang
|
||||
|
||||
type telegramLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
}
|
||||
|
||||
func (ts *telegramLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ts))
|
||||
i := 0
|
||||
for key, lang := range *ts {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
type langSection map[string]string
|
||||
type tmpl map[string]string
|
||||
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
"preview": "Vorschau",
|
||||
"reset": "Zurücksetzen",
|
||||
"edit": "Bearbeiten",
|
||||
"customizeEmails": "E-Mails anpassen",
|
||||
"customizeEmailsDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
|
||||
"customizeMessages": "E-Mails anpassen",
|
||||
"customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
|
||||
"announce": "Ankündigen",
|
||||
"subject": "E-Mail-Betreff",
|
||||
"message": "Nachricht",
|
||||
|
||||
@@ -69,9 +69,9 @@
|
||||
"preview": "Προεπισκόπηση",
|
||||
"reset": "Επαναφορά",
|
||||
"edit": "Επεξεργασία",
|
||||
"customizeEmails": "Παραμετροποίηση Emails",
|
||||
"customizeMessages": "Παραμετροποίηση Emails",
|
||||
"advancedSettings": "Προχωρημένες Ρυθμίσεις",
|
||||
"customizeEmailsDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",
|
||||
"customizeMessagesDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",
|
||||
"updates": "Ενημερώσεις",
|
||||
"update": "Ενημέρωση",
|
||||
"download": "Λήψη",
|
||||
@@ -87,7 +87,10 @@
|
||||
"subject": "Θέμα Email",
|
||||
"message": "Μήνυμα",
|
||||
"extendExpiry": "Παράταση λήξης",
|
||||
"markdownSupported": "Το Markdown υποστυρίζεται."
|
||||
"markdownSupported": "Το Markdown υποστυρίζεται.",
|
||||
"reEnable": "Επανα-ενεργοποίηση",
|
||||
"disable": "Απενεργοποίηση",
|
||||
"inviteMonths": "Μήνες"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Αλλαγή {n} διεύθυνσεων email.",
|
||||
@@ -162,6 +165,22 @@
|
||||
"extendedExpiry": {
|
||||
"singular": "Εκτεταμένη λήξη για {n} χρήστη.",
|
||||
"plural": "Εκτεταμένη λήξη για {n} χρήστες."
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Απενεργοποίηση {n} χρήστη",
|
||||
"plural": "Απενεργοποίηση {n} χρηστών"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Εκ νέου ενεργοποίηση {n} χρήστη",
|
||||
"plural": "Εκ νέου ενεργοποίηση {n} χρηστών"
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "Απενεργοποιήθηκε {n} χρήστης.",
|
||||
"plural": "Απενεργοποιήθηκαν {n} χρήστες."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "Εργοποιήθηκε {n} χρήστης.",
|
||||
"plural": "Εργοποιήθηκαν {n} χρήστες."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"invites": "Invites",
|
||||
"accounts": "Accounts",
|
||||
"settings": "Settings",
|
||||
"inviteMonths": "Months",
|
||||
"inviteDays": "Days",
|
||||
"inviteHours": "Hours",
|
||||
"inviteMinutes": "Minutes",
|
||||
@@ -23,6 +24,8 @@
|
||||
"date": "Date",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"reEnable": "Re-enable",
|
||||
"disable": "Disable",
|
||||
"admin": "Admin",
|
||||
"updates": "Updates",
|
||||
"update": "Update",
|
||||
@@ -43,20 +46,23 @@
|
||||
"unknown": "Unknown",
|
||||
"label": "Label",
|
||||
"announce": "Announce",
|
||||
"subject": "Email Subject",
|
||||
"subject": "Subject",
|
||||
"message": "Message",
|
||||
"variables": "Variables",
|
||||
"conditionals": "Conditionals",
|
||||
"preview": "Preview",
|
||||
"reset": "Reset",
|
||||
"edit": "Edit",
|
||||
"donate": "Donate",
|
||||
"contactThrough": "Contact through:",
|
||||
"extendExpiry": "Extend expiry",
|
||||
"customizeEmails": "Customize Emails",
|
||||
"customizeEmailsDescription": "If you don't want to use jfa-go's email templates, you can create your own using Markdown.",
|
||||
"customizeMessages": "Customize Messages",
|
||||
"customizeMessagesDescription": "If you don't want to use jfa-go's message templates, you can create your own using Markdown.",
|
||||
"markdownSupported": "Markdown is supported.",
|
||||
"modifySettings": "Modify Settings",
|
||||
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
|
||||
"applyHomescreenLayout": "Apply homescreen layout",
|
||||
"sendDeleteNotificationEmail": "Send notification email",
|
||||
"sendDeleteNotificationEmail": "Send notification message",
|
||||
"sendDeleteNotifiationExample": "Your account has been deleted.",
|
||||
"settingsRestart": "Restart",
|
||||
"settingsRestarting": "Restarting…",
|
||||
@@ -87,7 +93,8 @@
|
||||
"inviteExpiresInTime": "Expires in {n}",
|
||||
"notifyEvent": "Notify on:",
|
||||
"notifyInviteExpiry": "On expiry",
|
||||
"notifyUserCreation": "On user creation"
|
||||
"notifyUserCreation": "On user creation",
|
||||
"sendPIN": "Ask the user to send the PIN below to the bot."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Changed email address of {n}.",
|
||||
@@ -98,6 +105,8 @@
|
||||
"sentAnnouncement": "Announcement sent.",
|
||||
"setOmbiDefaults": "Stored ombi defaults.",
|
||||
"updateApplied": "Update applied, please restart.",
|
||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||
"telegramVerified": "Telegram account verified.",
|
||||
"errorConnection": "Couldn't connect to jfa-go.",
|
||||
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||
@@ -120,7 +129,7 @@
|
||||
"errorFailureCheckLogs": "Failed (check console/logs)",
|
||||
"errorPartialFailureCheckLogs": "Partial failure (check console/logs)",
|
||||
"errorUserCreated": "Failed to create user {n}.",
|
||||
"errorSendWelcomeEmail": "Failed to send welcome email (check console/logs)",
|
||||
"errorSendWelcomeEmail": "Failed to send welcome message (check console/logs)",
|
||||
"errorApplyUpdate": "Failed to apply update, try manually.",
|
||||
"errorCheckUpdate": "Failed to check for update.",
|
||||
"updateAvailable": "A new update is available, check settings.",
|
||||
@@ -135,6 +144,14 @@
|
||||
"singular": "Delete {n} user",
|
||||
"plural": "Delete {n} users"
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Disable {n} user",
|
||||
"plural": "Disable {n} users"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Re-enable {n} user",
|
||||
"plural": "Re-enable {n} users"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Add user",
|
||||
"plural": "Add users"
|
||||
@@ -147,6 +164,14 @@
|
||||
"singular": "Deleted {n} user.",
|
||||
"plural": "Deleted {n} users."
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "Disabled {n} user.",
|
||||
"plural": "Disabled {n} users."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "Enabled {n} user.",
|
||||
"plural": "Enabled {n} users."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Announce to {n} user",
|
||||
"plural": "Announce to {n} users"
|
||||
|
||||
187
lang/admin/es-es.json
Normal file
187
lang/admin/es-es.json
Normal file
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Español(ES)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Invitaciones",
|
||||
"accounts": "Cuentas",
|
||||
"settings": "Ajustes",
|
||||
"inviteMonths": "Meses",
|
||||
"inviteDays": "Días",
|
||||
"inviteHours": "Horas",
|
||||
"inviteMinutes": "Minutos",
|
||||
"inviteNumberOfUses": "Números de usos",
|
||||
"inviteDuration": "Duración de invitación",
|
||||
"warning": "Advertencia",
|
||||
"inviteInfiniteUsesWarning": "Las invitaciones con usos infinitos pueden usarse abusivamente",
|
||||
"inviteSendToEmail": "Enviar a",
|
||||
"login": "Acceso",
|
||||
"logout": "Cerrar sesión",
|
||||
"create": "Cerrar sesión",
|
||||
"apply": "Aplicar",
|
||||
"delete": "Eliminar",
|
||||
"name": "Nombre",
|
||||
"date": "Fecha",
|
||||
"enabled": "Activado",
|
||||
"disabled": "Desactivado",
|
||||
"reEnable": "Reactivar",
|
||||
"disable": "Desactivar",
|
||||
"admin": "Administrador",
|
||||
"updates": "Actualizaciones",
|
||||
"update": "Actualizar",
|
||||
"download": "Descargar",
|
||||
"search": "Buscar",
|
||||
"advancedSettings": "Ajustes avanzados",
|
||||
"lastActiveTime": "Último activo",
|
||||
"from": "De",
|
||||
"user": "Usuario",
|
||||
"expiry": "Expiración",
|
||||
"userExpiry": "Caducidad del usuario",
|
||||
"userExpiryDescription": "Una cantidad específica de tiempo después de cada registro, jfa-go eliminará/deshabilitará la cuenta. Puede cambiar este comportamiento en la configuración.",
|
||||
"aboutProgram": "Acerca de",
|
||||
"version": "Versión",
|
||||
"commitNoun": "Cometer",
|
||||
"newUser": "Nuevo usuario",
|
||||
"profile": "Perfil",
|
||||
"unknown": "Desconocido",
|
||||
"label": "Etiqueta",
|
||||
"announce": "Anunciar",
|
||||
"subject": "Asunto del email",
|
||||
"message": "Mensaje",
|
||||
"variables": "Variables",
|
||||
"preview": "Previsualizar",
|
||||
"reset": "Reiniciar",
|
||||
"edit": "Editar",
|
||||
"extendExpiry": "Extender el vencimiento",
|
||||
"customizeMessages": "Personalizar emails",
|
||||
"customizeMessagesDescription": "Si no desea utilizar las plantillas de correo electrónico de jfa-go, puede crear las suyas propias con Markdown.",
|
||||
"markdownSupported": "Se admite Markdown.",
|
||||
"modifySettings": "Modificar configuración",
|
||||
"modifySettingsDescription": "Aplique la configuración de un perfil existente u obténgalos directamente de un usuario.",
|
||||
"applyHomescreenLayout": "Aplicar el diseño de la pantalla de inicio",
|
||||
"sendDeleteNotificationEmail": "Enviar notificación a correo",
|
||||
"sendDeleteNotifiationExample": "Tu cuenta ha sido eliminada.",
|
||||
"settingsRestart": "Reiniciar",
|
||||
"settingsRestarting": "Reiniciando…",
|
||||
"settingsRestartRequired": "Reinicio necesario",
|
||||
"settingsRestartRequiredDescription": "Es necesario reiniciar para aplicar algunas configuraciones que cambió. ¿Reiniciar ahora o más tarde?",
|
||||
"settingsApplyRestartLater": "Aplicar, reiniciar más tarde",
|
||||
"settingsApplyRestartNow": "Aplicar, reiniciar más tarde",
|
||||
"settingsApplied": "Se aplicó la configuración.",
|
||||
"settingsRefreshPage": "Actualiza la página en unos segundos.",
|
||||
"settingsRequiredOrRestartMessage": "Nota: {n} indica un campo obligatorio, {n} indica que los cambios requieren un reinicio.",
|
||||
"settingsSave": "Guardar",
|
||||
"ombiUserDefaults": "Valores predeterminados de usuario de Ombi",
|
||||
"ombiUserDefaultsDescription": "Cree un usuario Ombi y configúrelo, luego selecciónelo a continuación. Sus configuraciones / permisos se almacenarán y aplicarán a los nuevos usuarios de Ombi creados por jfa-go",
|
||||
"userProfiles": "Perfiles de usuario",
|
||||
"userProfilesDescription": "Los perfiles se aplican a los usuarios cuando crean una cuenta. Un perfil incluye los derechos de acceso a la biblioteca y el diseño de la pantalla de inicio.",
|
||||
"userProfilesIsDefault": "Defecto",
|
||||
"userProfilesLibraries": "Bibliotecas",
|
||||
"addProfile": "Agregar perfil",
|
||||
"addProfileDescription": "Cree un usuario de Jellyfin y configúrelo, luego selecciónelo a continuación. Cuando este perfil se aplica a una invitación, se crearán nuevos usuarios con la configuración.",
|
||||
"addProfileNameOf": "Nombre de perfil",
|
||||
"addProfileStoreHomescreenLayout": "Diseño de la pantalla de inicio de la tienda",
|
||||
"inviteNoUsersCreated": "¡Ninguno todavía!",
|
||||
"inviteUsersCreated": "Usuarios creados",
|
||||
"inviteNoProfile": "Sin perfil",
|
||||
"inviteDateCreated": "Creado",
|
||||
"inviteRemainingUses": "Usos restantes",
|
||||
"inviteNoInvites": "Ninguno",
|
||||
"inviteExpiresInTime": "Caduca en {n}",
|
||||
"notifyEvent": "Notificar en:",
|
||||
"notifyInviteExpiry": "Al vencimiento",
|
||||
"notifyUserCreation": "Sobre la creación de usuarios",
|
||||
"conditionals": "Condicionales"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Se cambió la dirección de correo electrónico de {n}.",
|
||||
"userCreated": "Usuario {n} creado.",
|
||||
"createProfile": "Perfil creado {n}.",
|
||||
"saveSettings": "Se guardaron las configuraciones",
|
||||
"saveEmail": "Correo electrónico guardado.",
|
||||
"sentAnnouncement": "Anuncio enviado.",
|
||||
"setOmbiDefaults": "Valores predeterminados de ombi almacenados.",
|
||||
"updateApplied": "Actualización aplicada, por favor reinicie.",
|
||||
"errorConnection": "No se pudo conectar a jfa-go.",
|
||||
"error401Unauthorized": "No autorizado. Intente actualizar la página.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Se aplicó la configuración, pero es posible que no se haya aplicado el diseño de la pantalla de inicio.",
|
||||
"errorHomescreenAppliedNoSettings": "Se aplicó el diseño de la pantalla de inicio, pero es posible que no se haya aplicado la configuración.",
|
||||
"errorSettingsFailed": "La aplicación falló.",
|
||||
"errorLoginBlank": "El nombre de usuario y/o la contraseña se dejaron en blanco.",
|
||||
"errorUnknown": "Error desconocido.",
|
||||
"errorSaveEmail": "No se pudo guardar el correo electrónico.",
|
||||
"errorBlankFields": "Los campos se dejaron en blanco",
|
||||
"errorDeleteProfile": "No se pudo borrar el perfil {n}",
|
||||
"errorLoadProfiles": "No se pudieron cargar los perfiles.",
|
||||
"errorCreateProfile": "No se pudo crear el perfil {n}",
|
||||
"errorSetDefaultProfile": "No se pudo establecer el perfil predeterminado.",
|
||||
"errorLoadUsers": "No se pudieron cargar los usuarios.",
|
||||
"errorSaveSettings": "No se pudo guardar la configuración.",
|
||||
"errorLoadSettings": "No se pudo cargar la configuración.",
|
||||
"errorSetOmbiDefaults": "No se pudieron almacenar los valores predeterminados de ombi.",
|
||||
"errorLoadOmbiUsers": "No se pudieron cargar los usuarios de ombi.",
|
||||
"errorChangedEmailAddress": "No se pudo cambiar la dirección de correo electrónico de {n}.",
|
||||
"errorFailureCheckLogs": "Fallido (ver consola / registros)",
|
||||
"errorPartialFailureCheckLogs": "Fallo parcial (ver consola / registros)",
|
||||
"errorUserCreated": "No se pudo crear el usuario {n}.",
|
||||
"errorSendWelcomeEmail": "No se pudo enviar el correo electrónico de bienvenida (verifique la consola / registros)",
|
||||
"errorApplyUpdate": "No se pudo aplicar la actualización, intente manualmente.",
|
||||
"errorCheckUpdate": "No se pudo comprobar la actualización.",
|
||||
"updateAvailable": "Hay una nueva actualización disponible, verifique la configuración.",
|
||||
"noUpdatesAvailable": "No hay nuevas actualizaciones disponibles."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Modificar la configuración de {n} usuario",
|
||||
"plural": "Modificar la configuración de {n} usuarios"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Eliminar {n} usuario",
|
||||
"plural": "Eliminar {n} usuarios"
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Deshabilitar {n} usuario",
|
||||
"plural": "Inhabilitar {n} usuarios"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Reactivar {n} usuario",
|
||||
"plural": "Reactivar {n} usuarios"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Agregar usuario",
|
||||
"plural": "Agregar usuarios"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Borrar usuario",
|
||||
"plural": "Borrar usuarios"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "Usuario eliminado {n}.",
|
||||
"plural": "Usuarios eliminados {n}."
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "Usuario deshabilitado {n}.",
|
||||
"plural": "Usuarios deshabilitados {n}."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "Usuario {n} habilitado.",
|
||||
"plural": "Usuarios {n} habilitados."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Anunciar al usuario {n}",
|
||||
"plural": "Anunciar a los usuarios {n}"
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Se aplicó la configuración al usuario {n}.",
|
||||
"plural": "Se aplicó la configuración a los usuarios {n}."
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Extender la expiración para el usuario {n}",
|
||||
"plural": "Extender la expiración para los usuarios {n}"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Caducidad extendida para el usuario {n}.",
|
||||
"plural": "Caducidad extendida para los usuarios {n}."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
"invites": "Invitations",
|
||||
"accounts": "Comptes",
|
||||
"settings": "Réglages",
|
||||
"inviteMonths": "Mois",
|
||||
"inviteDays": "Jours",
|
||||
"inviteHours": "Heures",
|
||||
"inviteMinutes": "Minutes",
|
||||
@@ -67,15 +68,29 @@
|
||||
"settingsRestarting": "Redémarrage…",
|
||||
"settingsRestart": "Redémarrer",
|
||||
"announce": "Annoncer",
|
||||
"subject": "Sujet du courriel",
|
||||
"subject": "Sujet",
|
||||
"message": "Message",
|
||||
"markdownSupported": "Markdown est pris en charge.",
|
||||
"customizeEmailsDescription": "Si vous ne souhaitez pas utiliser les modèles d'e-mails de jfa-go, vous pouvez créer les vôtres à l'aide de Markdown.",
|
||||
"customizeMessagesDescription": "Si vous ne souhaitez pas utiliser les modèles d'e-mails de jfa-go, vous pouvez créer les vôtres à l'aide de Markdown.",
|
||||
"variables": "Variables",
|
||||
"preview": "Aperçu",
|
||||
"reset": "Réinitialiser",
|
||||
"edit": "Éditer",
|
||||
"customizeEmails": "Personnaliser les e-mails"
|
||||
"customizeMessages": "Personnaliser les e-mails",
|
||||
"inviteDuration": "Durée de l'invitation",
|
||||
"enabled": "Activé",
|
||||
"disabled": "Désactivé",
|
||||
"reEnable": "Ré-activé",
|
||||
"disable": "Désactivé",
|
||||
"admin": "Administrateur",
|
||||
"expiry": "Expiration",
|
||||
"advancedSettings": "Paramètres avancés",
|
||||
"userExpiry": "Expiration de l'utilisateur",
|
||||
"updates": "Mises à jour",
|
||||
"update": "Mise à jour",
|
||||
"download": "Téléchargement",
|
||||
"search": "Recherche",
|
||||
"conditionals": "Conditions"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Adresse e-mail modifiée de {n}.",
|
||||
@@ -107,7 +122,8 @@
|
||||
"errorSendWelcomeEmail": "Echec lors de l'envoi du mail de bienvenue (vérifier la console/les journaux)",
|
||||
"sentAnnouncement": "Annonce envoyée.",
|
||||
"saveEmail": "Email enregistré.",
|
||||
"errorSaveEmail": "Échec de l'enregistrement de l'e-mail."
|
||||
"errorSaveEmail": "Échec de l'enregistrement de l'e-mail.",
|
||||
"updateApplied": "Mise à jour appliquée, veuillez redémarrer."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
"preview": "Pratinjau",
|
||||
"reset": "Setel ulang",
|
||||
"edit": "Edit",
|
||||
"customizeEmails": "Sesuaikan Email",
|
||||
"customizeEmailsDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.",
|
||||
"customizeMessages": "Sesuaikan Email",
|
||||
"customizeMessagesDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.",
|
||||
"announce": "Mengumumkan",
|
||||
"subject": "Subjek Email",
|
||||
"message": "Pesan",
|
||||
|
||||
@@ -70,11 +70,11 @@
|
||||
"subject": "E-mailonderwerp",
|
||||
"message": "Bericht",
|
||||
"variables": "Variabelen",
|
||||
"customizeEmailsDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
|
||||
"customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
|
||||
"preview": "Voorbeeld",
|
||||
"reset": "Resetten",
|
||||
"edit": "Bewerken",
|
||||
"customizeEmails": "E-mails aanpassen",
|
||||
"customizeMessages": "E-mails aanpassen",
|
||||
"inviteDuration": "Geldigheidsduur uitnodiging",
|
||||
"userExpiryDescription": "Een bepaalde tijd na elke aanmelding, wordt de account verwijderd/uitgeschakeld door jfa-go. Dit kan aangepast worden in de instellingen.",
|
||||
"enabled": "Ingeschakeld",
|
||||
@@ -87,7 +87,11 @@
|
||||
"update": "Bijwerken",
|
||||
"download": "Download",
|
||||
"search": "Zoeken",
|
||||
"advancedSettings": "Geavanceerde instellingen"
|
||||
"advancedSettings": "Geavanceerde instellingen",
|
||||
"inviteMonths": "Maanden",
|
||||
"reEnable": "Opnieuw inschakelen",
|
||||
"disable": "Uitschakelen",
|
||||
"conditionals": "Voorwaarden"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "E-mailadres van {n} gewijzigd.",
|
||||
@@ -162,6 +166,22 @@
|
||||
"extendedExpiry": {
|
||||
"singular": "Verloop uitgesteld voor {n} gebruiker.",
|
||||
"plural": "Verloop uitgesteld voor {n} gebruikers."
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Schakel {n} gebruiker uit",
|
||||
"plural": "Schakel {n} gebruikers uit"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Schakel {n} gebruiker opnieuw in",
|
||||
"plural": "Schakel {n} gebruikers opnieuw in"
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "{n} gebruiker uitgeschakeld.",
|
||||
"plural": "{n} gebruikers uitgeschakeld."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "{n} gebruiker ingeschakeld.",
|
||||
"plural": "{n} gebruikers ingeschakeld."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,12 +69,12 @@
|
||||
"subject": "Assunto do email",
|
||||
"message": "Mensagem",
|
||||
"markdownSupported": "Suporte a Markdown.",
|
||||
"customizeEmailsDescription": "Se não quiser usar os modelos de email do jfa-go, você pode criar o seu próprio usando o Markdown.",
|
||||
"customizeMessagesDescription": "Se não quiser usar os modelos de email do jfa-go, você pode criar o seu próprio usando o Markdown.",
|
||||
"variables": "Variáveis",
|
||||
"preview": "Pre-visualizar",
|
||||
"reset": "Reiniciar",
|
||||
"edit": "Editar",
|
||||
"customizeEmails": "Customizar Emails",
|
||||
"customizeMessages": "Customizar Emails",
|
||||
"disabled": "Desativado",
|
||||
"userExpiryDescription": "Após um determinado período de tempo de cada inscrição, o jfa-go apagará/desabilitará a conta. Você pode alterar essa opção nas configurações.",
|
||||
"inviteDuration": "Duração do Convite",
|
||||
@@ -87,7 +87,11 @@
|
||||
"update": "Atualizar",
|
||||
"download": "Download",
|
||||
"search": "Procurar",
|
||||
"advancedSettings": "Configurações Avançada"
|
||||
"advancedSettings": "Configurações Avançada",
|
||||
"inviteMonths": "Meses",
|
||||
"reEnable": "Reativar",
|
||||
"disable": "Desativar",
|
||||
"conditionals": "Condicionais"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Endereço de e-mail alterado de {n}.",
|
||||
@@ -162,6 +166,22 @@
|
||||
"extendedExpiry": {
|
||||
"plural": "Extender o vencimento para {n} usuários.",
|
||||
"singular": "Extender vencimento para {n}."
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Desativar {n} usuário",
|
||||
"plural": "Desativar {n} usuários"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Reativar {n} usuário",
|
||||
"plural": "Reativar {n} usuários"
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "{n} Usuário desativado.",
|
||||
"plural": "{n} usuários desativado."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "{n} Usuário habilitado.",
|
||||
"plural": "{n} Usuários habilitado."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
"preview": "Förhandsvisning",
|
||||
"reset": "Återställ",
|
||||
"edit": "Redigera",
|
||||
"customizeEmails": "Anpassa e-post",
|
||||
"customizeEmailsDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.",
|
||||
"customizeMessages": "Anpassa e-post",
|
||||
"customizeMessagesDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.",
|
||||
"markdownSupported": "Markdown stöds.",
|
||||
"modifySettings": "Ändra inställningar",
|
||||
"modifySettingsDescription": "Tillämpa inställningar från en befintlig profil eller kopiera dem direkt från en användare.",
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"copied": "Copied",
|
||||
"time24h": "24h Time",
|
||||
"time12h": "12h Time",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Contact through Email",
|
||||
"contactTelegram": "Contact through Telegram",
|
||||
"theme": "Theme"
|
||||
}
|
||||
}
|
||||
|
||||
19
lang/common/es-es.json
Normal file
19
lang/common/es-es.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Español(ES)"
|
||||
},
|
||||
"strings": {
|
||||
"username": "Nombre de usuario",
|
||||
"password": "Contraseña",
|
||||
"emailAddress": "Dirección de correo electrónico",
|
||||
"name": "Nombre",
|
||||
"submit": "Enviar",
|
||||
"success": "Éxito",
|
||||
"error": "Error",
|
||||
"copy": "Copiar",
|
||||
"copied": "Copiado",
|
||||
"time24h": "24 horas",
|
||||
"time12h": "24 horas",
|
||||
"theme": "Tema"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Wenn du das nicht warst, ignoriere bitte diese E-Mail.",
|
||||
"reason": "Grund",
|
||||
"helloUser": "Hallo {username},"
|
||||
},
|
||||
"userCreated": {
|
||||
@@ -31,7 +32,6 @@
|
||||
"userDeleted": {
|
||||
"title": "Dein Konto wurde gelöscht - Jellyfin",
|
||||
"yourAccountWasDeleted": "Dein Jellyfin-Konto wurde gelöscht.",
|
||||
"reason": "Grund",
|
||||
"name": "Benutzerlöschung"
|
||||
},
|
||||
"inviteEmail": {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Αν δεν ήσασταν εσείς, παρακαλώ αγνοήστε αυτό το email.",
|
||||
"reason": "Λόγος",
|
||||
"helloUser": "Γεία σου {username},"
|
||||
},
|
||||
"userCreated": {
|
||||
@@ -32,7 +33,6 @@
|
||||
"userDeleted": {
|
||||
"title": "Ο λογαριασμός σας διαγράφηκε - Jellyfin",
|
||||
"yourAccountWasDeleted": "Ο λογαριασμός σας Jellyfin διαγράφηκε.",
|
||||
"reason": "Λόγος",
|
||||
"name": "Διαγραφή χρήστη"
|
||||
},
|
||||
"inviteEmail": {
|
||||
@@ -49,7 +49,8 @@
|
||||
"welcome": "Καλώς ήλθατε στο Jellyfin!",
|
||||
"youCanLoginWith": "Μπορείτε να συνδεθείτε με τα παρακάτω στοιχεία",
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Email καλωσορίσματος"
|
||||
"name": "Email καλωσορίσματος",
|
||||
"yourAccountWillExpire": "Ο λογαριασμός σας θα λήξει στις {date}."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
"title": "Επιβεβαιώστε το email σας - Jellyfin",
|
||||
@@ -62,5 +63,15 @@
|
||||
"title": "Ο λογαριασμός σας έληξε - Jellyfin",
|
||||
"yourAccountHasExpired": "Ο λογαριασμός σας έχει λήξει.",
|
||||
"contactTheAdmin": "Επικοινωνήστε με τον διαχειριστή για περισσότερες πληροφορίες."
|
||||
},
|
||||
"userDisabled": {
|
||||
"name": "Ο χρήστης απενεργοποιήθηκε",
|
||||
"title": "Ο λογαριασμός σας έχει απενεργοποιηθεί - Jellyfin",
|
||||
"yourAccountWasDisabled": "Ο λογαριασμός σας απενεργοποιήθηκε."
|
||||
},
|
||||
"userEnabled": {
|
||||
"title": "Ο λογαριασμός σας έχει ενεργοποιηθεί ξανά - Jellyfin",
|
||||
"name": "Ο χρήστης ενεργοποιήθηκε",
|
||||
"yourAccountWasEnabled": "Ο λογαριασμός σας ενεργοποιήθηκε εκ νέου."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
"name": "English (US)"
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "If this wasn't you, please ignore this email.",
|
||||
"helloUser": "Hi {username},"
|
||||
"ifItWasNotYou": "If this wasn't you, please ignore this.",
|
||||
"helloUser": "Hi {username},",
|
||||
"reason": "Reason"
|
||||
},
|
||||
"userCreated": {
|
||||
"name": "User creation",
|
||||
@@ -32,8 +33,17 @@
|
||||
"userDeleted": {
|
||||
"name": "User deletion",
|
||||
"title": "Your account was deleted - Jellyfin",
|
||||
"yourAccountWasDeleted": "Your Jellyfin account was deleted.",
|
||||
"reason": "Reason"
|
||||
"yourAccountWasDeleted": "Your Jellyfin account was deleted."
|
||||
},
|
||||
"userDisabled": {
|
||||
"name": "User disabled",
|
||||
"title": "Your account has been disabled - Jellyfin",
|
||||
"yourAccountWasDisabled": "Your account was disabled."
|
||||
},
|
||||
"userEnabled": {
|
||||
"name": "User enabled",
|
||||
"title": "Your account has been re-enabled - Jellyfin",
|
||||
"yourAccountWasEnabled": "Your account was re-enabled."
|
||||
},
|
||||
"inviteEmail": {
|
||||
"name": "Invite email",
|
||||
@@ -45,10 +55,11 @@
|
||||
"linkButton": "Setup your account"
|
||||
},
|
||||
"welcomeEmail": {
|
||||
"name": "Welcome email",
|
||||
"name": "Welcome",
|
||||
"title": "Welcome to Jellyfin",
|
||||
"welcome": "Welcome to Jellyfin!",
|
||||
"youCanLoginWith": "You can login with the details below",
|
||||
"yourAccountWillExpire": "Your account will expire on {date}.",
|
||||
"jellyfinURL": "URL"
|
||||
},
|
||||
"emailConfirmation": {
|
||||
|
||||
77
lang/email/es-es.json
Normal file
77
lang/email/es-es.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Español(ES)"
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Si no fue usted, ignore este correo electrónico.",
|
||||
"helloUser": "Hola {username},",
|
||||
"reason": "Razón"
|
||||
},
|
||||
"userCreated": {
|
||||
"name": "Creación de usuarios",
|
||||
"title": "Noticia: Usuario creado",
|
||||
"aUserWasCreated": "Se creó un usuario con el código {code}.",
|
||||
"time": "Hora",
|
||||
"notificationNotice": "Nota: los correos electrónicos de notificación se pueden alternar en el panel de administración."
|
||||
},
|
||||
"inviteExpiry": {
|
||||
"name": "Vencimiento de la invitación",
|
||||
"title": "Aviso: Invitación caducada",
|
||||
"inviteExpired": "Invitación caducada.",
|
||||
"expiredAt": "El código {code} venció a las {time}.",
|
||||
"notificationNotice": "Nota: Los correos electrónicos de notificación se pueden alternar en el panel de administración."
|
||||
},
|
||||
"passwordReset": {
|
||||
"name": "Restablecimiento de contraseña",
|
||||
"title": "Solicitud de restablecimiento de contraseña - Jellyfin",
|
||||
"someoneHasRequestedReset": "Alguien ha solicitado recientemente un restablecimiento de contraseña en Jellyfin.",
|
||||
"ifItWasYou": "Si era usted, ingrese el pin a continuación en el mensaje.",
|
||||
"ifItWasYouLink": "Si fue usted, haga clic en el enlace de abajo.",
|
||||
"codeExpiry": "El código vencerá el {date}, a las {time} UTC, que está en {expiresInMinutes}.",
|
||||
"pin": "PIN"
|
||||
},
|
||||
"userDeleted": {
|
||||
"name": "Eliminación de usuario",
|
||||
"title": "Su cuenta fue eliminada - Jellyfin",
|
||||
"yourAccountWasDeleted": "Su cuenta de Jellyfin fue eliminada."
|
||||
},
|
||||
"userDisabled": {
|
||||
"name": "Usuario deshabilitado",
|
||||
"title": "Su cuenta ha sido deshabilitada - Jellyfin",
|
||||
"yourAccountWasDisabled": "Su cuenta fue inhabilitada."
|
||||
},
|
||||
"userEnabled": {
|
||||
"name": "Usuario habilitado",
|
||||
"title": "Su cuenta ha sido reactivada - Jellyfin",
|
||||
"yourAccountWasEnabled": "Su cuenta se volvió a habilitar."
|
||||
},
|
||||
"inviteEmail": {
|
||||
"name": "Correo electrónico",
|
||||
"title": "Invitar - Jellyfin",
|
||||
"hello": "Hola",
|
||||
"youHaveBeenInvited": "Has sido invitado a Jellyfin.",
|
||||
"toJoin": "Para unirse, siga el enlace a continuación.",
|
||||
"inviteExpiry": "Esta invitación vencerá el {date} a las {time}, que está en {expiresInMinutes}, así que regístrese cuanto antes.",
|
||||
"linkButton": "Configurar tu cuenta"
|
||||
},
|
||||
"welcomeEmail": {
|
||||
"name": "Correo de bienvenida",
|
||||
"title": "Bienvenido a Jellyfin",
|
||||
"welcome": "¡Bienvenido a Jellyfin!",
|
||||
"youCanLoginWith": "Puede iniciar sesión con los detalles a continuación",
|
||||
"yourAccountWillExpire": "Su cuenta vencerá el {date}.",
|
||||
"jellyfinURL": "URL"
|
||||
},
|
||||
"emailConfirmation": {
|
||||
"name": "Email de confirmación",
|
||||
"title": "Confirma tu correo electrónico - Jellyfin",
|
||||
"clickBelow": "Haga clic en el enlace de abajo para confirmar su dirección de correo electrónico y comenzar a usar Jellyfin.",
|
||||
"confirmEmail": "Confirmar correo electrónico"
|
||||
},
|
||||
"userExpired": {
|
||||
"name": "Caducidad del usuario",
|
||||
"title": "Tu cuenta ha caducado - Jellyfin",
|
||||
"yourAccountHasExpired": "Tu cuenta ha expirado.",
|
||||
"contactTheAdmin": "Comuníquese con el administrador para obtener más información."
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Si ce n'était pas toi, tu peux ignorer ce mail.",
|
||||
"reason": "Motif",
|
||||
"helloUser": "Salut {username},"
|
||||
},
|
||||
"userCreated": {
|
||||
@@ -27,12 +28,12 @@
|
||||
"ifItWasYou": "Si c'était bien toi, renseigne le code PIN en dessous.",
|
||||
"codeExpiry": "Ce code expirera le {date}, à {time} UTC, soit dans {expiresInMinutes}.",
|
||||
"pin": "PIN",
|
||||
"name": "Réinitialisation du mot de passe"
|
||||
"name": "Réinitialisation du mot de passe",
|
||||
"ifItWasYouLink": "Si c'était bien toi, clique sur le lien en dessous."
|
||||
},
|
||||
"userDeleted": {
|
||||
"title": "Ton compte a été désactivé - Jellyfin",
|
||||
"yourAccountWasDeleted": "Ton compte Jellyfin a été supprimé.",
|
||||
"reason": "Motif",
|
||||
"name": "Suppression de l'utilisateur"
|
||||
},
|
||||
"inviteEmail": {
|
||||
@@ -40,7 +41,7 @@
|
||||
"hello": "Salut",
|
||||
"youHaveBeenInvited": "Tu as été invité à rejoindre Jellyfin.",
|
||||
"toJoin": "Pour continuer, suis le lien en dessous.",
|
||||
"inviteExpiry": "L'invitation expirera le {date}, à {time}, soit dans {expiresInMinutes}, alors fais vite !",
|
||||
"inviteExpiry": "L'invitation expirera le {date}, à {time}, soit dans {expiresInMinutes}, alors fais vite.",
|
||||
"linkButton": "Lien",
|
||||
"name": "Courriel d'invitation"
|
||||
},
|
||||
@@ -49,12 +50,29 @@
|
||||
"title": "Bienvenue sur Jellyfin",
|
||||
"welcome": "Bienvenue sur Jellyfin !",
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Courriel de bienvenue"
|
||||
"name": "Courriel de bienvenue",
|
||||
"yourAccountWillExpire": "Ton compte expirera le {date}."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
"title": "Confirmez votre adresse e-mail - Jellyfin",
|
||||
"clickBelow": "Clique sur le lien ci-dessous pour confirmer ton adresse e-mail et commencer à utiliser Jellyfin.",
|
||||
"confirmEmail": "Confirmer l'adresse e-mail",
|
||||
"name": "Email de confirmation"
|
||||
},
|
||||
"userExpired": {
|
||||
"contactTheAdmin": "Contacte l'administrateur pour plus d'informations.",
|
||||
"name": "Utilisateur expiré",
|
||||
"title": "Ton compte a expiré - Jellyfin",
|
||||
"yourAccountHasExpired": "Ton compte a expiré."
|
||||
},
|
||||
"userDisabled": {
|
||||
"name": "Utilisateur désactivé",
|
||||
"title": "Ton compte a été désactivé - Jellyfin",
|
||||
"yourAccountWasDisabled": "Ton compte a été désactivé."
|
||||
},
|
||||
"userEnabled": {
|
||||
"name": "Utilisateur activé",
|
||||
"title": "Ton compte a été ré-activé - Jellyfin",
|
||||
"yourAccountWasEnabled": "Ton compte a été ré-activé."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Jika ini bukan kamu, silahkan mengabaikan email ini.",
|
||||
"reason": "Alasan",
|
||||
"helloUser": "Halo {username},"
|
||||
},
|
||||
"userCreated": {
|
||||
@@ -31,7 +32,6 @@
|
||||
"userDeleted": {
|
||||
"title": "Akun anda telah dihapus - Jellyfin",
|
||||
"yourAccountWasDeleted": "Akun Jellyfin anda telah dihapus.",
|
||||
"reason": "Alasan",
|
||||
"name": "Penghapusan pengguna"
|
||||
},
|
||||
"inviteEmail": {
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Se non sei stato tu, puoi ignorare questa email.",
|
||||
"helloUser": "Ciao {username},"
|
||||
"helloUser": "Ciao {username},",
|
||||
"reason": "Motivo"
|
||||
},
|
||||
"userCreated": {
|
||||
"title": "Nota: Utente creato",
|
||||
@@ -27,8 +28,7 @@
|
||||
},
|
||||
"userDeleted": {
|
||||
"title": "Il tuo account è stato eliminato - Jellyfin",
|
||||
"yourAccountWasDeleted": "Il tuo account di Jellyfin è stato eliminato.",
|
||||
"reason": "Motivo"
|
||||
"yourAccountWasDeleted": "Il tuo account di Jellyfin è stato eliminato."
|
||||
},
|
||||
"inviteEmail": {
|
||||
"title": "Invita - Jellyfin",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Als jij dit niet was, negeer dan alsjeblieft deze email.",
|
||||
"reason": "Reden",
|
||||
"helloUser": "Hoi {username},"
|
||||
},
|
||||
"userCreated": {
|
||||
@@ -32,7 +33,6 @@
|
||||
"userDeleted": {
|
||||
"title": "Je account is verwijderd - Jellyfin",
|
||||
"yourAccountWasDeleted": "Je Jellyfin account is verwijderd.",
|
||||
"reason": "Reden",
|
||||
"name": "Gebruiker verwijderd"
|
||||
},
|
||||
"inviteEmail": {
|
||||
@@ -49,7 +49,8 @@
|
||||
"welcome": "Welkom bij Jellyfin!",
|
||||
"youCanLoginWith": "Je kunt inloggen met onderstaande gegevens",
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Welkomste-mail"
|
||||
"name": "Welkomste-mail",
|
||||
"yourAccountWillExpire": "Je account verloopt op {date}."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
"title": "Bevestig je e-mailadres - Jellyfin",
|
||||
@@ -62,5 +63,15 @@
|
||||
"title": "Je account is verlopen - Jellyfin",
|
||||
"yourAccountHasExpired": "Je account is verlopen.",
|
||||
"contactTheAdmin": "Neem contact op met de beheerder voor meer info."
|
||||
},
|
||||
"userDisabled": {
|
||||
"title": "Je account is uitgeschakeld - Jellyfin",
|
||||
"name": "Gebruiker uitgeschakeld",
|
||||
"yourAccountWasDisabled": "Je account is uitgeschakeld."
|
||||
},
|
||||
"userEnabled": {
|
||||
"yourAccountWasEnabled": "Je account is opnieuw ingeschakeld.",
|
||||
"name": "Gebruiker ingeschakeld",
|
||||
"title": "Je account is opnieuw ingeschakeld - Jellyfin"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Se não foi você, ignore este e-mail.",
|
||||
"reason": "Razão",
|
||||
"helloUser": "Ola {username},"
|
||||
},
|
||||
"userCreated": {
|
||||
@@ -32,7 +33,6 @@
|
||||
"userDeleted": {
|
||||
"title": "Sua conta foi excluída - Jellyfin",
|
||||
"yourAccountWasDeleted": "Sua conta Jellyfin foi excluída.",
|
||||
"reason": "Razão",
|
||||
"name": "Exclusão do usuário"
|
||||
},
|
||||
"inviteEmail": {
|
||||
@@ -49,7 +49,8 @@
|
||||
"welcome": "Bem vindo ao Jellyfin!",
|
||||
"youCanLoginWith": "Abaixo está os detalhes para fazer o login",
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Email de Boas vindas"
|
||||
"name": "Email de Boas vindas",
|
||||
"yourAccountWillExpire": "Sua conta irá expirar em {date}."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
"title": "Confirme seu email - Jellyfin",
|
||||
@@ -62,5 +63,15 @@
|
||||
"title": "Sua conta expirou - Jellyfin",
|
||||
"yourAccountHasExpired": "Sua conta expirou.",
|
||||
"contactTheAdmin": "Entre em contato com administrador para mais informações."
|
||||
},
|
||||
"userDisabled": {
|
||||
"name": "Usuário desativado",
|
||||
"title": "Sua conta foi desativada - Jellyfin",
|
||||
"yourAccountWasDisabled": "Sua conta foi desativada."
|
||||
},
|
||||
"userEnabled": {
|
||||
"title": "Sua conta foi reativada - Jellyfin",
|
||||
"name": "Usuário ativado",
|
||||
"yourAccountWasEnabled": "Sua conta foi reativada."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Om detta inte var du, ignorera det här e-postmeddelandet.",
|
||||
"helloUser": "Hej {användarnamn},"
|
||||
"helloUser": "Hej {username},",
|
||||
"reason": "Anledning"
|
||||
},
|
||||
"userCreated": {
|
||||
"name": "Användarskapande",
|
||||
@@ -31,8 +32,7 @@
|
||||
"userDeleted": {
|
||||
"name": "Radering av användare",
|
||||
"title": "Ditt konto raderades - Jellyfin",
|
||||
"yourAccountWasDeleted": "Ditt Jellyfin-konto raderades.",
|
||||
"reason": "Anledning"
|
||||
"yourAccountWasDeleted": "Ditt Jellyfin-konto raderades."
|
||||
},
|
||||
"inviteEmail": {
|
||||
"name": "Inbjudnings e-post",
|
||||
|
||||
@@ -17,11 +17,15 @@
|
||||
"successContinueButton": "Continue",
|
||||
"confirmationRequired": "Email confirmation required",
|
||||
"confirmationRequiredMessage": "Please check your email inbox to verify your address.",
|
||||
"yourAccountIsValidUntil": "Your account will be valid until {date}."
|
||||
"yourAccountIsValidUntil": "Your account will be valid until {date}.",
|
||||
"sendPIN": "Send the PIN below to the bot, then come back here to link your account."
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "User already exists.",
|
||||
"errorInvalidCode": "Invalid invite code."
|
||||
"errorInvalidCode": "Invalid invite code.",
|
||||
"errorTelegramVerification": "Telegram verification required.",
|
||||
"errorInvalidPIN": "Telegram PIN is invalid.",
|
||||
"telegramVerified": "Telegram account verified."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
|
||||
48
lang/form/es-es.json
Normal file
48
lang/form/es-es.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Español (ES)"
|
||||
},
|
||||
"strings": {
|
||||
"pageTitle": "Crear cuenta de Jellyfin",
|
||||
"createAccountHeader": "Crear una cuenta",
|
||||
"accountDetails": "Detalles",
|
||||
"emailAddress": "Correo electrónico",
|
||||
"username": "Nombre de usuario",
|
||||
"password": "Contraseña",
|
||||
"reEnterPassword": "Rescriba su contraseña",
|
||||
"reEnterPasswordInvalid": "Las contraseñas no son coincidentes.",
|
||||
"createAccountButton": "Crear una cuenta",
|
||||
"passwordRequirementsHeader": "Requisitos de contraseña",
|
||||
"successHeader": "¡Éxito!",
|
||||
"successContinueButton": "Continuar",
|
||||
"confirmationRequired": "Se requiere confirmación por correo electrónico",
|
||||
"confirmationRequiredMessage": "Revise la bandeja de entrada de su correo electrónico para verificar su dirección.",
|
||||
"yourAccountIsValidUntil": "Su cuenta será válida hasta el {date}."
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "El usuario ya existe.",
|
||||
"errorInvalidCode": "Código de invitación no es válido."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
"singular": "Debe tener al menos {n} carácter",
|
||||
"plural": "Debe tener al menos {n} caracteres"
|
||||
},
|
||||
"uppercase": {
|
||||
"singular": "Debe tener al menos {n} caracteres en mayúscula",
|
||||
"plural": "Debe tener al menos {n} caracteres en mayúscula"
|
||||
},
|
||||
"lowercase": {
|
||||
"singular": "Debe tener al menos {n} caracteres en minúscula",
|
||||
"plural": "Debe tener al menos {n} caracteres en minúscula"
|
||||
},
|
||||
"number": {
|
||||
"singular": "Debe tener al menos {n} número",
|
||||
"plural": "Debe tener al menos {n} números"
|
||||
},
|
||||
"special": {
|
||||
"singular": "Debe tener al menos {n} carácter especial",
|
||||
"plural": "Debe tener al menos {n} caracteres especiales"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
"resetFailed": "Password reset failed",
|
||||
"tryAgain": "Please try again.",
|
||||
"youCanLogin": "You can now log in with the below code as your password.",
|
||||
"youCanLoginOmbi": "You can now log in to Jellyfin & Ombi with the below code as your password.",
|
||||
"changeYourPassword": "Make sure to change your password after you log in."
|
||||
}
|
||||
}
|
||||
|
||||
12
lang/pwreset/es-es.json
Normal file
12
lang/pwreset/es-es.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Español (ES)"
|
||||
},
|
||||
"strings": {
|
||||
"passwordReset": "Cambiar contraseña",
|
||||
"resetFailed": "Error al cambiar contraseña",
|
||||
"tryAgain": "Por favor intente nuevamente.",
|
||||
"youCanLogin": "Ahora puedes logearte con el codigo como contraseña.",
|
||||
"changeYourPassword": "Recuerda cambiar tu contraseña luego de iniciar sesión."
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@
|
||||
},
|
||||
"ombi": {
|
||||
"title": "Ombi",
|
||||
"description": "By connecting to Ombi, both a Jellyfin and Ombi account will be created when a user joins through jfa-go. After setup if finished, go to Settings to set a default profile for new ombi users.",
|
||||
"description": "By connecting to Ombi, both a Jellyfin and Ombi account will be created when a user joins through jfa-go. After setup is finished, go to Settings to set a default profile for new ombi users.",
|
||||
"apiKeyNotice": "Find this in the first tab of Ombi settings."
|
||||
},
|
||||
"email": {
|
||||
@@ -108,7 +108,10 @@
|
||||
"title": "Password Resets",
|
||||
"description": "When a user tries to reset their password, Jellyfin creates a file named 'passwordreset-*.json' which contains a PIN. jfa-go reads the file and sends the PIN to the user.",
|
||||
"pathToJellyfin": "Path to Jellyfin configuration directory",
|
||||
"pathToJellyfinNotice": "If you don't know where this is, try resetting your password in Jellyfin. A popup with '<path to jellyfin>/passwordreset-*.json' will appear."
|
||||
"pathToJellyfinNotice": "If you don't know where this is, try resetting your password in Jellyfin. A popup with '<path to jellyfin>/passwordreset-*.json' will appear.",
|
||||
"resetLinks": "Send a link instead of a PIN",
|
||||
"resetLinksNotice": "If Ombi integration is enabled, use this to sync Jellyfin password resets with Ombi.",
|
||||
"resetLinksLanguage": "Default reset link language"
|
||||
},
|
||||
"passwordValidation": {
|
||||
"title": "Password Validation",
|
||||
|
||||
134
lang/setup/es-es.json
Normal file
134
lang/setup/es-es.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Español(ES)"
|
||||
},
|
||||
"strings": {
|
||||
"pageTitle": "Configuración - jfa-go",
|
||||
"next": "Siguiente",
|
||||
"back": "Volver",
|
||||
"optional": "Opcional",
|
||||
"serverType": "Tipo de servidor",
|
||||
"disabled": "Desactivado",
|
||||
"enabled": "Activado",
|
||||
"port": "Puerto",
|
||||
"message": "Mensaje",
|
||||
"serverAddress": "Dirección del servidor",
|
||||
"emailSubject": "Asunto",
|
||||
"URL": "URL",
|
||||
"apiKey": "Llave de autorización (API)"
|
||||
},
|
||||
"startPage": {
|
||||
"welcome": "¡Bienvenido!",
|
||||
"pressStart": "Deberá hacer algunas cosas para configurar jfa-go. Presione comenzar para continuar.",
|
||||
"httpsNotice": "Asegúrese de acceder a esta página a través de HTTPS o bien desde una red privada.",
|
||||
"start": "Empezar"
|
||||
},
|
||||
"endPage": {
|
||||
"finished": "¡Terminado!",
|
||||
"restartMessage": "Hay más opciones que puede configurar en la página de administración. Haga clic a continuación para reiniciar, luego actualice la página.",
|
||||
"refreshPage": "Actualizar"
|
||||
},
|
||||
"language": {
|
||||
"title": "Lenguaje",
|
||||
"description": "Las traducciones de la comunidad están disponibles para la mayor parte de jfa-go. Puede elegir los idiomas predeterminados a continuación, pero los usuarios aún pueden cambiarlo si lo desean. Si quieres ayudar a traducir, regístrate en {n} para empezar a contribuir!",
|
||||
"defaultAdminLang": "Idioma de administrador predeterminado",
|
||||
"defaultFormLang": "Idioma de creación de cuenta predeterminado",
|
||||
"defaultEmailLang": "Idioma de correo electrónico predeterminado"
|
||||
},
|
||||
"general": {
|
||||
"title": "General",
|
||||
"listenAddress": "Dirección de recibidor (Listen Address)",
|
||||
"urlBase": "Base de URL",
|
||||
"urlBaseNotice": "Solo es necesario si se usa un proxy inverso en un subdominio (por ejemplo, 'jellyf.in/accounts').",
|
||||
"lightTheme": "Claro",
|
||||
"darkTheme": "Oscuro",
|
||||
"useHTTPS": "Usar HTTPS (Conexión segura SSL)",
|
||||
"httpsPort": "Puerto HTTPS",
|
||||
"useHTTPSNotice": "Solo se recomienda si no está utilizando un proxy inverso.",
|
||||
"pathToCertificate": "Ruta al certificado",
|
||||
"pathToKeyFile": "Ruta al archivo de claves"
|
||||
},
|
||||
"updates": {
|
||||
"title": "Actualizaciones",
|
||||
"description": "Habilite para recibir notificaciones cuando haya nuevas actualizaciones disponibles. jfa-go comprobará {n} cada 30 minutos. No se recopilan IP ni información de identificación personal.",
|
||||
"updateChannel": "Actualizar canal",
|
||||
"stable": "Estable",
|
||||
"unstable": "Inestable"
|
||||
},
|
||||
"login": {
|
||||
"title": "Iniciar sesión",
|
||||
"description": "Para acceder a la página de administración, debe iniciar sesión con un método a continuación:",
|
||||
"authorizeWithJellyfin": "Autorizar con Jellyfin/Emby: los detalles de inicio de sesión se comparten con Jellyfin, lo que permite a varios usuarios.",
|
||||
"authorizeManual": "Nombre de usuario y contraseña: establezca manualmente el nombre de usuario y la contraseña.",
|
||||
"adminOnly": "Solo usuarios administradores (recomendado)",
|
||||
"emailNotice": "Su dirección de correo electrónico se puede utilizar para recibir notificaciones."
|
||||
},
|
||||
"jellyfinEmby": {
|
||||
"title": "Jellyfin/Emby",
|
||||
"description": "Se necesita una cuenta de administrador porque la API no permite la creación de usuarios mediante una clave de API. Debe crear una cuenta separada y marcar 'Permitir que este usuario administre el servidor'. Puede desactivar todo lo demás. Una vez hecho esto, ingrese los detalles de inicio de sesión aquí.",
|
||||
"embyNotice": "El soporte de Emby es limitado y no admite el restablecimiento de contraseñas.",
|
||||
"internal": "Interno",
|
||||
"external": "Externo",
|
||||
"replaceJellyfin": "Nombre del servidor",
|
||||
"replaceJellyfinNotice": "Si se proporciona, reemplazará cualquier aparición de 'Jellyfin' en la aplicación.",
|
||||
"addressExternalNotice": "Déjelo en blanco para usar la misma dirección.",
|
||||
"testConnection": "Probar conexión"
|
||||
},
|
||||
"ombi": {
|
||||
"title": "Ombi.",
|
||||
"description": "Al conectarse a Ombi, se creará una cuenta de Jellyfin y Ombi cuando un usuario se una a través de jfa-go. Una vez finalizada la configuración, vaya a Configuración para establecer un perfil predeterminado para los nuevos usuarios de ombi.",
|
||||
"apiKeyNotice": "Encuentra esto en la primera pestaña de la configuración de Ombi."
|
||||
},
|
||||
"email": {
|
||||
"title": "Correo electrónico",
|
||||
"description": "jfa-go puede enviar PIN de restablecimiento de contraseña y varias notificaciones por correo electrónico. Puede conectarse a un servidor SMTP o utilizar la {n} API.",
|
||||
"method": "Método de envío",
|
||||
"useEmailAsUsername": "Utilice direcciones de correo electrónico como nombre de usuario",
|
||||
"useEmailAsUsernameNotice": "Si está habilitado, los nuevos usuarios iniciarán sesión en Jellyfin / Emby con su dirección de correo electrónico en lugar de un nombre de usuario.",
|
||||
"fromAddress": "Dirección de envío",
|
||||
"senderName": "Nombre del remitente",
|
||||
"dateFormat": "Formato de fecha",
|
||||
"dateFormatNotice": "La fecha sigue el formato strftime. Para obtener más información, visite {n}.",
|
||||
"encryption": "Cifrado",
|
||||
"mailgunApiURL": "URL de API"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notificaciones",
|
||||
"description": "Si está habilitado, puede elegir (por invitación) recibir un correo electrónico cuando una invitación caduque o se cree un usuario. Si no eligió el método de inicio de sesión de Jellyfin, asegúrese de proporcionar su dirección de correo electrónico."
|
||||
},
|
||||
"welcomeEmails": {
|
||||
"title": "Correos de bienvenida",
|
||||
"description": "Si está habilitado, se enviará un correo electrónico a los nuevos usuarios con la URL de Jellyfin/Emby y su nombre de usuario."
|
||||
},
|
||||
"inviteEmails": {
|
||||
"title": "Correos de invitación",
|
||||
"description": "Si está habilitado, puede enviar invitaciones directamente a la dirección de correo electrónico de un usuario. Debido a que es posible que esté utilizando un proxy inverso, debe proporcionar la URL desde la que se accede a las invitaciones. Escriba su base de URL y agregue '/ invite'."
|
||||
},
|
||||
"passwordResets": {
|
||||
"title": "Restablecimiento de contraseña",
|
||||
"description": "Cuando un usuario intenta restablecer su contraseña, Jellyfin crea un archivo llamado 'passwordreset - *. Json' que contiene un PIN. jfa-go lee el archivo y envía el PIN al usuario.",
|
||||
"pathToJellyfin": "Ruta al directorio de configuración de Jellyfin",
|
||||
"pathToJellyfinNotice": "Si no sabe dónde está, intente restablecer su contraseña en Jellyfin. Aparecerá una ventana emergente con '<ruta a jellyfin>/passwordreset-. Json'."
|
||||
},
|
||||
"passwordValidation": {
|
||||
"title": "Validación de contraseña",
|
||||
"description": "Si está habilitado, se mostrará un conjunto de requisitos de contraseña en la página de creación de la cuenta, como la longitud mínima, caracteres en mayúsculas/minúsculas, etc.",
|
||||
"length": "Largo",
|
||||
"uppercase": "Letras mayúsculas",
|
||||
"lowercase": "Caracteres en minúscula",
|
||||
"numbers": "Números",
|
||||
"special": "Caracteres especiales (%, *, etc.)"
|
||||
},
|
||||
"helpMessages": {
|
||||
"title": "Mensajes de ayuda",
|
||||
"description": "Estos mensajes se mostrarán en la página de creación de la cuenta y en algunos correos electrónicos.",
|
||||
"contactMessage": "Mensaje de contacto",
|
||||
"contactMessageNotice": "Aparece en la parte inferior de todas las páginas excepto admin.",
|
||||
"helpMessage": "Mensaje de ayuda",
|
||||
"helpMessageNotice": "Aparece en la página de creación de cuenta.",
|
||||
"successMessage": "Mensaje de éxito",
|
||||
"successMessageNotice": "Se muestra cuando un usuario crea su cuenta.",
|
||||
"emailMessage": "Mensaje de correo electrónico",
|
||||
"emailMessageNotice": "Aparece en la parte inferior de los correos electrónicos."
|
||||
}
|
||||
}
|
||||
5
lang/telegram/en-gb.json
Normal file
5
lang/telegram/en-gb.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (GB)"
|
||||
}
|
||||
}
|
||||
11
lang/telegram/en-us.json
Normal file
11
lang/telegram/en-us.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (US)"
|
||||
},
|
||||
"strings": {
|
||||
"startMessage": "Hi!\nEnter your Jellyfin PIN code here to verify your account.",
|
||||
"invalidPIN": "That PIN was invalid, try again.",
|
||||
"pinSuccess": "Success! You can now return to the sign-up page.",
|
||||
"languageMessage": "Note: See available languages with /lang, and set language with /lang <language code>."
|
||||
}
|
||||
}
|
||||
5
logger/go.mod
Normal file
5
logger/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/hrfee/jfa-go/logger
|
||||
|
||||
go 1.16
|
||||
|
||||
require github.com/fatih/color v1.10.0
|
||||
9
logger/go.sum
Normal file
9
logger/go.sum
Normal file
@@ -0,0 +1,9 @@
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1,4 +1,5 @@
|
||||
package main
|
||||
// Package logger provides a wrapper around log that adds color support with github.com/fatih/color.
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -99,10 +100,10 @@ func (l logger) Fatalf(format string, v ...interface{}) {
|
||||
l.logger.Fatal(out)
|
||||
}
|
||||
|
||||
type emptyLogger bool
|
||||
type EmptyLogger bool
|
||||
|
||||
func (l emptyLogger) Printf(format string, v ...interface{}) {}
|
||||
func (l emptyLogger) Print(v ...interface{}) {}
|
||||
func (l emptyLogger) Println(v ...interface{}) {}
|
||||
func (l emptyLogger) Fatal(v ...interface{}) {}
|
||||
func (l emptyLogger) Fatalf(format string, v ...interface{}) {}
|
||||
func (l EmptyLogger) Printf(format string, v ...interface{}) {}
|
||||
func (l EmptyLogger) Print(v ...interface{}) {}
|
||||
func (l EmptyLogger) Println(v ...interface{}) {}
|
||||
func (l EmptyLogger) Fatal(v ...interface{}) {}
|
||||
func (l EmptyLogger) Fatalf(format string, v ...interface{}) {}
|
||||
@@ -60,7 +60,7 @@
|
||||
<mj-section mj-class="bg">
|
||||
<mj-column>
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<h3>{{ .yourAccountWasDeleted }}</h3>
|
||||
<h3>{{ .yourAccountWas }}</h3>
|
||||
<p>{{ .reasonString }}: <i>{{ .reason }}</i></p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{ .yourAccountWasDeleted }}
|
||||
{{ .yourAccountWas }}
|
||||
|
||||
{{ .reasonString }}: {{ .reason }}
|
||||
|
||||
|
||||
@@ -62,8 +62,9 @@
|
||||
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
|
||||
<h3>{{ .welcome }}</h3>
|
||||
<p>{{ .youCanLoginWith }}:</p>
|
||||
{{ .jellyfinURLString }}: <a href="{{ .jellyfinURLVal }}">{{ .jellyfinURL }}</a>
|
||||
{{ .jellyfinURLString }}: <a href="{{ .jellyfinURL }}">{{ .jellyfinURL }}</a>
|
||||
<p>{{ .usernameString }}: <i>{{ .username }}</i></p>
|
||||
<p>{{ .yourAccountWillExpire }}</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
|
||||
{{ .usernameString }}: {{ .username }}
|
||||
|
||||
{{ .yourAccountWillExpire }}
|
||||
|
||||
{{ .message }}
|
||||
|
||||
138
main.go
138
main.go
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
@@ -24,6 +23,7 @@ import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
_ "github.com/hrfee/jfa-go/docs"
|
||||
"github.com/hrfee/jfa-go/logger"
|
||||
"github.com/hrfee/jfa-go/ombi"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
@@ -93,7 +93,8 @@ type appContext struct {
|
||||
storage Storage
|
||||
validator Validator
|
||||
email *Emailer
|
||||
info, debug, err Logger
|
||||
telegram *TelegramDaemon
|
||||
info, debug, err logger.Logger
|
||||
host string
|
||||
port int
|
||||
version string
|
||||
@@ -165,60 +166,10 @@ func start(asDaemon, firstCall bool) {
|
||||
fs: localFS,
|
||||
}
|
||||
|
||||
app.info = NewLogger(os.Stdout, "[INFO] ", log.Ltime, color.FgHiWhite)
|
||||
app.err = NewLogger(os.Stdout, "[ERROR] ", log.Ltime, color.FgRed)
|
||||
app.info = logger.NewLogger(os.Stdout, "[INFO] ", log.Ltime, color.FgHiWhite)
|
||||
app.err = logger.NewLogger(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile, color.FgRed)
|
||||
|
||||
if firstCall {
|
||||
DATA = flag.String("data", app.dataPath, "alternate path to data directory.")
|
||||
CONFIG = flag.String("config", app.configPath, "alternate path to config file.")
|
||||
HOST = flag.String("host", "", "alternate address to host web ui on.")
|
||||
PORT = flag.Int("port", 0, "alternate port to host web ui on.")
|
||||
DEBUG = flag.Bool("debug", false, "Enables debug logging.")
|
||||
PPROF = flag.Bool("pprof", false, "Exposes pprof profiler on /debug/pprof.")
|
||||
SWAGGER = flag.Bool("swagger", false, "Enable swagger at /swagger/index.html")
|
||||
|
||||
flag.Parse()
|
||||
if *SWAGGER {
|
||||
os.Setenv("SWAGGER", "1")
|
||||
}
|
||||
if *DEBUG {
|
||||
os.Setenv("DEBUG", "1")
|
||||
}
|
||||
if *PPROF {
|
||||
os.Setenv("PPROF", "1")
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("SWAGGER") == "1" {
|
||||
*SWAGGER = true
|
||||
}
|
||||
if os.Getenv("DEBUG") == "1" {
|
||||
*DEBUG = true
|
||||
}
|
||||
if os.Getenv("PPROF") == "1" {
|
||||
*PPROF = true
|
||||
}
|
||||
// attempt to apply command line flags correctly
|
||||
if app.configPath == *CONFIG && app.dataPath != *DATA {
|
||||
app.dataPath = *DATA
|
||||
app.configPath = filepath.Join(app.dataPath, "config.ini")
|
||||
} else if app.configPath != *CONFIG && app.dataPath == *DATA {
|
||||
app.configPath = *CONFIG
|
||||
} else {
|
||||
app.configPath = *CONFIG
|
||||
app.dataPath = *DATA
|
||||
}
|
||||
|
||||
// Previously used for self-restarts but leaving them here as they might be useful.
|
||||
if v := os.Getenv("JFA_CONFIGPATH"); v != "" {
|
||||
app.configPath = v
|
||||
}
|
||||
if v := os.Getenv("JFA_DATAPATH"); v != "" {
|
||||
app.dataPath = v
|
||||
}
|
||||
|
||||
os.Setenv("JFA_CONFIGPATH", app.configPath)
|
||||
os.Setenv("JFA_DATAPATH", app.dataPath)
|
||||
app.loadArgs(firstCall)
|
||||
|
||||
var firstRun bool
|
||||
if _, err := os.Stat(app.dataPath); os.IsNotExist(err) {
|
||||
@@ -245,9 +196,15 @@ func start(asDaemon, firstCall bool) {
|
||||
|
||||
var debugMode bool
|
||||
var address string
|
||||
if app.loadConfig() != nil {
|
||||
app.err.Fatalf("Failed to load config file \"%s\"", app.configPath)
|
||||
if err := app.loadConfig(); err != nil {
|
||||
app.err.Fatalf("Failed to load config file \"%s\": %v", app.configPath, err)
|
||||
}
|
||||
|
||||
// Some message settings have been moved from "email" to "messages", this will switch them.
|
||||
if app.config.Section("email").Key("use_24h").Value() != "" {
|
||||
app.migrateEmailConfig()
|
||||
}
|
||||
|
||||
app.version = app.config.Section("jellyfin").Key("version").String()
|
||||
// read from config...
|
||||
debugMode = app.config.Section("ui").Key("debug").MustBool(false)
|
||||
@@ -256,9 +213,9 @@ func start(asDaemon, firstCall bool) {
|
||||
debugMode = true
|
||||
}
|
||||
if debugMode {
|
||||
app.debug = NewLogger(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile, color.FgYellow)
|
||||
app.debug = logger.NewLogger(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile, color.FgYellow)
|
||||
} else {
|
||||
app.debug = emptyLogger(false)
|
||||
app.debug = logger.EmptyLogger(false)
|
||||
}
|
||||
if *PPROF {
|
||||
app.info.Print(warning("\n\nWARNING: Don't use pprof in production.\n\n"))
|
||||
@@ -267,9 +224,8 @@ func start(asDaemon, firstCall bool) {
|
||||
// Starts listener to receive commands over a unix socket. Use with 'jfa-go start/stop'
|
||||
if asDaemon {
|
||||
go func() {
|
||||
socket := SOCK
|
||||
os.Remove(socket)
|
||||
listener, err := net.Listen("unix", socket)
|
||||
os.Remove(SOCK)
|
||||
listener, err := net.Listen("unix", SOCK)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Couldn't establish socket connection at %s\n", SOCK)
|
||||
}
|
||||
@@ -277,7 +233,7 @@ func start(asDaemon, firstCall bool) {
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
<-c
|
||||
os.Remove(socket)
|
||||
os.Remove(SOCK)
|
||||
os.Exit(1)
|
||||
}()
|
||||
defer func() {
|
||||
@@ -287,13 +243,13 @@ func start(asDaemon, firstCall bool) {
|
||||
for {
|
||||
con, err := listener.Accept()
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't read message on %s: %s", socket, err)
|
||||
app.err.Printf("Couldn't read message on %s: %s", SOCK, err)
|
||||
continue
|
||||
}
|
||||
buf := make([]byte, 512)
|
||||
nr, err := con.Read(buf)
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't read message on %s: %s", socket, err)
|
||||
app.err.Printf("Couldn't read message on %s: %s", SOCK, err)
|
||||
continue
|
||||
}
|
||||
command := string(buf[0:nr])
|
||||
@@ -308,6 +264,7 @@ func start(asDaemon, firstCall bool) {
|
||||
app.storage.lang.FormPath = "form"
|
||||
app.storage.lang.AdminPath = "admin"
|
||||
app.storage.lang.EmailPath = "email"
|
||||
app.storage.lang.TelegramPath = "telegram"
|
||||
app.storage.lang.PasswordResetPath = "pwreset"
|
||||
externalLang := app.config.Section("files").Key("lang_files").MustString("")
|
||||
var err error
|
||||
@@ -376,9 +333,14 @@ func start(asDaemon, firstCall bool) {
|
||||
if err := app.storage.loadUsers(); err != nil {
|
||||
app.err.Printf("Failed to load Users: %v", err)
|
||||
}
|
||||
app.storage.telegram_path = app.config.Section("files").Key("telegram_users").String()
|
||||
if err := app.storage.loadTelegramUsers(); err != nil {
|
||||
app.err.Printf("Failed to load Telegram users: %v", err)
|
||||
}
|
||||
|
||||
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
|
||||
app.storage.loadProfiles()
|
||||
// Migrate from pre-0.2.0 user templates to profiles
|
||||
if !(app.storage.policy.BlockedTags == nil && app.storage.configuration.GroupedFolders == nil && len(app.storage.displayprefs) == 0) {
|
||||
app.info.Println("Migrating user template files to new profile format")
|
||||
app.storage.migrateToProfile()
|
||||
@@ -417,7 +379,7 @@ func start(asDaemon, firstCall bool) {
|
||||
"Jellyfin (Dark)": "dark-theme",
|
||||
"Default (Light)": "light-theme",
|
||||
}
|
||||
// For move from Bootstrap to a17t
|
||||
// For move from Bootstrap to a17t (0.2.5)
|
||||
if app.config.Section("ui").Key("theme").String() == "Bootstrap (Light)" {
|
||||
app.config.Section("ui").Key("theme").SetValue("Default (Light)")
|
||||
}
|
||||
@@ -591,6 +553,16 @@ func start(asDaemon, firstCall bool) {
|
||||
if app.config.Section("updates").Key("enabled").MustBool(false) {
|
||||
go app.checkForUpdates()
|
||||
}
|
||||
|
||||
if telegramEnabled {
|
||||
app.telegram, err = newTelegramDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to authenticate with Telegram: %v", err)
|
||||
} else {
|
||||
go app.telegram.run()
|
||||
defer app.telegram.Shutdown()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugMode = false
|
||||
address = "0.0.0.0:8056"
|
||||
@@ -739,6 +711,40 @@ func main() {
|
||||
fmt.Println("Sent.")
|
||||
} else if flagPassed("daemon") {
|
||||
start(true, true)
|
||||
} else if flagPassed("systemd") {
|
||||
service, err := fs.ReadFile(localFS, "jfa-go.service")
|
||||
if err != nil {
|
||||
fmt.Printf("Couldn't read jfa-go.service: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
absPath, err := filepath.Abs(os.Args[0])
|
||||
if err != nil {
|
||||
absPath = os.Args[0]
|
||||
}
|
||||
command := absPath
|
||||
for i, v := range os.Args {
|
||||
if i != 0 && v != "systemd" {
|
||||
command += " " + v
|
||||
}
|
||||
}
|
||||
service = []byte(strings.Replace(string(service), "{executable}", command, 1))
|
||||
err = os.WriteFile("jfa-go.service", service, 0666)
|
||||
if err != nil {
|
||||
fmt.Printf("Couldn't write jfa-go.service: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Print(info(`If you want to execute jfa-go with special arguments, re-run this command with them.
|
||||
Move the newly created "jfa-go.service" file to ~/.config/systemd/user (Creating it if necessary).
|
||||
Then run "systemctl --user daemon-reload".
|
||||
You can then run:
|
||||
|
||||
`))
|
||||
color.New(color.FgGreen).PrintFunc()("To start: ")
|
||||
fmt.Print(info("systemctl --user start jfa-go\n\n"))
|
||||
color.New(color.FgRed).PrintFunc()("To stop: ")
|
||||
fmt.Print(info("systemctl --user stop jfa-go\n\n"))
|
||||
color.New(color.FgYellow).PrintFunc()("To restart: ")
|
||||
fmt.Print(info("systemctl --user stop jfa-go\n"))
|
||||
} else {
|
||||
RESTART = make(chan bool, 1)
|
||||
start(false, true)
|
||||
|
||||
64
models.go
64
models.go
@@ -11,10 +11,12 @@ type boolResponse struct {
|
||||
}
|
||||
|
||||
type newUserDTO struct {
|
||||
Username string `json:"username" example:"jeff" binding:"required"` // User's username
|
||||
Password string `json:"password" example:"guest" binding:"required"` // User's password
|
||||
Email string `json:"email" example:"jeff@jellyf.in"` // User's email address
|
||||
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
|
||||
Username string `json:"username" example:"jeff" binding:"required"` // User's username
|
||||
Password string `json:"password" example:"guest" binding:"required"` // User's password
|
||||
Email string `json:"email" example:"jeff@jellyf.in"` // User's email address
|
||||
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
|
||||
TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used)
|
||||
TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs
|
||||
}
|
||||
|
||||
type newUserResponse struct {
|
||||
@@ -29,11 +31,20 @@ type deleteUserDTO struct {
|
||||
Reason string `json:"reason"` // Account deletion reason (for notification)
|
||||
}
|
||||
|
||||
type enableDisableUserDTO struct {
|
||||
Users []string `json:"users" binding:"required"` // List of usernames to delete
|
||||
Enabled bool `json:"enabled"` // True = enable users, False = disable.
|
||||
Notify bool `json:"notify"` // Whether to notify users of deletion
|
||||
Reason string `json:"reason"` // Account deletion reason (for notification)
|
||||
}
|
||||
|
||||
type generateInviteDTO struct {
|
||||
Months int `json:"months" example:"0"` // Number of months
|
||||
Days int `json:"days" example:"1"` // Number of days
|
||||
Hours int `json:"hours" example:"2"` // Number of hours
|
||||
Minutes int `json:"minutes" example:"3"` // Number of minutes
|
||||
UserExpiry bool `json:"user-expiry"` // Whether or not user expiry is enabled
|
||||
UserMonths int `json:"user-months,omitempty" example:"1"` // Number of months till user expiry
|
||||
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
|
||||
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
|
||||
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
|
||||
@@ -73,10 +84,12 @@ type newProfileDTO struct {
|
||||
|
||||
type inviteDTO struct {
|
||||
Code string `json:"code" example:"sajdlj23423j23"` // Invite code
|
||||
Months int `json:"months" example:"1"` // Number of months till expiry
|
||||
Days int `json:"days" example:"1"` // Number of days till expiry
|
||||
Hours int `json:"hours" example:"2"` // Number of hours till expiry
|
||||
Minutes int `json:"minutes" example:"3"` // Number of minutes till expiry
|
||||
UserExpiry bool `json:"user-expiry"` // Whether or not user expiry is enabled
|
||||
UserMonths int `json:"user-months,omitempty" example:"1"` // Number of months till user expiry
|
||||
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
|
||||
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
|
||||
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
|
||||
@@ -109,13 +122,15 @@ type deleteInviteDTO struct {
|
||||
}
|
||||
|
||||
type respUser struct {
|
||||
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
|
||||
Name string `json:"name" example:"jeff"` // Username of user
|
||||
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
|
||||
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
|
||||
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
|
||||
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
|
||||
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
|
||||
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
|
||||
Name string `json:"name" example:"jeff"` // Username of user
|
||||
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
|
||||
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
|
||||
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
|
||||
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
|
||||
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
|
||||
Telegram string `json:"telegram"` // Telegram username (if known)
|
||||
NotifyThroughTelegram bool `json:"notify_telegram"`
|
||||
}
|
||||
|
||||
type getUsersDTO struct {
|
||||
@@ -203,15 +218,17 @@ type emailTestDTO struct {
|
||||
}
|
||||
|
||||
type customEmailDTO struct {
|
||||
Content string `json:"content"`
|
||||
Variables []string `json:"variables"`
|
||||
Values map[string]interface{} `json:"values"`
|
||||
HTML string `json:"html"`
|
||||
Plaintext string `json:"plaintext"`
|
||||
Content string `json:"content"`
|
||||
Variables []string `json:"variables"`
|
||||
Conditionals []string `json:"conditionals"`
|
||||
Values map[string]interface{} `json:"values"`
|
||||
HTML string `json:"html"`
|
||||
Plaintext string `json:"plaintext"`
|
||||
}
|
||||
|
||||
type extendExpiryDTO struct {
|
||||
Users []string `json:"users"` // List of user IDs to apply to.
|
||||
Months int `json:"months" example:"1"` // Number of months to add.
|
||||
Days int `json:"days" example:"1"` // Number of days to add.
|
||||
Hours int `json:"hours" example:"2"` // Number of hours to add.
|
||||
Minutes int `json:"minutes" example:"3"` // Number of minutes to add.
|
||||
@@ -221,3 +238,18 @@ type checkUpdateDTO struct {
|
||||
New bool `json:"new"` // Whether or not there's a new update.
|
||||
Update Update `json:"update"`
|
||||
}
|
||||
|
||||
type telegramPinDTO struct {
|
||||
Token string `json:"token" example:"A1-B2-3C"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type telegramSetDTO struct {
|
||||
Token string `json:"token" example:"A1-B2-3C"`
|
||||
ID string `json:"id"` // Jellyfin ID of user.
|
||||
}
|
||||
|
||||
type telegramNotifyDTO struct {
|
||||
ID string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func (ombi *Ombi) ModifyUser(user map[string]interface{}) (status int, err error
|
||||
err = fmt.Errorf("No ID provided")
|
||||
return
|
||||
}
|
||||
_, status, err = ombi.put(ombi.server+"/api/v1/Identity", user, false)
|
||||
_, status, err = ombi.put(ombi.server+"/api/v1/Identity/", user, false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ts-stack/markdown": "^1.3.0",
|
||||
"@types/node": "^15.0.1",
|
||||
"a17t": "^0.4.0",
|
||||
"esbuild": "^0.8.57",
|
||||
"lodash": "^4.17.19",
|
||||
@@ -35,9 +36,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "14.14.16",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-14.14.16.tgz?cache=0&sync_timestamp=1608756036972&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-14.14.16.tgz",
|
||||
"integrity": "sha1-PMNR+NSBAd6t/tTJ5PEWBI1De0s="
|
||||
"version": "15.0.1",
|
||||
"resolved": "https://registry.nlark.com/@types/node/download/@types/node-15.0.1.tgz?cache=0&sync_timestamp=1619534647758&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-15.0.1.tgz",
|
||||
"integrity": "sha1-7zTeoIgQKNETmL5b9OhWdD49w1o="
|
||||
},
|
||||
"node_modules/a17t": {
|
||||
"version": "0.4.0",
|
||||
@@ -1835,9 +1836,9 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.14.16",
|
||||
"resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-14.14.16.tgz?cache=0&sync_timestamp=1608756036972&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-14.14.16.tgz",
|
||||
"integrity": "sha1-PMNR+NSBAd6t/tTJ5PEWBI1De0s="
|
||||
"version": "15.0.1",
|
||||
"resolved": "https://registry.nlark.com/@types/node/download/@types/node-15.0.1.tgz?cache=0&sync_timestamp=1619534647758&other_urls=https%3A%2F%2Fregistry.nlark.com%2F%40types%2Fnode%2Fdownload%2F%40types%2Fnode-15.0.1.tgz",
|
||||
"integrity": "sha1-7zTeoIgQKNETmL5b9OhWdD49w1o="
|
||||
},
|
||||
"a17t": {
|
||||
"version": "0.4.0",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"homepage": "https://github.com/hrfee/jfa-go#readme",
|
||||
"dependencies": {
|
||||
"@ts-stack/markdown": "^1.3.0",
|
||||
"@types/node": "^15.0.1",
|
||||
"a17t": "^0.4.0",
|
||||
"esbuild": "^0.8.57",
|
||||
"lodash": "^4.17.19",
|
||||
|
||||
29
pwreset.go
29
pwreset.go
@@ -69,27 +69,24 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
||||
return
|
||||
}
|
||||
app.storage.loadEmails()
|
||||
var address string
|
||||
uid := user.ID
|
||||
if uid == "" {
|
||||
app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username)
|
||||
return
|
||||
}
|
||||
addr, ok := app.storage.emails[uid]
|
||||
if !ok || addr == nil {
|
||||
app.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username)
|
||||
return
|
||||
}
|
||||
address = addr.(string)
|
||||
msg, err := app.email.constructReset(pwr, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct password reset email for %s", pwr.Username)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
} else if err := app.email.send(msg, address); err != nil {
|
||||
app.err.Printf("Failed to send password reset email to \"%s\"", address)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
} else {
|
||||
app.info.Printf("Sent password reset email to \"%s\"", address)
|
||||
name := app.getAddressOrName(uid)
|
||||
if name != "" {
|
||||
msg, err := app.email.constructReset(pwr, app, false)
|
||||
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct password reset message for %s", pwr.Username)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
} else if err := app.sendByID(msg, uid); err != nil {
|
||||
app.err.Printf("Failed to send password reset message to \"%s\"", name)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
} else {
|
||||
app.info.Printf("Sent password reset message to \"%s\"", name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry)
|
||||
|
||||
32
restart.go
Normal file
32
restart.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func (app *appContext) HardRestart() error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
signal.Notify(app.quit, os.Interrupt)
|
||||
<-app.quit
|
||||
}
|
||||
}()
|
||||
args := os.Args
|
||||
// After a single restart, args[0] gets messed up and isnt the real executable.
|
||||
// JFA_DEEP tells the new process its a child, and JFA_EXEC is the real executable
|
||||
if os.Getenv("JFA_DEEP") == "" {
|
||||
os.Setenv("JFA_DEEP", "1")
|
||||
os.Setenv("JFA_EXEC", args[0])
|
||||
}
|
||||
env := os.Environ()
|
||||
err := syscall.Exec(os.Getenv("JFA_EXEC"), []string{""}, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
panic(fmt.Errorf("r"))
|
||||
}
|
||||
7
restart_windows.go
Normal file
7
restart_windows.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (app *appContext) HardRestart() error {
|
||||
return fmt.Errorf("hard restarts not available on windows")
|
||||
}
|
||||
18
router.go
18
router.go
@@ -118,6 +118,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
router.POST(p+"/newUser", app.NewUser)
|
||||
router.Use(static.Serve(p+"/invite/", app.webFS))
|
||||
router.GET(p+"/invite/:invCode", app.InviteProxy)
|
||||
if telegramEnabled {
|
||||
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
|
||||
}
|
||||
}
|
||||
if *SWAGGER {
|
||||
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
||||
@@ -132,6 +135,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
api.GET(p+"/users", app.GetUsers)
|
||||
api.POST(p+"/users", app.NewUserAdmin)
|
||||
api.POST(p+"/users/extend", app.ExtendExpiry)
|
||||
api.POST(p+"/users/enable", app.EnableDisableUsers)
|
||||
api.POST(p+"/invites", app.GenerateInvite)
|
||||
api.GET(p+"/invites", app.GetInvites)
|
||||
api.DELETE(p+"/invites", app.DeleteInvite)
|
||||
@@ -147,13 +151,19 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
api.POST(p+"/users/announce", app.Announce)
|
||||
api.GET(p+"/config/update", app.CheckUpdate)
|
||||
api.POST(p+"/config/update", app.ApplyUpdate)
|
||||
api.GET(p+"/config/emails", app.GetEmails)
|
||||
api.GET(p+"/config/emails/:id", app.GetEmail)
|
||||
api.POST(p+"/config/emails/:id", app.SetEmail)
|
||||
api.POST(p+"/config/emails/:id/state/:state", app.SetEmailState)
|
||||
api.GET(p+"/config/emails", app.GetCustomEmails)
|
||||
api.GET(p+"/config/emails/:id", app.GetCustomEmailTemplate)
|
||||
api.POST(p+"/config/emails/:id", app.SetCustomEmail)
|
||||
api.POST(p+"/config/emails/:id/state/:state", app.SetCustomEmailState)
|
||||
api.GET(p+"/config", app.GetConfig)
|
||||
api.POST(p+"/config", app.ModifyConfig)
|
||||
api.POST(p+"/restart", app.restart)
|
||||
if telegramEnabled {
|
||||
api.GET(p+"/telegram/pin", app.TelegramGetPin)
|
||||
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
|
||||
api.POST(p+"/users/telegram", app.TelegramAddUser)
|
||||
api.POST(p+"/users/telegram/notify", app.TelegramSetNotify)
|
||||
}
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
api.GET(p+"/ombi/users", app.OmbiUsers)
|
||||
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)
|
||||
|
||||
76
setup.go
76
setup.go
@@ -29,7 +29,7 @@ func (app *appContext) ServeSetup(gc *gin.Context) {
|
||||
"success_message": app.config.Section("ui").Key("success_message").String(),
|
||||
},
|
||||
"email": {
|
||||
"message": app.config.Section("email").Key("message").String(),
|
||||
"message": app.config.Section("messages").Key("message").String(),
|
||||
},
|
||||
}
|
||||
msg, err := json.Marshal(messages)
|
||||
@@ -73,7 +73,10 @@ func (app *appContext) TestJF(gc *gin.Context) {
|
||||
func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
|
||||
st.lang.Setup = map[string]setupLang{}
|
||||
var english setupLang
|
||||
load := func(filesystem fs.FS, fname string) error {
|
||||
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 := setupLang{}
|
||||
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.SetupPath, fname))
|
||||
@@ -84,21 +87,47 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(index, &lang.Strings)
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
if fname != "en-us.json" {
|
||||
patchLang(&english.Strings, &lang.Strings)
|
||||
patchLang(&english.StartPage, &lang.StartPage)
|
||||
patchLang(&english.Updates, &lang.Updates)
|
||||
patchLang(&english.EndPage, &lang.EndPage)
|
||||
patchLang(&english.Language, &lang.Language)
|
||||
patchLang(&english.Login, &lang.Login)
|
||||
patchLang(&english.JellyfinEmby, &lang.JellyfinEmby)
|
||||
patchLang(&english.Email, &lang.Email)
|
||||
patchLang(&english.Notifications, &lang.Notifications)
|
||||
patchLang(&english.PasswordResets, &lang.PasswordResets)
|
||||
patchLang(&english.InviteEmails, &lang.InviteEmails)
|
||||
patchLang(&english.PasswordValidation, &lang.PasswordValidation)
|
||||
patchLang(&english.HelpMessages, &lang.HelpMessages)
|
||||
if lang.Meta.Fallback != "" {
|
||||
fallback, ok := st.lang.Setup[lang.Meta.Fallback]
|
||||
err = nil
|
||||
if !ok {
|
||||
err = load(fsIndex, lang.Meta.Fallback+".json")
|
||||
fallback = st.lang.Setup[lang.Meta.Fallback]
|
||||
}
|
||||
if err == nil {
|
||||
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
||||
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
||||
patchLang(&lang.StartPage, &fallback.StartPage, &english.StartPage)
|
||||
patchLang(&lang.Updates, &fallback.Updates, &english.Updates)
|
||||
patchLang(&lang.EndPage, &fallback.EndPage, &english.EndPage)
|
||||
patchLang(&lang.Language, &fallback.Language, &english.Language)
|
||||
patchLang(&lang.Login, &fallback.Login, &english.Login)
|
||||
patchLang(&lang.JellyfinEmby, &fallback.JellyfinEmby, &english.JellyfinEmby)
|
||||
patchLang(&lang.Email, &fallback.Email, &english.Email)
|
||||
patchLang(&lang.Notifications, &fallback.Notifications, &english.Notifications)
|
||||
patchLang(&lang.PasswordResets, &fallback.PasswordResets, &english.PasswordResets)
|
||||
patchLang(&lang.InviteEmails, &fallback.InviteEmails, &english.InviteEmails)
|
||||
patchLang(&lang.PasswordValidation, &fallback.PasswordValidation, &english.PasswordValidation)
|
||||
patchLang(&lang.HelpMessages, &fallback.HelpMessages, &english.HelpMessages)
|
||||
}
|
||||
}
|
||||
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
||||
patchLang(&lang.Strings, &english.Strings)
|
||||
patchLang(&lang.StartPage, &english.StartPage)
|
||||
patchLang(&lang.Updates, &english.Updates)
|
||||
patchLang(&lang.EndPage, &english.EndPage)
|
||||
patchLang(&lang.Language, &english.Language)
|
||||
patchLang(&lang.Login, &english.Login)
|
||||
patchLang(&lang.JellyfinEmby, &english.JellyfinEmby)
|
||||
patchLang(&lang.Email, &english.Email)
|
||||
patchLang(&lang.Notifications, &english.Notifications)
|
||||
patchLang(&lang.PasswordResets, &english.PasswordResets)
|
||||
patchLang(&lang.InviteEmails, &english.InviteEmails)
|
||||
patchLang(&lang.PasswordValidation, &english.PasswordValidation)
|
||||
patchLang(&lang.HelpMessages, &english.HelpMessages)
|
||||
}
|
||||
}
|
||||
stringSettings, err := json.Marshal(lang)
|
||||
if err != nil {
|
||||
@@ -110,27 +139,30 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
|
||||
}
|
||||
engFound := false
|
||||
var err error
|
||||
for _, filesystem := range filesystems {
|
||||
err = load(filesystem, "en-us.json")
|
||||
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.Setup["en-us"]
|
||||
setupLoaded := false
|
||||
for _, filesystem := range filesystems {
|
||||
files, err := fs.ReadDir(filesystem, st.lang.SetupPath)
|
||||
for i := range filesystems {
|
||||
files, err := fs.ReadDir(filesystems[i], st.lang.SetupPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range files {
|
||||
if f.Name() != "en-us.json" {
|
||||
err = load(filesystem, f.Name())
|
||||
if !loadedLangs[i][f.Name()] {
|
||||
err = load(i, f.Name())
|
||||
if err == nil {
|
||||
setupLoaded = true
|
||||
loadedLangs[i][f.Name()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
481
storage.go
481
storage.go
@@ -15,18 +15,26 @@ import (
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path string
|
||||
users map[string]time.Time
|
||||
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
|
||||
invitesLock, usersLock sync.Mutex
|
||||
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[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
|
||||
customEmails customEmails
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
lang Lang
|
||||
invitesLock, usersLock sync.Mutex
|
||||
}
|
||||
|
||||
type TelegramUser struct {
|
||||
ChatID int64
|
||||
Username string
|
||||
Lang string
|
||||
Contact bool // Whether to contact through telegram or not
|
||||
}
|
||||
|
||||
type customEmails struct {
|
||||
@@ -34,6 +42,8 @@ type customEmails struct {
|
||||
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"`
|
||||
@@ -41,9 +51,10 @@ type customEmails struct {
|
||||
}
|
||||
|
||||
type customEmail struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Content string `json:"content"`
|
||||
Variables []string `json:"variables,omitempty"`
|
||||
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
|
||||
@@ -64,6 +75,7 @@ type Invite struct {
|
||||
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"`
|
||||
@@ -77,23 +89,26 @@ type Invite struct {
|
||||
}
|
||||
|
||||
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
|
||||
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) {
|
||||
@@ -114,57 +129,93 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
||||
return
|
||||
}
|
||||
err = st.loadLangEmail(filesystems...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = st.loadLangTelegram(filesystems...)
|
||||
return
|
||||
}
|
||||
|
||||
func (common *commonLangs) patchCommon(lang string, other *langSection) {
|
||||
if *other == nil {
|
||||
*other = langSection{}
|
||||
// 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{}
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a given language has missing values, fill it in with the english value.
|
||||
func patchLang(english, other *langSection) {
|
||||
if *other == nil {
|
||||
*other = langSection{}
|
||||
func patchLang(to *langSection, from ...*langSection) {
|
||||
if *to == nil {
|
||||
*to = langSection{}
|
||||
}
|
||||
for n, ev := range *english {
|
||||
if v, ok := (*other)[n]; !ok || v == "" {
|
||||
(*other)[n] = ev
|
||||
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(english, other *map[string]quantityString) {
|
||||
if *other == nil {
|
||||
*other = map[string]quantityString{}
|
||||
func patchQuantityStrings(to *map[string]quantityString, from ...*map[string]quantityString) {
|
||||
if *to == nil {
|
||||
*to = map[string]quantityString{}
|
||||
}
|
||||
for n, ev := range *english {
|
||||
qs, ok := (*other)[n]
|
||||
if !ok {
|
||||
(*other)[n] = ev
|
||||
continue
|
||||
} else if qs.Singular == "" {
|
||||
qs.Singular = ev.Singular
|
||||
} else if (*other)[n].Plural == "" {
|
||||
qs.Plural = ev.Plural
|
||||
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
|
||||
}
|
||||
(*other)[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
|
||||
load := func(filesystem fs.FS, fname string) error {
|
||||
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))
|
||||
@@ -179,34 +230,51 @@ func (st *Storage) loadLangCommon(filesystems ...fs.FS) error {
|
||||
return err
|
||||
}
|
||||
if fname != "en-us.json" {
|
||||
patchLang(&english.Strings, &lang.Strings)
|
||||
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 _, filesystem := range filesystems {
|
||||
err = load(filesystem, "en-us.json")
|
||||
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 _, filesystem := range filesystems {
|
||||
files, err := fs.ReadDir(filesystem, st.lang.CommonPath)
|
||||
for i := range filesystems {
|
||||
files, err := fs.ReadDir(filesystems[i], st.lang.CommonPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, f := range files {
|
||||
if f.Name() != "en-us.json" {
|
||||
err = load(filesystem, f.Name())
|
||||
if !loadedLangs[i][f.Name()] {
|
||||
err = load(i, f.Name())
|
||||
if err == nil {
|
||||
commonLoaded = true
|
||||
loadedLangs[i][f.Name()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,7 +288,10 @@ func (st *Storage) loadLangCommon(filesystems ...fs.FS) error {
|
||||
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 {
|
||||
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))
|
||||
@@ -234,11 +305,27 @@ func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(index, &lang.Strings)
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
if fname != "en-us.json" {
|
||||
patchLang(&english.Strings, &lang.Strings)
|
||||
patchLang(&english.Notifications, &lang.Notifications)
|
||||
patchQuantityStrings(&english.QuantityStrings, &lang.QuantityStrings)
|
||||
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 {
|
||||
@@ -250,27 +337,30 @@ func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
|
||||
}
|
||||
engFound := false
|
||||
var err error
|
||||
for _, filesystem := range filesystems {
|
||||
err = load(filesystem, "en-us.json")
|
||||
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 _, filesystem := range filesystems {
|
||||
files, err := fs.ReadDir(filesystem, st.lang.AdminPath)
|
||||
for i := range filesystems {
|
||||
files, err := fs.ReadDir(filesystems[i], st.lang.AdminPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, f := range files {
|
||||
if f.Name() != "en-us.json" {
|
||||
err = load(filesystem, f.Name())
|
||||
if !loadedLangs[i][f.Name()] {
|
||||
err = load(i, f.Name())
|
||||
if err == nil {
|
||||
adminLoaded = true
|
||||
loadedLangs[i][f.Name()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,7 +374,10 @@ func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
|
||||
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 {
|
||||
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))
|
||||
@@ -298,11 +391,27 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(index, &lang.Strings)
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
if fname != "en-us.json" {
|
||||
patchLang(&english.Strings, &lang.Strings)
|
||||
patchLang(&english.Notifications, &lang.Notifications)
|
||||
patchQuantityStrings(&english.ValidationStrings, &lang.ValidationStrings)
|
||||
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 {
|
||||
@@ -319,27 +428,30 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
|
||||
}
|
||||
engFound := false
|
||||
var err error
|
||||
for _, filesystem := range filesystems {
|
||||
err = load(filesystem, "en-us.json")
|
||||
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 _, filesystem := range filesystems {
|
||||
files, err := fs.ReadDir(filesystem, st.lang.FormPath)
|
||||
for i := range filesystems {
|
||||
files, err := fs.ReadDir(filesystems[i], st.lang.FormPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, f := range files {
|
||||
if f.Name() != "en-us.json" {
|
||||
err = load(filesystem, f.Name())
|
||||
if !loadedLangs[i][f.Name()] {
|
||||
err = load(i, f.Name())
|
||||
if err == nil {
|
||||
formLoaded = true
|
||||
loadedLangs[i][f.Name()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,7 +465,10 @@ func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
|
||||
func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
|
||||
st.lang.PasswordReset = map[string]pwrLang{}
|
||||
var english pwrLang
|
||||
load := func(filesystem fs.FS, fname string) error {
|
||||
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))
|
||||
@@ -367,36 +482,52 @@ func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(index, &lang.Strings)
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
if fname != "en-us.json" {
|
||||
patchLang(&english.Strings, &lang.Strings)
|
||||
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 _, filesystem := range filesystems {
|
||||
err = load(filesystem, "en-us.json")
|
||||
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 _, filesystem := range filesystems {
|
||||
files, err := fs.ReadDir(filesystem, st.lang.PasswordResetPath)
|
||||
for i := range filesystems {
|
||||
files, err := fs.ReadDir(filesystems[i], st.lang.PasswordResetPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, f := range files {
|
||||
if f.Name() != "en-us.json" {
|
||||
err = load(filesystem, f.Name())
|
||||
if !loadedLangs[i][f.Name()] {
|
||||
err = load(i, f.Name())
|
||||
if err == nil {
|
||||
formLoaded = true
|
||||
loadedLangs[i][f.Name()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,7 +541,10 @@ func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
|
||||
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 {
|
||||
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))
|
||||
@@ -424,43 +558,73 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(index, &lang.Strings)
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
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)
|
||||
patchLang(&english.EmailConfirmation, &lang.EmailConfirmation)
|
||||
patchLang(&english.UserExpired, &lang.UserExpired)
|
||||
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 _, filesystem := range filesystems {
|
||||
err = load(filesystem, "en-us.json")
|
||||
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 _, filesystem := range filesystems {
|
||||
files, err := fs.ReadDir(filesystem, st.lang.EmailPath)
|
||||
for i := range filesystems {
|
||||
files, err := fs.ReadDir(filesystems[i], st.lang.EmailPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, f := range files {
|
||||
if f.Name() != "en-us.json" {
|
||||
err = load(filesystem, f.Name())
|
||||
if !loadedLangs[i][f.Name()] {
|
||||
err = load(i, f.Name())
|
||||
if err == nil {
|
||||
emailLoaded = true
|
||||
loadedLangs[i][f.Name()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -471,6 +635,83 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
|
||||
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 {
|
||||
@@ -516,6 +757,14 @@ 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)
|
||||
}
|
||||
|
||||
220
telegram.go
Normal file
220
telegram.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
)
|
||||
|
||||
type VerifiedToken struct {
|
||||
Token string
|
||||
ChatID int64
|
||||
Username string
|
||||
}
|
||||
|
||||
type TelegramDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
bot *tg.BotAPI
|
||||
username string
|
||||
tokens []string
|
||||
verifiedTokens []VerifiedToken
|
||||
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
|
||||
link string
|
||||
app *appContext
|
||||
}
|
||||
|
||||
func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
|
||||
token := app.config.Section("telegram").Key("token").String()
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("token was blank")
|
||||
}
|
||||
bot, err := tg.NewBotAPI(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
td := &TelegramDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
bot: bot,
|
||||
username: bot.Self.UserName,
|
||||
tokens: []string{},
|
||||
verifiedTokens: []VerifiedToken{},
|
||||
languages: map[int64]string{},
|
||||
link: "https://t.me/" + bot.Self.UserName,
|
||||
app: app,
|
||||
}
|
||||
for _, user := range app.storage.telegram {
|
||||
if user.Lang != "" {
|
||||
td.languages[user.ChatID] = user.Lang
|
||||
}
|
||||
}
|
||||
return td, nil
|
||||
}
|
||||
|
||||
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||
func (t *TelegramDaemon) NewAuthToken() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
pin := make([]rune, 8)
|
||||
for i := range pin {
|
||||
if i == 2 || i == 5 {
|
||||
pin[i] = '-'
|
||||
} else {
|
||||
pin[i] = runes[rand.Intn(len(runes))]
|
||||
}
|
||||
}
|
||||
t.tokens = append(t.tokens, string(pin))
|
||||
return string(pin)
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) run() {
|
||||
t.app.info.Println("Starting Telegram bot daemon")
|
||||
u := tg.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
updates, err := t.bot.GetUpdatesChan(u)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Failed to start Telegram daemon: %v", err)
|
||||
return
|
||||
}
|
||||
for {
|
||||
var upd tg.Update
|
||||
select {
|
||||
case upd = <-updates:
|
||||
if upd.Message == nil {
|
||||
continue
|
||||
}
|
||||
sects := strings.Split(upd.Message.Text, " ")
|
||||
if len(sects) == 0 {
|
||||
continue
|
||||
}
|
||||
lang := t.app.storage.lang.chosenTelegramLang
|
||||
storedLang, ok := t.languages[upd.Message.Chat.ID]
|
||||
if !ok {
|
||||
found := false
|
||||
for code := range t.app.storage.lang.Telegram {
|
||||
if code[:2] == upd.Message.From.LanguageCode {
|
||||
lang = code
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
t.languages[upd.Message.Chat.ID] = lang
|
||||
}
|
||||
} else {
|
||||
lang = storedLang
|
||||
}
|
||||
switch msg := sects[0]; msg {
|
||||
case "/start":
|
||||
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
||||
content += t.app.storage.lang.Telegram[lang].Strings.get("languageMessage")
|
||||
err := t.Reply(&upd, content)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
continue
|
||||
case "/lang":
|
||||
if len(sects) == 1 {
|
||||
list := "/lang <lang>\n"
|
||||
for code := range t.app.storage.lang.Telegram {
|
||||
list += fmt.Sprintf("%s: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
|
||||
}
|
||||
err := t.Reply(&upd, list)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, ok := t.app.storage.lang.Telegram[sects[1]]; ok {
|
||||
t.languages[upd.Message.Chat.ID] = sects[1]
|
||||
for jfID, user := range t.app.storage.telegram {
|
||||
if user.ChatID == upd.Message.Chat.ID {
|
||||
user.Lang = sects[1]
|
||||
t.app.storage.telegram[jfID] = user
|
||||
err := t.app.storage.storeTelegramUsers()
|
||||
if err != nil {
|
||||
t.app.err.Printf("Failed to store Telegram users: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
default:
|
||||
tokenIndex := -1
|
||||
for i, token := range t.tokens {
|
||||
if upd.Message.Text == token {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if tokenIndex == -1 {
|
||||
err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
t.verifiedTokens = append(t.verifiedTokens, VerifiedToken{
|
||||
Token: upd.Message.Text,
|
||||
ChatID: upd.Message.Chat.ID,
|
||||
Username: upd.Message.Chat.UserName,
|
||||
})
|
||||
t.tokens[len(t.tokens)-1], t.tokens[tokenIndex] = t.tokens[tokenIndex], t.tokens[len(t.tokens)-1]
|
||||
t.tokens = t.tokens[:len(t.tokens)-1]
|
||||
}
|
||||
|
||||
case <-t.ShutdownChannel:
|
||||
t.bot.StopReceivingUpdates()
|
||||
t.ShutdownChannel <- "Down"
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) Reply(upd *tg.Update, content string) error {
|
||||
msg := tg.NewMessage((*upd).Message.Chat.ID, content)
|
||||
_, err := t.bot.Send(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) QuoteReply(upd *tg.Update, content string) error {
|
||||
msg := tg.NewMessage((*upd).Message.Chat.ID, content)
|
||||
msg.ReplyToMessageID = (*upd).Message.MessageID
|
||||
_, err := t.bot.Send(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
// Send will send a telegram message to a list of chat IDs. message.text is used if no markdown is given.
|
||||
func (t *TelegramDaemon) Send(message *Message, ID ...int64) error {
|
||||
for _, id := range ID {
|
||||
var msg tg.MessageConfig
|
||||
if message.Markdown == "" {
|
||||
msg = tg.NewMessage(id, message.Text)
|
||||
} else {
|
||||
msg = tg.NewMessage(id, strings.ReplaceAll(message.Markdown, ".", "\\."))
|
||||
msg.ParseMode = "MarkdownV2"
|
||||
}
|
||||
_, err := t.bot.Send(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) Shutdown() {
|
||||
t.Stopped = true
|
||||
t.ShutdownChannel <- "Down"
|
||||
<-t.ShutdownChannel
|
||||
close(t.ShutdownChannel)
|
||||
}
|
||||
8
telegram/go.mod
Normal file
8
telegram/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module github.com/hrfee/jfa-go/telegram
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
)
|
||||
4
telegram/go.sum
Normal file
4
telegram/go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||
59
telegram/telegram.go
Normal file
59
telegram/telegram.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
)
|
||||
|
||||
const (
|
||||
TOKEN = "1785754648:AAG4G6PKZpGDEJM_-MeQHJqD-xUDrrLrTC4"
|
||||
USER = "johnikwock"
|
||||
|
||||
AUTH = "AB-CD-EF"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("Connecting...")
|
||||
bot, err := tg.NewBotAPI(TOKEN)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize bot: %v", err)
|
||||
}
|
||||
bot.Debug = false
|
||||
log.Printf("Authorized Telegram bot \"%s\"", bot.Self.UserName)
|
||||
|
||||
u := tg.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
|
||||
updates, err := bot.GetUpdatesChan(u)
|
||||
|
||||
for update := range updates {
|
||||
if update.Message == nil {
|
||||
continue
|
||||
}
|
||||
log.Printf("New message from \"@%s\": \"%s\"", update.Message.From.UserName, update.Message.Text)
|
||||
if update.Message.From.UserName != USER {
|
||||
continue
|
||||
}
|
||||
var msg tg.MessageConfig
|
||||
sects := strings.Split(update.Message.Text, " ")
|
||||
if sects[0] == "/start" {
|
||||
msg = tg.NewMessage(update.Message.Chat.ID, fmt.Sprintf("Enter this code on the sign-up page to continue: %s", AUTH))
|
||||
} else if sects[0] != "/auth" || sects[len(sects)-1] != AUTH {
|
||||
log.Println("Invalid command or auth token")
|
||||
msg = tg.NewMessage(update.Message.Chat.ID, "Invalid command or token")
|
||||
} else {
|
||||
msg = tg.NewMessage(update.Message.Chat.ID, "Success!")
|
||||
log.Println("Successful auth")
|
||||
}
|
||||
msg.ReplyToMessageID = update.Message.MessageID
|
||||
|
||||
_, err := bot.Send(msg)
|
||||
if err != nil {
|
||||
log.Printf("Send failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
4
telegram/telegram.txt
Normal file
4
telegram/telegram.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
API key: 1785754648:AAG4G6PKZpGDEJM_-MeQHJqD-xUDrrLrTC4
|
||||
|
||||
Name: jfa-bot
|
||||
Username: jfago_bot
|
||||
117
template.go
Normal file
117
template.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func truthy(val interface{}) bool {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
return v != ""
|
||||
case bool:
|
||||
return v
|
||||
case int:
|
||||
return v != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Templater for custom emails.
|
||||
// Variables should be written as {varName}.
|
||||
// If statements should be written as {if (!)varName}...{endif}.
|
||||
// Strings are true if != "", ints are true if != 0.
|
||||
func templateEmail(content string, variables []string, conditionals []string, values map[string]interface{}) string {
|
||||
ifStart, ifEnd := -1, -1
|
||||
ifTrue := false
|
||||
invalidIf := false
|
||||
previousEnd := -2
|
||||
cStart, cEnd := -1, -1
|
||||
varStart, varEnd := -1, -1
|
||||
varName := ""
|
||||
out := ""
|
||||
for i, c := range content {
|
||||
if c == '{' {
|
||||
cStart = i + 1
|
||||
for content[cStart] == ' ' {
|
||||
cStart++
|
||||
}
|
||||
if content[cStart:cStart+3] == "if " {
|
||||
varStart = cStart + 3
|
||||
for content[varStart] == ' ' {
|
||||
varStart++
|
||||
}
|
||||
}
|
||||
if ifStart == -1 {
|
||||
out += content[previousEnd+2 : i]
|
||||
}
|
||||
if content[cStart:cStart+5] != "endif" || invalidIf {
|
||||
continue
|
||||
}
|
||||
ifEnd = i - 1
|
||||
if ifTrue {
|
||||
out += templateEmail(content[ifStart:ifEnd+1], variables, conditionals, values)
|
||||
ifTrue = false
|
||||
}
|
||||
} else if c == '}' {
|
||||
if varStart != -1 {
|
||||
ifStart = i + 1
|
||||
varEnd = i - 1
|
||||
for content[varEnd] == ' ' {
|
||||
varEnd--
|
||||
}
|
||||
varName = content[varStart : varEnd+1]
|
||||
positive := true
|
||||
if varName[0] == '!' {
|
||||
positive = false
|
||||
varName = varName[1:]
|
||||
}
|
||||
validVar := false
|
||||
wrappedVarName := "{" + varName + "}"
|
||||
for _, v := range conditionals {
|
||||
if v == wrappedVarName {
|
||||
validVar = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if validVar {
|
||||
ifTrue = positive == truthy(values[varName])
|
||||
} else {
|
||||
invalidIf = true
|
||||
ifStart, ifEnd = -1, -1
|
||||
}
|
||||
varStart, varEnd = -1, -1
|
||||
}
|
||||
cEnd = i - 1
|
||||
for content[cEnd] == ' ' {
|
||||
cEnd--
|
||||
}
|
||||
previousEnd = i - 1
|
||||
if content[cEnd-4:cEnd+1] == "endif" && !invalidIf {
|
||||
continue
|
||||
}
|
||||
validVar := false
|
||||
varName = content[cStart : cEnd+1]
|
||||
cStart, cEnd = -1, -1
|
||||
if ifStart != -1 {
|
||||
continue
|
||||
}
|
||||
wrappedVarName := "{" + varName + "}"
|
||||
for _, v := range variables {
|
||||
if v == wrappedVarName {
|
||||
validVar = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validVar {
|
||||
out += wrappedVarName
|
||||
continue
|
||||
}
|
||||
out += fmt.Sprint(values[varName])
|
||||
}
|
||||
}
|
||||
if previousEnd+1 != len(content)-1 {
|
||||
out += content[previousEnd+2:]
|
||||
}
|
||||
if out == "" {
|
||||
return content
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -62,6 +62,10 @@ window.availableProfiles = window.availableProfiles || [];
|
||||
window.modals.extendExpiry = new Modal(document.getElementById("modal-extend-expiry"));
|
||||
|
||||
window.modals.updateInfo = new Modal(document.getElementById("modal-update"));
|
||||
|
||||
if (window.telegramEnabled) {
|
||||
window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
|
||||
}
|
||||
})();
|
||||
|
||||
var inviteCreator = new createInvite();
|
||||
|
||||
68
ts/form.ts
68
ts/form.ts
@@ -1,16 +1,21 @@
|
||||
import { Modal } from "./modules/modal.js";
|
||||
import { notificationBox, whichAnimationEvent } from "./modules/common.js";
|
||||
import { _get, _post, toggleLoader, toDateString } from "./modules/common.js";
|
||||
import { loadLangSelector } from "./modules/lang.js";
|
||||
|
||||
interface formWindow extends Window {
|
||||
validationStrings: pwValStrings;
|
||||
invalidPassword: string;
|
||||
modal: Modal;
|
||||
successModal: Modal;
|
||||
telegramModal: Modal;
|
||||
confirmationModal: Modal
|
||||
code: string;
|
||||
messages: { [key: string]: string };
|
||||
confirmation: boolean;
|
||||
confirmationModal: Modal
|
||||
telegramRequired: boolean;
|
||||
telegramPIN: string;
|
||||
userExpiryEnabled: boolean;
|
||||
userExpiryMonths: number;
|
||||
userExpiryDays: number;
|
||||
userExpiryHours: number;
|
||||
userExpiryMinutes: number;
|
||||
@@ -33,7 +38,52 @@ interface pwValStrings {
|
||||
|
||||
loadLangSelector("form");
|
||||
|
||||
window.modal = new Modal(document.getElementById("modal-success"), true);
|
||||
window.notifications = new notificationBox(document.getElementById("notification-box") as HTMLDivElement);
|
||||
|
||||
window.animationEvent = whichAnimationEvent();
|
||||
|
||||
window.successModal = new Modal(document.getElementById("modal-success"), true);
|
||||
|
||||
var telegramVerified = false;
|
||||
if (window.telegramEnabled) {
|
||||
window.telegramModal = new Modal(document.getElementById("modal-telegram"), window.telegramRequired);
|
||||
const telegramButton = document.getElementById("link-telegram") as HTMLSpanElement;
|
||||
telegramButton.onclick = () => {
|
||||
const waiting = document.getElementById("telegram-waiting") as HTMLSpanElement;
|
||||
toggleLoader(waiting);
|
||||
window.telegramModal.show();
|
||||
let modalClosed = false;
|
||||
window.telegramModal.onclose = () => {
|
||||
modalClosed = true;
|
||||
toggleLoader(waiting);
|
||||
}
|
||||
const checkVerified = () => _get("/invite/" + window.code + "/telegram/verified/" + window.telegramPIN, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 401) {
|
||||
window.telegramModal.close();
|
||||
window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]);
|
||||
return;
|
||||
} else if (req.status == 200) {
|
||||
if (req.response["success"] as boolean) {
|
||||
telegramVerified = true;
|
||||
waiting.classList.add("~positive");
|
||||
waiting.classList.remove("~info");
|
||||
window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]);
|
||||
setTimeout(window.telegramModal.close, 2000);
|
||||
telegramButton.classList.add("unfocused");
|
||||
document.getElementById("contact-via").classList.remove("unfocused");
|
||||
const radio = document.getElementById("contact-via-telegram") as HTMLInputElement;
|
||||
radio.checked = true;
|
||||
} else if (!modalClosed) {
|
||||
setTimeout(checkVerified, 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
checkVerified();
|
||||
};
|
||||
}
|
||||
|
||||
if (window.confirmation) {
|
||||
window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true);
|
||||
}
|
||||
@@ -43,6 +93,7 @@ if (window.userExpiryEnabled) {
|
||||
const messageEl = document.getElementById("user-expiry-message") as HTMLElement;
|
||||
const calculateTime = () => {
|
||||
let time = new Date()
|
||||
time.setMonth(time.getMonth() + window.userExpiryMonths);
|
||||
time.setDate(time.getDate() + window.userExpiryDays);
|
||||
time.setHours(time.getHours() + window.userExpiryHours);
|
||||
time.setMinutes(time.getMinutes() + window.userExpiryMinutes);
|
||||
@@ -108,6 +159,8 @@ interface sendDTO {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
telegram_pin?: string;
|
||||
telegram_contact?: boolean;
|
||||
}
|
||||
|
||||
const create = (event: SubmitEvent) => {
|
||||
@@ -119,6 +172,13 @@ const create = (event: SubmitEvent) => {
|
||||
email: emailField.value,
|
||||
password: passwordField.value
|
||||
};
|
||||
if (telegramVerified) {
|
||||
send.telegram_pin = window.telegramPIN;
|
||||
const radio = document.getElementById("contact-via-telegram") as HTMLInputElement;
|
||||
if (radio.checked) {
|
||||
send.telegram_contact = true;
|
||||
}
|
||||
}
|
||||
_post("/newUser", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
let vals = req.response as respDTO;
|
||||
@@ -128,7 +188,7 @@ const create = (event: SubmitEvent) => {
|
||||
if (!vals[type]) { valid = false; }
|
||||
}
|
||||
if (req.status == 200 && valid) {
|
||||
window.modal.show();
|
||||
window.successModal.show();
|
||||
} else {
|
||||
submitSpan.classList.add("~critical");
|
||||
submitSpan.classList.remove("~urge");
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { _get, _post, _delete, toggleLoader, toDateString } from "../modules/common.js";
|
||||
import { _get, _post, _delete, toggleLoader, addLoader, removeLoader, toDateString } from "../modules/common.js";
|
||||
import { templateEmail } from "../modules/settings.js";
|
||||
import { Marked } from "@ts-stack/markdown";
|
||||
import { stripMarkdown } from "../modules/stripmd.js";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@@ -8,6 +11,13 @@ interface User {
|
||||
admin: boolean;
|
||||
disabled: boolean;
|
||||
expiry: number;
|
||||
telegram: string;
|
||||
notify_telegram: boolean;
|
||||
}
|
||||
|
||||
interface getPinResponse {
|
||||
token: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
class user implements User {
|
||||
@@ -19,6 +29,9 @@ class user implements User {
|
||||
private _email: HTMLInputElement;
|
||||
private _emailAddress: string;
|
||||
private _emailEditButton: HTMLElement;
|
||||
private _telegram: HTMLTableDataCellElement;
|
||||
private _telegramUsername: string;
|
||||
private _notifyTelegram: boolean;
|
||||
private _expiry: HTMLTableDataCellElement;
|
||||
private _expiryUnix: number;
|
||||
private _lastActive: HTMLTableDataCellElement;
|
||||
@@ -69,6 +82,89 @@ class user implements User {
|
||||
}
|
||||
}
|
||||
|
||||
get telegram(): string { return this._telegramUsername; }
|
||||
set telegram(u: string) {
|
||||
if (!window.telegramEnabled) return;
|
||||
this._telegramUsername = u;
|
||||
if (u == "") {
|
||||
this._telegram.innerHTML = `<span class="chip btn !low">Add</span>`;
|
||||
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
|
||||
} else {
|
||||
this._telegram.innerHTML = `
|
||||
<a href="https://t.me/${u}" target="_blank">@${u}</a>
|
||||
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
|
||||
<div class="dropdown manual">
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral !low">
|
||||
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
|
||||
<label class="switch pb-1 mt-half">
|
||||
<input type="radio" name="accounts-contact-${this.id}" class="accounts-contact-email">
|
||||
<span>Email</span>
|
||||
</label>
|
||||
<label class="switch pb-1">
|
||||
<input type="radio" name="accounts-contact-${this.id}">
|
||||
<span>Telegram</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
|
||||
const button = this._telegram.querySelector("i");
|
||||
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
|
||||
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||
for (let i = 0; i < radios.length; i++) {
|
||||
radios[i].onclick = this._setTelegramNotify;
|
||||
}
|
||||
|
||||
button.onclick = () => {
|
||||
dropdown.classList.add("selected");
|
||||
document.addEventListener("click", outerClickListener);
|
||||
};
|
||||
const outerClickListener = (event: Event) => {
|
||||
if (!(event.target instanceof HTMLElement && (this._telegram.contains(event.target) || button.contains(event.target)))) {
|
||||
dropdown.classList.remove("selected");
|
||||
document.removeEventListener("click", outerClickListener);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get notify_telegram(): boolean { return this._notifyTelegram; }
|
||||
set notify_telegram(s: boolean) {
|
||||
if (!window.telegramEnabled || !this._telegramUsername) return;
|
||||
this._notifyTelegram = s;
|
||||
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||
radios[0].checked = !s;
|
||||
radios[1].checked = s;
|
||||
}
|
||||
|
||||
private _setTelegramNotify = () => {
|
||||
const radios = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||
let send = {
|
||||
id: this.id,
|
||||
enabled: radios[1].checked
|
||||
};
|
||||
_post("/users/telegram/notify", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("errorSetTelegramNotify", window.lang.notif("errorSaveSettings"));
|
||||
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, false, (req: XMLHttpRequest) => {
|
||||
if (req.status == 0) {
|
||||
window.notifications.connectionError();
|
||||
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
|
||||
return;
|
||||
} else if (req.status == 401) {
|
||||
radios[0].checked, radios[1].checked= radios[1].checked, radios[0].checked;
|
||||
window.notifications.customError("401Error", window.lang.notif("error401Unauthorized"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get expiry(): number { return this._expiryUnix; }
|
||||
set expiry(unix: number) {
|
||||
this._expiryUnix = unix;
|
||||
@@ -94,13 +190,21 @@ class user implements User {
|
||||
|
||||
constructor(user: User) {
|
||||
this._row = document.createElement("tr") as HTMLTableRowElement;
|
||||
this._row.innerHTML = `
|
||||
let innerHTML = `
|
||||
<td><input type="checkbox" value=""></td>
|
||||
<td><span class="accounts-username"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></td>
|
||||
<td><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-half"></span></td>
|
||||
<td class="accounts-expiry"></td>
|
||||
<td class="accounts-last-active"></td>
|
||||
`;
|
||||
if (window.telegramEnabled) {
|
||||
innerHTML += `
|
||||
<td class="accounts-telegram"></td>
|
||||
`;
|
||||
}
|
||||
innerHTML += `
|
||||
<td class="accounts-expiry"></td>
|
||||
<td class="accounts-last-active"></td>
|
||||
`;
|
||||
this._row.innerHTML = innerHTML;
|
||||
const emailEditor = `<input type="email" class="input ~neutral !normal stealth-input">`;
|
||||
this._check = this._row.querySelector("input[type=checkbox]") as HTMLInputElement;
|
||||
this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement;
|
||||
@@ -108,6 +212,7 @@ class user implements User {
|
||||
this._disabled = this._row.querySelector(".accounts-disabled") as HTMLSpanElement;
|
||||
this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement;
|
||||
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
|
||||
this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement;
|
||||
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
|
||||
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
|
||||
this._check.onchange = () => { this.selected = this._check.checked; }
|
||||
@@ -166,14 +271,60 @@ class user implements User {
|
||||
});
|
||||
}
|
||||
|
||||
private _addTelegram = () => _get("/telegram/pin", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4 && req.status == 200) {
|
||||
const pin = document.getElementById("telegram-pin");
|
||||
const link = document.getElementById("telegram-link") as HTMLAnchorElement;
|
||||
const username = document.getElementById("telegram-username") as HTMLSpanElement;
|
||||
const waiting = document.getElementById("telegram-waiting") as HTMLSpanElement;
|
||||
let resp = req.response as getPinResponse;
|
||||
pin.textContent = resp.token;
|
||||
link.href = "https://t.me/" + resp.username;
|
||||
username.textContent = resp.username;
|
||||
addLoader(waiting);
|
||||
let modalClosed = false;
|
||||
window.modals.telegram.onclose = () => {
|
||||
modalClosed = true;
|
||||
removeLoader(waiting);
|
||||
}
|
||||
let send = {
|
||||
token: resp.token,
|
||||
id: this.id
|
||||
};
|
||||
const checkVerified = () => _post("/users/telegram", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 200 && req.response["success"] as boolean) {
|
||||
removeLoader(waiting);
|
||||
waiting.classList.add("~positive");
|
||||
waiting.classList.remove("~info");
|
||||
window.notifications.customSuccess("telegramVerified", window.lang.notif("telegramVerified"));
|
||||
setTimeout(() => {
|
||||
window.modals.telegram.close();
|
||||
waiting.classList.add("~info");
|
||||
waiting.classList.remove("~positive");
|
||||
}, 2000);
|
||||
document.dispatchEvent(new CustomEvent("accounts-reload"));
|
||||
} else if (!modalClosed) {
|
||||
setTimeout(checkVerified, 1500);
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
window.modals.telegram.show();
|
||||
checkVerified();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
update = (user: User) => {
|
||||
this.id = user.id;
|
||||
this.name = user.name;
|
||||
this.email = user.email || "";
|
||||
this.telegram = user.telegram;
|
||||
this.last_active = user.last_active;
|
||||
this.admin = user.admin;
|
||||
this.disabled = user.disabled;
|
||||
this.expiry = user.expiry;
|
||||
this.notify_telegram = user.notify_telegram;
|
||||
}
|
||||
|
||||
asElement = (): HTMLTableRowElement => { return this._row; }
|
||||
@@ -185,15 +336,16 @@ class user implements User {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export class accountsList {
|
||||
private _table = document.getElementById("accounts-list") as HTMLTableSectionElement;
|
||||
|
||||
private _addUserButton = document.getElementById("accounts-add-user") as HTMLSpanElement;
|
||||
private _announceButton = document.getElementById("accounts-announce") as HTMLSpanElement;
|
||||
private _announcePreview: HTMLElement;
|
||||
private _previewLoaded = false;
|
||||
private _announceTextarea = document.getElementById("textarea-announce") as HTMLTextAreaElement;
|
||||
private _deleteUser = document.getElementById("accounts-delete-user") as HTMLSpanElement;
|
||||
private _disableEnable = document.getElementById("accounts-disable-enable") as HTMLSpanElement;
|
||||
private _deleteNotify = document.getElementById("delete-user-notify") as HTMLInputElement;
|
||||
private _deleteReason = document.getElementById("textarea-delete-user") as HTMLTextAreaElement;
|
||||
private _extendExpiry = document.getElementById("accounts-extend-expiry") as HTMLSpanElement;
|
||||
@@ -209,6 +361,8 @@ export class accountsList {
|
||||
private _sortedByName: string[] = [];
|
||||
private _checkCount: number = 0;
|
||||
private _inSearch = false;
|
||||
// Whether the enable/disable button should enable or not.
|
||||
private _shouldEnable = false;
|
||||
|
||||
private _addUserForm = document.getElementById("form-add-user") as HTMLFormElement;
|
||||
private _addUserName = this._addUserForm.querySelector("input[type=text]") as HTMLInputElement;
|
||||
@@ -217,7 +371,7 @@ export class accountsList {
|
||||
|
||||
private _count = 30;
|
||||
private _populateNumbers = () => {
|
||||
const fieldIDs = ["days", "hours", "minutes"];
|
||||
const fieldIDs = ["months", "days", "hours", "minutes"];
|
||||
const prefixes = ["extend-expiry-"];
|
||||
for (let i = 0; i < fieldIDs.length; i++) {
|
||||
for (let j = 0; j < prefixes.length; j++) {
|
||||
@@ -325,10 +479,11 @@ export class accountsList {
|
||||
this._selectAll.checked = false;
|
||||
this._modifySettings.classList.add("unfocused");
|
||||
this._deleteUser.classList.add("unfocused");
|
||||
if (window.emailEnabled) {
|
||||
if (window.emailEnabled || window.telegramEnabled) {
|
||||
this._announceButton.classList.add("unfocused");
|
||||
}
|
||||
this._extendExpiry.classList.add("unfocused");
|
||||
this._disableEnable.classList.add("unfocused");
|
||||
} else {
|
||||
let visibleCount = 0;
|
||||
for (let id in this._users) {
|
||||
@@ -346,20 +501,41 @@ export class accountsList {
|
||||
this._modifySettings.classList.remove("unfocused");
|
||||
this._deleteUser.classList.remove("unfocused");
|
||||
this._deleteUser.textContent = window.lang.quantity("deleteUser", list.length);
|
||||
if (window.emailEnabled) {
|
||||
if (window.emailEnabled || window.telegramEnabled) {
|
||||
this._announceButton.classList.remove("unfocused");
|
||||
}
|
||||
let anyNonExpiries = list.length == 0 ? true : false;
|
||||
// Only show enable/disable button if all selected have the same state.
|
||||
this._shouldEnable = this._users[list[0]].disabled
|
||||
let showDisableEnable = true;
|
||||
for (let id of list) {
|
||||
if (!this._users[id].expiry) {
|
||||
if (!anyNonExpiries && !this._users[id].expiry) {
|
||||
anyNonExpiries = true;
|
||||
this._extendExpiry.classList.add("unfocused");
|
||||
break;
|
||||
}
|
||||
if (showDisableEnable && this._users[id].disabled != this._shouldEnable) {
|
||||
showDisableEnable = false;
|
||||
this._disableEnable.classList.add("unfocused");
|
||||
}
|
||||
if (!showDisableEnable && anyNonExpiries) { break; }
|
||||
}
|
||||
if (!anyNonExpiries) {
|
||||
this._extendExpiry.classList.remove("unfocused");
|
||||
}
|
||||
if (showDisableEnable) {
|
||||
let message: string;
|
||||
if (this._shouldEnable) {
|
||||
message = window.lang.strings("reEnable");
|
||||
this._disableEnable.classList.add("~positive");
|
||||
this._disableEnable.classList.remove("~warning");
|
||||
} else {
|
||||
message = window.lang.strings("disable");
|
||||
this._disableEnable.classList.add("~warning");
|
||||
this._disableEnable.classList.remove("~positive");
|
||||
}
|
||||
this._disableEnable.classList.remove("unfocused");
|
||||
this._disableEnable.textContent = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,7 +583,16 @@ export class accountsList {
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
loadPreview = () => {
|
||||
let content = this._announceTextarea.value;
|
||||
if (!this._previewLoaded) {
|
||||
content = stripMarkdown(content);
|
||||
this._announcePreview.textContent = content;
|
||||
} else {
|
||||
content = Marked.parse(content);
|
||||
this._announcePreview.innerHTML = content;
|
||||
}
|
||||
}
|
||||
announce = () => {
|
||||
const modalHeader = document.getElementById("header-announce");
|
||||
modalHeader.textContent = window.lang.quantity("announceTo", this._collectUsers().length);
|
||||
@@ -415,16 +600,16 @@ export class accountsList {
|
||||
let list = this._collectUsers();
|
||||
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
||||
const subject = document.getElementById("announce-subject") as HTMLInputElement;
|
||||
const message = document.getElementById("textarea-announce") as HTMLTextAreaElement;
|
||||
|
||||
subject.value = "";
|
||||
message.value = "";
|
||||
this._announceTextarea.value = "";
|
||||
form.onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
toggleLoader(button);
|
||||
let send = {
|
||||
"users": list,
|
||||
"subject": subject.value,
|
||||
"message": message.value
|
||||
"message": this._announceTextarea.value
|
||||
}
|
||||
_post("/users/announce", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
@@ -438,15 +623,91 @@ export class accountsList {
|
||||
}
|
||||
});
|
||||
};
|
||||
window.modals.announce.show();
|
||||
_get("/config/emails/Announcement", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
const preview = document.getElementById("announce-preview") as HTMLDivElement;
|
||||
if (req.status != 200) {
|
||||
preview.innerHTML = `<pre class="preview-content" class="monospace"></pre>`;
|
||||
window.modals.announce.show();
|
||||
this._previewLoaded = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let templ = req.response as templateEmail;
|
||||
if (!templ.html) {
|
||||
preview.innerHTML = `<pre class="preview-content" class="monospace"></pre>`;
|
||||
this._previewLoaded = false;
|
||||
} else {
|
||||
preview.innerHTML = templ.html;
|
||||
this._previewLoaded = true;
|
||||
}
|
||||
this._announcePreview = preview.getElementsByClassName("preview-content")[0] as HTMLElement;
|
||||
this.loadPreview();
|
||||
window.modals.announce.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableDisableUsers = () => {
|
||||
// We can share the delete modal for this
|
||||
const modalHeader = document.getElementById("header-delete-user");
|
||||
const form = document.getElementById("form-delete-user") as HTMLFormElement;
|
||||
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
||||
let list = this._collectUsers();
|
||||
if (this._shouldEnable) {
|
||||
modalHeader.textContent = window.lang.quantity("reEnableUsers", list.length);
|
||||
button.textContent = window.lang.strings("reEnable");
|
||||
button.classList.add("~urge");
|
||||
button.classList.remove("~critical");
|
||||
} else {
|
||||
modalHeader.textContent = window.lang.quantity("disableUsers", list.length);
|
||||
button.textContent = window.lang.strings("disable");
|
||||
button.classList.add("~critical");
|
||||
button.classList.remove("~urge");
|
||||
}
|
||||
this._deleteNotify.checked = false;
|
||||
this._deleteReason.value = "";
|
||||
this._deleteReason.classList.add("unfocused");
|
||||
form.onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
toggleLoader(button);
|
||||
let send = {
|
||||
"users": list,
|
||||
"enabled": this._shouldEnable,
|
||||
"notify": this._deleteNotify.checked,
|
||||
"reason": this._deleteNotify ? this._deleteReason.value : ""
|
||||
};
|
||||
_post("/users/enable", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(button);
|
||||
window.modals.deleteUser.close();
|
||||
if (req.status != 200 && req.status != 204) {
|
||||
let errorMsg = window.lang.notif("errorFailureCheckLogs");
|
||||
if (!("error" in req.response)) {
|
||||
errorMsg = window.lang.notif("errorPartialFailureCheckLogs");
|
||||
}
|
||||
window.notifications.customError("deleteUserError", errorMsg);
|
||||
} else if (this._shouldEnable) {
|
||||
window.notifications.customSuccess("enableUserSuccess", window.lang.quantity("enabledUser", list.length));
|
||||
} else {
|
||||
window.notifications.customSuccess("disableUserSuccess", window.lang.quantity("disabledUser", list.length));
|
||||
}
|
||||
this.reload();
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
window.modals.deleteUser.show();
|
||||
}
|
||||
|
||||
deleteUsers = () => {
|
||||
const modalHeader = document.getElementById("header-delete-user");
|
||||
modalHeader.textContent = window.lang.quantity("deleteNUsers", this._collectUsers().length);
|
||||
let list = this._collectUsers();
|
||||
modalHeader.textContent = window.lang.quantity("deleteNUsers", list.length);
|
||||
const form = document.getElementById("form-delete-user") as HTMLFormElement;
|
||||
const button = form.querySelector("span.submit") as HTMLSpanElement;
|
||||
button.textContent = window.lang.strings("delete");
|
||||
button.classList.add("~critical");
|
||||
button.classList.remove("~urge");
|
||||
this._deleteNotify.checked = false;
|
||||
this._deleteReason.value = "";
|
||||
this._deleteReason.classList.add("unfocused");
|
||||
@@ -469,7 +730,7 @@ export class accountsList {
|
||||
}
|
||||
window.notifications.customError("deleteUserError", errorMsg);
|
||||
} else {
|
||||
window.notifications.customSuccess("deleteUserSuccess", window.lang.quantity("deletedUser", this._collectUsers().length));
|
||||
window.notifications.customSuccess("deleteUserSuccess", window.lang.quantity("deletedUser", list.length));
|
||||
}
|
||||
this.reload();
|
||||
}
|
||||
@@ -560,7 +821,7 @@ export class accountsList {
|
||||
form.onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
let send = { "users": applyList }
|
||||
for (let field of ["days", "hours", "minutes"]) {
|
||||
for (let field of ["months", "days", "hours", "minutes"]) {
|
||||
send[field] = +(document.getElementById("extend-expiry-"+field) as HTMLSelectElement).value;
|
||||
}
|
||||
_post("/users/extend", send, (req: XMLHttpRequest) => {
|
||||
@@ -585,6 +846,7 @@ export class accountsList {
|
||||
this._selectAll.onchange = () => {
|
||||
this.selectAll = this._selectAll.checked;
|
||||
};
|
||||
document.addEventListener("accounts-reload", this.reload);
|
||||
document.addEventListener("accountCheckEvent", () => { this._checkCount++; this._checkCheckCount(); });
|
||||
document.addEventListener("accountUncheckEvent", () => { this._checkCount--; this._checkCheckCount(); });
|
||||
this._addUserButton.onclick = window.modals.addUser.toggle;
|
||||
@@ -630,6 +892,9 @@ export class accountsList {
|
||||
this._extendExpiry.onclick = this.extendExpiry;
|
||||
this._extendExpiry.classList.add("unfocused");
|
||||
|
||||
this._disableEnable.onclick = this.enableDisableUsers;
|
||||
this._disableEnable.classList.add("unfocused");
|
||||
|
||||
if (!window.usernameEnabled) {
|
||||
this._addUserName.classList.add("unfocused");
|
||||
this._addUserName = this._addUserEmail;
|
||||
@@ -668,6 +933,8 @@ export class accountsList {
|
||||
}
|
||||
this._checkCheckCount();
|
||||
};
|
||||
|
||||
this._announceTextarea.onkeyup = this.loadPreview;
|
||||
}
|
||||
|
||||
reload = () => _get("/users", null, (req: XMLHttpRequest) => {
|
||||
@@ -687,7 +954,7 @@ export class accountsList {
|
||||
this._users[id].remove();
|
||||
delete this._users[id];
|
||||
}
|
||||
this._checkCheckCount;
|
||||
this._checkCheckCount();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
declare var window: Window;
|
||||
|
||||
export function createEl(html: string): HTMLElement {
|
||||
let div = document.createElement('div') as HTMLDivElement;
|
||||
div.innerHTML = html;
|
||||
return div.firstElementChild as HTMLElement;
|
||||
}
|
||||
|
||||
export function toDateString(date: Date): string {
|
||||
const locale = window.language || (window as any).navigator.userLanguage || window.navigator.language;
|
||||
const t12 = document.getElementById("lang-12h") as HTMLInputElement;
|
||||
@@ -27,49 +21,6 @@ export function toDateString(date: Date): string {
|
||||
return date.toLocaleDateString(locale, args1) + " " + date.toLocaleString(locale, args2);
|
||||
}
|
||||
|
||||
export function serializeForm(id: string): Object {
|
||||
const form = document.getElementById(id) as HTMLFormElement;
|
||||
let formData = {};
|
||||
for (let i = 0; i < form.elements.length; i++) {
|
||||
const el = form.elements[i];
|
||||
if ((el as HTMLInputElement).type == "submit") {
|
||||
continue;
|
||||
}
|
||||
let name = (el as HTMLInputElement).name;
|
||||
if (!name) {
|
||||
name = el.id;
|
||||
}
|
||||
switch ((el as HTMLInputElement).type) {
|
||||
case "checkbox":
|
||||
formData[name] = (el as HTMLInputElement).checked;
|
||||
break;
|
||||
case "text":
|
||||
case "password":
|
||||
case "email":
|
||||
case "number":
|
||||
formData[name] = (el as HTMLInputElement).value;
|
||||
break;
|
||||
case "select-one":
|
||||
case "select":
|
||||
let val: string = (el as HTMLSelectElement).value.toString();
|
||||
if (!isNaN(val as any)) {
|
||||
formData[name] = +val;
|
||||
} else {
|
||||
formData[name] = val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
|
||||
export const rmAttr = (el: HTMLElement, attr: string): void => {
|
||||
if (el.classList.contains(attr)) {
|
||||
el.classList.remove(attr);
|
||||
}
|
||||
};
|
||||
|
||||
export const addAttr = (el: HTMLElement, attr: string): void => el.classList.add(attr);
|
||||
export const _get = (url: string, data: Object, onreadystatechange: (req: XMLHttpRequest) => void): void => {
|
||||
let req = new XMLHttpRequest();
|
||||
if (window.URLBase) { url = window.URLBase + url; }
|
||||
@@ -228,3 +179,22 @@ export function toggleLoader(el: HTMLElement, small: boolean = true) {
|
||||
el.appendChild(dot);
|
||||
}
|
||||
}
|
||||
|
||||
export function addLoader(el: HTMLElement, small: boolean = true) {
|
||||
if (!el.classList.contains("loader")) {
|
||||
el.classList.add("loader");
|
||||
if (small) { el.classList.add("loader-sm"); }
|
||||
const dot = document.createElement("span") as HTMLSpanElement;
|
||||
dot.classList.add("dot")
|
||||
el.appendChild(dot);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeLoader(el: HTMLElement, small: boolean = true) {
|
||||
if (el.classList.contains("loader")) {
|
||||
el.classList.remove("loader");
|
||||
el.classList.remove("loader-sm");
|
||||
const dot = el.querySelector("span.dot");
|
||||
if (dot) { dot.remove(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js";
|
||||
|
||||
export class DOMInvite implements Invite {
|
||||
class DOMInvite implements Invite {
|
||||
updateNotify = (checkbox: HTMLInputElement) => {
|
||||
let state: { [code: string]: { [type: string]: boolean } } = {};
|
||||
let revertChanges: () => void;
|
||||
@@ -120,7 +120,7 @@ export class DOMInvite implements Invite {
|
||||
get usedBy(): { [name: string]: number } { return this._usedBy; }
|
||||
set usedBy(uB: { [name: string]: number }) {
|
||||
this._usedBy = uB;
|
||||
if (uB.length == 0) {
|
||||
if (Object.keys(uB).length == 0) {
|
||||
this._right.classList.add("empty");
|
||||
this._userTable.innerHTML = `<p class="content">${window.lang.strings("inviteNoUsersCreated")}</p>`;
|
||||
return;
|
||||
@@ -157,7 +157,7 @@ export class DOMInvite implements Invite {
|
||||
this._createdUnix = unix;
|
||||
const el = this._middle.querySelector("strong.inv-created");
|
||||
if (unix == 0) {
|
||||
el.textContent = "n/a";
|
||||
el.textContent = window.lang.strings("unknown");
|
||||
} else {
|
||||
el.textContent = toDateString(new Date(unix*1000));
|
||||
}
|
||||
@@ -479,20 +479,24 @@ export class inviteList implements inviteList {
|
||||
}
|
||||
|
||||
|
||||
function parseInvite(invite: { [f: string]: string | number | string[][] | boolean }): Invite {
|
||||
function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite {
|
||||
let parsed: Invite = {};
|
||||
parsed.code = invite["code"] as string;
|
||||
parsed.email = invite["email"] as string || "";
|
||||
parsed.label = invite["label"] as string || "";
|
||||
let time = "";
|
||||
let userExpiryTime = "";
|
||||
const fields = ["days", "hours", "minutes"];
|
||||
const fields = ["months", "days", "hours", "minutes"];
|
||||
let prefixes = [""];
|
||||
if (invite["user-expiry"] as boolean) { prefixes.push("user-"); }
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
for (let j = 0; j < prefixes.length; j++) {
|
||||
if (invite[prefixes[j]+fields[i]]) {
|
||||
let text = `${invite[prefixes[j]+fields[i]]}${fields[i][0]} `;
|
||||
let abbreviation = fields[i][0];
|
||||
if (fields[i] == "months") {
|
||||
abbreviation += fields[i][1];
|
||||
}
|
||||
let text = `${invite[prefixes[j]+fields[i]]}${abbreviation} `;
|
||||
if (prefixes[j] == "user-") {
|
||||
userExpiryTime += text;
|
||||
} else {
|
||||
@@ -505,8 +509,8 @@ function parseInvite(invite: { [f: string]: string | number | string[][] | boole
|
||||
parsed.userExpiry = invite["user-expiry"] as boolean;
|
||||
parsed.userExpiryTime = userExpiryTime.slice(0, -1);
|
||||
parsed.remainingUses = invite["no-limit"] ? "∞" : String(invite["remaining-uses"])
|
||||
parsed.usedBy = invite["used-by"] as string[][] || [];
|
||||
parsed.created = invite["created"] as string || window.lang.strings("unknown");
|
||||
parsed.usedBy = invite["used-by"] as { [name: string]: number } || {} ;
|
||||
parsed.created = invite["created"] as number || 0;
|
||||
parsed.profile = invite["profile"] as string || "";
|
||||
parsed.notifyExpiry = invite["notify-expiry"] as boolean || false;
|
||||
parsed.notifyCreation = invite["notify-creation"] as boolean || false;
|
||||
@@ -524,9 +528,11 @@ export class createInvite {
|
||||
private _profile = document.getElementById("create-profile") as HTMLSelectElement;
|
||||
private _label = document.getElementById("create-label") as HTMLInputElement;
|
||||
|
||||
private _months = document.getElementById("create-months") as HTMLSelectElement;
|
||||
private _days = document.getElementById("create-days") as HTMLSelectElement;
|
||||
private _hours = document.getElementById("create-hours") as HTMLSelectElement;
|
||||
private _minutes = document.getElementById("create-minutes") as HTMLSelectElement;
|
||||
private _userMonths = document.getElementById("user-months") as HTMLSelectElement;
|
||||
private _userDays = document.getElementById("user-days") as HTMLSelectElement;
|
||||
private _userHours = document.getElementById("user-hours") as HTMLSelectElement;
|
||||
private _userMinutes = document.getElementById("user-minutes") as HTMLSelectElement;
|
||||
@@ -542,7 +548,7 @@ export class createInvite {
|
||||
|
||||
private _count: Number = 30;
|
||||
private _populateNumbers = () => {
|
||||
const fieldIDs = ["days", "hours", "minutes"];
|
||||
const fieldIDs = ["months", "days", "hours", "minutes"];
|
||||
const prefixes = ["create-", "user-"];
|
||||
for (let i = 0; i < fieldIDs.length; i++) {
|
||||
for (let j = 0; j < prefixes.length; j++) {
|
||||
@@ -597,7 +603,7 @@ export class createInvite {
|
||||
set uses(n: number) { this._uses.valueAsNumber = n; }
|
||||
|
||||
private _checkDurationValidity = () => {
|
||||
if (this.days + this.hours + this.minutes == 0) {
|
||||
if (this.months + this.days + this.hours + this.minutes == 0) {
|
||||
this._createButton.setAttribute("disabled", "");
|
||||
this._createButton.onclick = null;
|
||||
} else {
|
||||
@@ -606,6 +612,13 @@ export class createInvite {
|
||||
}
|
||||
}
|
||||
|
||||
get months(): number {
|
||||
return +this._months.value;
|
||||
}
|
||||
set months(n: number) {
|
||||
this._months.value = ""+n;
|
||||
this._checkDurationValidity();
|
||||
}
|
||||
get days(): number {
|
||||
return +this._days.value;
|
||||
}
|
||||
@@ -640,10 +653,17 @@ export class createInvite {
|
||||
parent.classList.add("~neutral");
|
||||
parent.classList.remove("~urge");
|
||||
}
|
||||
this._userMonths.disabled = !enabled;
|
||||
this._userDays.disabled = !enabled;
|
||||
this._userHours.disabled = !enabled;
|
||||
this._userMinutes.disabled = !enabled;
|
||||
}
|
||||
get userMonths(): number {
|
||||
return +this._userMonths.value;
|
||||
}
|
||||
set userMonths(n: number) {
|
||||
this._userMonths.value = ""+n;
|
||||
}
|
||||
get userDays(): number {
|
||||
return +this._userDays.value;
|
||||
}
|
||||
@@ -696,14 +716,16 @@ export class createInvite {
|
||||
create = () => {
|
||||
toggleLoader(this._createButton);
|
||||
let userExpiry = this.userExpiry;
|
||||
if (this.userDays == 0 && this.userHours == 0 && this.userMinutes == 0) {
|
||||
if (this.userMonths == 0 && this.userDays == 0 && this.userHours == 0 && this.userMinutes == 0) {
|
||||
userExpiry = false;
|
||||
}
|
||||
let send = {
|
||||
"months": this.months,
|
||||
"days": this.days,
|
||||
"hours": this.hours,
|
||||
"minutes": this.minutes,
|
||||
"user-expiry": userExpiry,
|
||||
"user-months": this.userMonths,
|
||||
"user-days": this.userDays,
|
||||
"user-hours": this.userHours,
|
||||
"user-minutes": this.userMinutes,
|
||||
@@ -726,6 +748,7 @@ export class createInvite {
|
||||
|
||||
constructor() {
|
||||
this._populateNumbers();
|
||||
this.months = 0;
|
||||
this.days = 0;
|
||||
this.hours = 0;
|
||||
this.minutes = 30;
|
||||
@@ -734,6 +757,7 @@ export class createInvite {
|
||||
this._sendToEnabled.onchange = () => { this.sendToEnabled = this.sendToEnabled; };
|
||||
this.userExpiry = false;
|
||||
this._userExpiryToggle.onchange = () => { this.userExpiry = this._userExpiryToggle.checked; }
|
||||
this._userMonths.disabled = true;
|
||||
this._userDays.disabled = true;
|
||||
this._userHours.disabled = true;
|
||||
this._userMinutes.disabled = true;
|
||||
@@ -769,6 +793,7 @@ export class createInvite {
|
||||
this._invDurationButton.onchange = checkDuration;
|
||||
|
||||
this._days.onchange = this._checkDurationValidity;
|
||||
this._months.onchange = this._checkDurationValidity;
|
||||
this._hours.onchange = this._checkDurationValidity;
|
||||
this._minutes.onchange = this._checkDurationValidity;
|
||||
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
|
||||
|
||||
@@ -3,9 +3,13 @@ declare var window: Window;
|
||||
export class Modal implements Modal {
|
||||
modal: HTMLElement;
|
||||
closeButton: HTMLSpanElement;
|
||||
openEvent: CustomEvent;
|
||||
closeEvent: CustomEvent;
|
||||
constructor(modal: HTMLElement, important: boolean = false) {
|
||||
this.modal = modal;
|
||||
const closeButton = this.modal.querySelector('span.modal-close')
|
||||
this.openEvent = new CustomEvent("modal-open-" + modal.id);
|
||||
this.closeEvent = new CustomEvent("modal-close-" + modal.id);
|
||||
const closeButton = this.modal.querySelector('span.modal-close');
|
||||
if (closeButton !== null) {
|
||||
this.closeButton = closeButton as HTMLSpanElement;
|
||||
this.closeButton.onclick = this.close;
|
||||
@@ -22,15 +26,25 @@ export class Modal implements Modal {
|
||||
}
|
||||
this.modal.classList.add('modal-hiding');
|
||||
const modal = this.modal;
|
||||
const listenerFunc = function () {
|
||||
const listenerFunc = () => {
|
||||
modal.classList.remove('modal-shown');
|
||||
modal.classList.remove('modal-hiding');
|
||||
modal.removeEventListener(window.animationEvent, listenerFunc);
|
||||
document.dispatchEvent(this.closeEvent);
|
||||
};
|
||||
this.modal.addEventListener(window.animationEvent, listenerFunc, false);
|
||||
}
|
||||
|
||||
set onopen(f: () => void) {
|
||||
document.addEventListener("modal-open-"+this.modal.id, f);
|
||||
}
|
||||
set onclose(f: () => void) {
|
||||
document.addEventListener("modal-close-"+this.modal.id, f);
|
||||
}
|
||||
|
||||
show = () => {
|
||||
this.modal.classList.add('modal-shown');
|
||||
document.dispatchEvent(this.openEvent);
|
||||
}
|
||||
toggle = () => {
|
||||
if (this.modal.classList.contains('modal-shown')) {
|
||||
|
||||
@@ -560,10 +560,18 @@ export class settingsList {
|
||||
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
|
||||
if (Boolean(event.detail) !== state) {
|
||||
button.classList.add("unfocused");
|
||||
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false }));
|
||||
} else {
|
||||
button.classList.remove("unfocused");
|
||||
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: true }));
|
||||
}
|
||||
});
|
||||
document.addEventListener(`settings-${dependant[0]}`, (event: settingsBoolEvent) => {
|
||||
if (Boolean(event.detail) !== state) {
|
||||
button.classList.add("unfocused");
|
||||
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false }));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (s.meta.advanced) {
|
||||
document.addEventListener("settings-advancedState", (event: settingsBoolEvent) => {
|
||||
@@ -669,7 +677,7 @@ export class settingsList {
|
||||
if (name in this._sections) {
|
||||
this._sections[name].update(settings.sections[name]);
|
||||
} else {
|
||||
if (name == "email") {
|
||||
if (name == "messages") {
|
||||
const editButton = document.createElement("div");
|
||||
editButton.classList.add("tooltip", "left");
|
||||
editButton.innerHTML = `
|
||||
@@ -677,7 +685,7 @@ export class settingsList {
|
||||
<i class="icon ri-edit-line"></i>
|
||||
</span>
|
||||
<span class="content sm">
|
||||
${window.lang.get("strings", "customizeEmails")}
|
||||
${window.lang.get("strings", "customizeMessages")}
|
||||
</span>
|
||||
`;
|
||||
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = this._emailEditor.showList;
|
||||
@@ -766,9 +774,10 @@ class ombiDefaults {
|
||||
}
|
||||
}
|
||||
|
||||
interface templateEmail {
|
||||
export interface templateEmail {
|
||||
content: string;
|
||||
variables: string[];
|
||||
conditionals: string[];
|
||||
values: { [key: string]: string };
|
||||
html: string;
|
||||
plaintext: string;
|
||||
@@ -788,6 +797,8 @@ class EmailEditor {
|
||||
private _header = document.getElementById("header-editor") as HTMLSpanElement;
|
||||
private _variables = document.getElementById("editor-variables") as HTMLDivElement;
|
||||
private _variablesLabel = document.getElementById("label-editor-variables") as HTMLElement;
|
||||
private _conditionals = document.getElementById("editor-conditionals") as HTMLDivElement;
|
||||
private _conditionalsLabel = document.getElementById("label-editor-conditionals") as HTMLElement;
|
||||
private _textArea = document.getElementById("textarea-editor") as HTMLTextAreaElement;
|
||||
private _preview = document.getElementById("editor-preview") as HTMLDivElement;
|
||||
private _previewContent: HTMLElement;
|
||||
@@ -826,11 +837,11 @@ class EmailEditor {
|
||||
this._templ = req.response as templateEmail;
|
||||
this._textArea.value = this._templ.content;
|
||||
if (this._templ.html == "") {
|
||||
this._preview.innerHTML = `<pre id="preview-content" class="monospace"></pre>`;
|
||||
this._preview.innerHTML = `<pre class="preview-content" class="monospace"></pre>`;
|
||||
} else {
|
||||
this._preview.innerHTML = this._templ.html;
|
||||
}
|
||||
this._previewContent = document.getElementById("preview-content");
|
||||
this._previewContent = this._preview.getElementsByClassName("preview-content")[0] as HTMLElement;
|
||||
this.loadPreview();
|
||||
this._content = this._templ.content;
|
||||
const colors = ["info", "urge", "positive", "neutral"];
|
||||
@@ -845,7 +856,7 @@ class EmailEditor {
|
||||
this._variablesLabel.classList.remove("unfocused");
|
||||
}
|
||||
this._variables.innerHTML = innerHTML
|
||||
const buttons = this._variables.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>;
|
||||
let buttons = this._variables.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>;
|
||||
for (let i = 0; i < this._templ.variables.length; i++) {
|
||||
buttons[i].innerHTML = `<span class="monospace">` + this._templ.variables[i] + `</span>`;
|
||||
buttons[i].onclick = () => {
|
||||
@@ -854,6 +865,27 @@ class EmailEditor {
|
||||
// this._timeout = setTimeout(this.loadPreview, this._finishInterval);
|
||||
}
|
||||
}
|
||||
|
||||
innerHTML = '';
|
||||
if (this._templ.conditionals == null || this._templ.conditionals.length == 0) {
|
||||
this._conditionalsLabel.classList.add("unfocused");
|
||||
} else {
|
||||
for (let i = this._templ.conditionals.length-1; i >= 0; i--) {
|
||||
let ci = i % colors.length;
|
||||
innerHTML += '<span class="button ~' + colors[ci] +' !normal mb-1" style="margin-left: 0.25rem; margin-right: 0.25rem;"></span>'
|
||||
}
|
||||
this._conditionalsLabel.classList.remove("unfocused");
|
||||
this._conditionals.innerHTML = innerHTML
|
||||
buttons = this._conditionals.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>;
|
||||
for (let i = 0; i < this._templ.conditionals.length; i++) {
|
||||
buttons[i].innerHTML = `<span class="monospace">{if ` + this._templ.conditionals[i].slice(1) + `</span>`;
|
||||
buttons[i].onclick = () => {
|
||||
this.insert(this._textArea, "{if " + this._templ.conditionals[i].slice(1) + "{endif}");
|
||||
this.loadPreview();
|
||||
// this._timeout = setTimeout(this.loadPreview, this._finishInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
window.modals.editor.show();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -107,13 +107,20 @@ export class Updater implements updater {
|
||||
_post("/config/update", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
toggleLoader(update);
|
||||
if (req.status != 200) {
|
||||
const success = req.response["success"] as Boolean;
|
||||
if (req.status == 500 && success) {
|
||||
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh"));
|
||||
} else if (req.status != 200) {
|
||||
window.notifications.customError("applyUpdateError", window.lang.notif("errorApplyUpdate"));
|
||||
} else {
|
||||
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateApplied"));
|
||||
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh"));
|
||||
}
|
||||
window.modals.updateInfo.close();
|
||||
}
|
||||
}, true, (req: XMLHttpRequest) => {
|
||||
if (req.status == 0) {
|
||||
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateAppliedRefresh"));
|
||||
}
|
||||
});
|
||||
};
|
||||
this.checkForUpdates(() => {
|
||||
|
||||
@@ -187,8 +187,8 @@ class Select {
|
||||
}
|
||||
|
||||
class LangSelect extends Select {
|
||||
constructor(page: string, el: HTMLElement) {
|
||||
super(el);
|
||||
constructor(page: string, el: HTMLElement, depends?: string, dependsTrue?: boolean, section?: string, setting?: string) {
|
||||
super(el, depends, dependsTrue, section, setting);
|
||||
_get("/lang/" + page, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4 && req.status == 200) {
|
||||
for (let code in req.response) {
|
||||
@@ -256,7 +256,9 @@ const settings = {
|
||||
"password_resets": {
|
||||
"enabled": new Checkbox(get("password_resets-enabled"), "", false, "password_resets", "enabled"),
|
||||
"watch_directory": new Input(get("password_resets-watch_directory"), "", "", "enabled", true, "password_resets"),
|
||||
"subject": new Input(get("password_resets-subject"), "", "", "enabled", true, "password_resets")
|
||||
"subject": new Input(get("password_resets-subject"), "", "", "enabled", true, "password_resets"),
|
||||
"link_reset": new Checkbox(get("password_resets-link_reset"), "enabled", true, "password_resets", "link_reset"),
|
||||
"language": new LangSelect("pwr", get("password_resets-language"), "link_reset", true, "password_resets", "language")
|
||||
},
|
||||
"notifications": {
|
||||
"enabled": new Checkbox(get("notifications-enabled"))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"outDir": "../js",
|
||||
"target": "es6",
|
||||
"lib": ["dom", "es2017"],
|
||||
"typeRoots": ["./typings", "./node_modules/@types"],
|
||||
"typeRoots": ["./typings", "../node_modules/@types"],
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ declare interface Modal {
|
||||
show: () => void;
|
||||
close: (event?: Event) => void;
|
||||
toggle: () => void;
|
||||
onopen: (f: () => void) => void;
|
||||
onclose: (f: () => void) => void;
|
||||
}
|
||||
|
||||
interface ArrayConstructor {
|
||||
@@ -18,6 +20,7 @@ declare interface Window {
|
||||
jfUsers: Array<Object>;
|
||||
notificationsEnabled: boolean;
|
||||
emailEnabled: boolean;
|
||||
telegramEnabled: boolean;
|
||||
ombiEnabled: boolean;
|
||||
usernameEnabled: boolean;
|
||||
token: string;
|
||||
@@ -97,6 +100,7 @@ declare interface Modals {
|
||||
customizeEmails: Modal;
|
||||
extendExpiry: Modal;
|
||||
updateInfo: Modal;
|
||||
telegram: Modal;
|
||||
}
|
||||
|
||||
interface Invite {
|
||||
|
||||
52
updater.go
52
updater.go
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -390,11 +391,13 @@ func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
gz, err := gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
status = -1
|
||||
return
|
||||
}
|
||||
defer gz.Close()
|
||||
tarReader := tar.NewReader(gz)
|
||||
var header *tar.Header
|
||||
for {
|
||||
@@ -410,32 +413,43 @@ func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int
|
||||
case tar.TypeReg:
|
||||
// Search only for file named ud.binary
|
||||
if header.Name == ud.binary {
|
||||
var file string
|
||||
file, err = os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var path string
|
||||
path, err = filepath.EvalSymlinks(file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var info fs.FileInfo
|
||||
info, err = os.Stat(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mode := info.Mode()
|
||||
var f *os.File
|
||||
f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, tarReader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
applyUpdate = func() error {
|
||||
defer gz.Close()
|
||||
defer resp.Body.Close()
|
||||
file, err := os.Executable()
|
||||
oldName := path + "-" + version + "-" + commit
|
||||
err := os.Rename(path, oldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, err := filepath.EvalSymlinks(file)
|
||||
err = os.Rename(path+"_", path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mode := info.Mode()
|
||||
f, err := os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, tarReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(path+"_", path)
|
||||
return os.Remove(oldName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,9 +71,9 @@ func (app *appContext) checkUsers() {
|
||||
mode = "delete"
|
||||
termPlural = "Deleting"
|
||||
}
|
||||
email := false
|
||||
if emailEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) {
|
||||
email = true
|
||||
contact := false
|
||||
if messagesEnabled && app.config.Section("user_expiry").Key("send_email").MustBool(true) {
|
||||
contact = true
|
||||
}
|
||||
// Use a map to speed up checking for deleted users later
|
||||
userExists := map[string]bool{}
|
||||
@@ -114,18 +114,18 @@ func (app *appContext) checkUsers() {
|
||||
}
|
||||
delete(app.storage.users, id)
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
if email {
|
||||
address, ok := app.storage.emails[id]
|
||||
if contact {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name := app.getAddressOrName(user.ID)
|
||||
msg, err := app.email.constructUserExpired(app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct expiry email for \"%s\": %s", user.Name, err)
|
||||
} else if err := app.email.send(msg, address.(string)); err != nil {
|
||||
app.err.Printf("Failed to send expiry email to \"%s\": %s", user.Name, err)
|
||||
app.err.Printf("Failed to construct expiry message for \"%s\": %s", user.Name, err)
|
||||
} else if err := app.sendByID(msg, user.ID); err != nil {
|
||||
app.err.Printf("Failed to send expiry message to \"%s\": %s", name, err)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to \"%s\"", address.(string))
|
||||
app.info.Printf("Sent expiry notification to \"%s\"", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
121
views.go
121
views.go
@@ -53,19 +53,58 @@ func (app *appContext) pushResources(gc *gin.Context, admin bool) {
|
||||
gc.Header("Link", cssHeader)
|
||||
}
|
||||
|
||||
func (app *appContext) getLang(gc *gin.Context, chosen string) string {
|
||||
type Page int
|
||||
|
||||
const (
|
||||
AdminPage Page = iota + 1
|
||||
FormPage
|
||||
PWRPage
|
||||
)
|
||||
|
||||
func (app *appContext) getLang(gc *gin.Context, page Page, chosen string) string {
|
||||
lang := gc.Query("lang")
|
||||
if lang == "" {
|
||||
lang = chosen
|
||||
} else if _, ok := app.storage.lang.Admin[lang]; !ok {
|
||||
lang = chosen
|
||||
cookie, err := gc.Cookie("lang")
|
||||
if lang != "" {
|
||||
switch page {
|
||||
case AdminPage:
|
||||
if _, ok := app.storage.lang.Admin[lang]; ok {
|
||||
gc.SetCookie("lang", lang, (365 * 3600), "/", gc.Request.URL.Hostname(), true, true)
|
||||
return lang
|
||||
}
|
||||
case FormPage:
|
||||
if _, ok := app.storage.lang.Form[lang]; ok {
|
||||
gc.SetCookie("lang", lang, (365 * 3600), "/", gc.Request.URL.Hostname(), true, true)
|
||||
return lang
|
||||
}
|
||||
case PWRPage:
|
||||
if _, ok := app.storage.lang.PasswordReset[lang]; ok {
|
||||
gc.SetCookie("lang", lang, (365 * 3600), "/", gc.Request.URL.Hostname(), true, true)
|
||||
return lang
|
||||
}
|
||||
}
|
||||
}
|
||||
return lang
|
||||
if cookie != "" && err == nil {
|
||||
switch page {
|
||||
case AdminPage:
|
||||
if _, ok := app.storage.lang.Admin[cookie]; ok {
|
||||
return cookie
|
||||
}
|
||||
case FormPage:
|
||||
if _, ok := app.storage.lang.Form[cookie]; ok {
|
||||
return cookie
|
||||
}
|
||||
case PWRPage:
|
||||
if _, ok := app.storage.lang.PasswordReset[cookie]; ok {
|
||||
return cookie
|
||||
}
|
||||
}
|
||||
}
|
||||
return chosen
|
||||
}
|
||||
|
||||
func (app *appContext) AdminPage(gc *gin.Context) {
|
||||
app.pushResources(gc, true)
|
||||
lang := app.getLang(gc, app.storage.lang.chosenAdminLang)
|
||||
lang := app.getLang(gc, AdminPage, app.storage.lang.chosenAdminLang)
|
||||
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
|
||||
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
|
||||
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
|
||||
@@ -77,20 +116,21 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
||||
}
|
||||
license = string(l)
|
||||
gcHTML(gc, http.StatusOK, "admin.html", gin.H{
|
||||
"urlBase": app.getURLBase(gc),
|
||||
"cssClass": app.cssClass,
|
||||
"contactMessage": "",
|
||||
"email_enabled": emailEnabled,
|
||||
"notifications": notificationsEnabled,
|
||||
"version": version,
|
||||
"commit": commit,
|
||||
"ombiEnabled": ombiEnabled,
|
||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||
"strings": app.storage.lang.Admin[lang].Strings,
|
||||
"quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
|
||||
"language": app.storage.lang.Admin[lang].JSON,
|
||||
"langName": lang,
|
||||
"license": license,
|
||||
"urlBase": app.getURLBase(gc),
|
||||
"cssClass": app.cssClass,
|
||||
"contactMessage": "",
|
||||
"email_enabled": emailEnabled,
|
||||
"telegram_enabled": telegramEnabled,
|
||||
"notifications": notificationsEnabled,
|
||||
"version": version,
|
||||
"commit": commit,
|
||||
"ombiEnabled": ombiEnabled,
|
||||
"username": !app.config.Section("email").Key("no_username").MustBool(false),
|
||||
"strings": app.storage.lang.Admin[lang].Strings,
|
||||
"quantityStrings": app.storage.lang.Admin[lang].QuantityStrings,
|
||||
"language": app.storage.lang.Admin[lang].JSON,
|
||||
"langName": lang,
|
||||
"license": license,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -101,12 +141,13 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
app.pushResources(gc, false)
|
||||
lang := app.getLang(gc, app.storage.lang.chosenPWRLang)
|
||||
lang := app.getLang(gc, PWRPage, app.storage.lang.chosenPWRLang)
|
||||
data := gin.H{
|
||||
"urlBase": app.getURLBase(gc),
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
"strings": app.storage.lang.PasswordReset[lang].Strings,
|
||||
"success": false,
|
||||
"ombiEnabled": app.config.Section("ombi").Key("enabled").MustBool(false),
|
||||
}
|
||||
resp, status, err := app.jf.ResetPassword(pin)
|
||||
if status == 200 && err == nil && resp.Success {
|
||||
@@ -115,13 +156,32 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
|
||||
} else {
|
||||
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
||||
}
|
||||
gcHTML(gc, http.StatusOK, "password-reset.html", data)
|
||||
defer gcHTML(gc, http.StatusOK, "password-reset.html", data)
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
jfUser, status, err := app.jf.UserByName(resp.UsersReset[0], false)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", resp.UsersReset[0], status, err)
|
||||
return
|
||||
}
|
||||
ombiUser, status, err := app.getOmbiUser(jfUser.ID)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", resp.UsersReset[0], status, err)
|
||||
return
|
||||
}
|
||||
ombiUser["password"] = pin
|
||||
status, err = app.ombi.ModifyUser(ombiUser)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
|
||||
return
|
||||
}
|
||||
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
app.pushResources(gc, false)
|
||||
code := gc.Param("invCode")
|
||||
lang := app.getLang(gc, app.storage.lang.chosenFormLang)
|
||||
lang := app.getLang(gc, FormPage, app.storage.lang.chosenFormLang)
|
||||
/* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */
|
||||
// if app.checkInvite(code, false, "") {
|
||||
inv, ok := app.storage.invites[code]
|
||||
@@ -200,7 +260,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
if strings.Contains(email, "Failed") {
|
||||
email = ""
|
||||
}
|
||||
gcHTML(gc, http.StatusOK, "form-loader.html", gin.H{
|
||||
data := gin.H{
|
||||
"urlBase": app.getURLBase(gc),
|
||||
"cssClass": app.cssClass,
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
@@ -217,12 +277,21 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
"code": code,
|
||||
"confirmation": app.config.Section("email_confirmation").Key("enabled").MustBool(false),
|
||||
"userExpiry": inv.UserExpiry,
|
||||
"userExpiryMonths": inv.UserMonths,
|
||||
"userExpiryDays": inv.UserDays,
|
||||
"userExpiryHours": inv.UserHours,
|
||||
"userExpiryMinutes": inv.UserMinutes,
|
||||
"userExpiryMessage": app.storage.lang.Form[lang].Strings.get("yourAccountIsValidUntil"),
|
||||
"langName": lang,
|
||||
})
|
||||
"telegramEnabled": telegramEnabled,
|
||||
}
|
||||
if data["telegramEnabled"].(bool) {
|
||||
data["telegramPIN"] = app.telegram.NewAuthToken()
|
||||
data["telegramUsername"] = app.telegram.username
|
||||
data["telegramURL"] = app.telegram.link
|
||||
data["telegramRequired"] = app.config.Section("telegram").Key("required").MustBool(false)
|
||||
}
|
||||
gcHTML(gc, http.StatusOK, "form-loader.html", data)
|
||||
}
|
||||
|
||||
func (app *appContext) NoRouteHandler(gc *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user