mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-01-19 00:57:37 +01:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
d51a6abb02 | ||
|
|
374ffbf01f | ||
|
|
871bc9f396 | ||
|
|
66b7df7cde | ||
|
|
bc76770ca4 | ||
|
|
7196361cf6 | ||
|
|
3e73d16cce | ||
|
|
3f8414c70a | ||
|
|
6ec2186bdf | ||
|
|
6dd575b276 | ||
|
|
1a98946d71 | ||
|
|
8922549bdb | ||
|
|
173b49aeb7 | ||
|
|
eee6046465 | ||
|
|
b76011be4f | ||
|
|
3d93d79b0b | ||
|
|
7dcc9b20a1 | ||
|
|
754b956206 | ||
|
|
47ac505cac | ||
|
|
e6e5231f63 | ||
|
|
78049d4a33 | ||
|
|
8a6cfe0b4d | ||
|
|
afedc78113 | ||
|
|
76b822213e | ||
|
|
ab3d5f3321 | ||
|
|
e1d42c8a87 | ||
|
|
f53c852a4d | ||
|
|
aaea889e47 | ||
|
|
bf98c74ecf | ||
|
|
fcadabd339 | ||
|
|
2a0edeb3c5 | ||
|
|
30f16e7207 | ||
|
|
dbe7e2e659 | ||
|
|
e16f05b130 | ||
|
|
07573a515a | ||
|
|
b3a2de50cf | ||
|
|
5388d3d4c0 | ||
|
|
c392d48174 | ||
|
|
967fab3411 | ||
|
|
d7845b78f6 | ||
|
|
a253858625 | ||
|
|
ad1aae16e3 | ||
|
|
9370913ace | ||
|
|
dcd2e234e8 | ||
|
|
762dac2581 | ||
|
|
1cf8d3037b | ||
|
|
40808bdcb9 | ||
|
|
2451d69341 | ||
|
|
e449853568 | ||
|
|
2082e960c2 | ||
|
|
7b2a083f98 | ||
|
|
270143a8f6 | ||
|
|
766b69d95e | ||
|
|
f5addc4947 | ||
|
|
55eb59c526 | ||
|
|
679cac4dbd | ||
|
|
a0a25d64f1 | ||
|
|
9875458b01 | ||
|
|
f0dccc58aa | ||
|
|
636bc22d52 | ||
|
|
fc6b6a9c6b | ||
|
|
1a6d78352c | ||
|
|
e351c35cc8 |
18
.drone.yml
18
.drone.yml
@@ -16,10 +16,10 @@ steps:
|
||||
GITHUB_TOKEN:
|
||||
from_secret: github_token
|
||||
commands:
|
||||
- apt update -y
|
||||
- apt install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- apt-get update -y
|
||||
- apt-get install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||
- apt install nodejs
|
||||
- apt-get install nodejs
|
||||
- curl -sL https://git.io/goreleaser > ../goreleaser
|
||||
- chmod +x ../goreleaser
|
||||
- ./scripts/version.sh ../goreleaser
|
||||
@@ -73,10 +73,10 @@ steps:
|
||||
- name: build
|
||||
image: golang:latest
|
||||
commands:
|
||||
- apt update -y
|
||||
- apt install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- apt-get update -y
|
||||
- apt-get install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||
- apt install nodejs
|
||||
- apt-get install nodejs
|
||||
- curl -sL https://git.io/goreleaser > goreleaser
|
||||
- chmod +x goreleaser
|
||||
- ./scripts/version.sh ./goreleaser --snapshot --skip-publish --rm-dist
|
||||
@@ -143,10 +143,10 @@ steps:
|
||||
- name: build
|
||||
image: golang:latest
|
||||
commands:
|
||||
- apt update -y
|
||||
- apt install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- apt-get update -y
|
||||
- apt-get install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||
- apt install nodejs
|
||||
- apt-get install nodejs
|
||||
- curl -sL https://git.io/goreleaser > goreleaser
|
||||
- chmod +x goreleaser
|
||||
- ./scripts/version.sh ./goreleaser --snapshot --skip-publish --rm-dist
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,7 +4,6 @@ dist/
|
||||
build/
|
||||
data/
|
||||
version.go
|
||||
embed.go
|
||||
notes
|
||||
docs/*
|
||||
lang/langtostruct.py
|
||||
@@ -14,3 +13,4 @@ server.key
|
||||
server.pem
|
||||
server.crt
|
||||
instructions-debian.txt
|
||||
cl.md
|
||||
|
||||
@@ -22,11 +22,11 @@ before:
|
||||
- python3 scripts/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
||||
- python3 scripts/compile_mjml.py -o data/
|
||||
- npx esbuild --bundle ts/admin.ts --outfile=./data/web/js/admin.js --minify
|
||||
- npx esbuild --bundle ts/pwr.ts --outfile=./data/web/js/pwr.js --minify
|
||||
- npx esbuild --bundle ts/form.ts --outfile=./data/web/js/form.js --minify
|
||||
- npx esbuild --bundle ts/setup.ts --outfile=./data/web/js/setup.js --minify
|
||||
- go get -u github.com/swaggo/swag/cmd/swag
|
||||
- swag init -g main.go
|
||||
- python3 scripts/embed.py internal
|
||||
builds:
|
||||
- dir: ./
|
||||
env:
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
#### Code
|
||||
I use 4 spaces for indentation. Go should ideally be formatted with `goimports` and/or `gofmt`. I don't use a formatter on typescript, so don't worry about that.
|
||||
|
||||
If you need to test your changes:
|
||||
* `make debug` will build everything, and include sourcemaps for typescript. This should be the first thing you run.
|
||||
* `make compile` compiles go into `build/jfa-go`.
|
||||
* `make ts-debug` will compile typescript w/ sourcemaps into `build/data/web/js`.
|
||||
* `make copy` will copy css, html, language and static files into `build/data`.
|
||||
#### Compiling
|
||||
|
||||
Prefix each of these with `make DEBUG=on INTERNAL=off `:
|
||||
* `all` will download deps and build everything. The executable and data will be placed in `build`. This is only necessary the first time.
|
||||
* `compile` will only compile go code into the `build/jfa-go` executable.
|
||||
* `typescript` will compile typescript w/ sourcemaps into `build/data/web/js`.
|
||||
* `bundle-css` will bundle CSS and place it in `build/data/web/css`.
|
||||
* `configuration` will generate the `config-base.json` (used to render settings in the web ui) and `config-default.ini` and put them in `build/data`.
|
||||
* `email` will compile email mjml, and copy the text versions in to `build/data`.
|
||||
* `copy` will copy iconography, html, language files and static data into `build/data`.
|
||||
|
||||
See the [wiki](https://github.com/hrfee/jfa-go/wiki/Build) for more info.
|
||||
|
||||
@@ -6,7 +6,7 @@ RUN apt-get update -y \
|
||||
&& apt-get install build-essential python3-pip curl software-properties-common sed -y \
|
||||
&& (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \
|
||||
&& apt-get install nodejs \
|
||||
&& (cd /opt/build; make configuration npm email typescript bundle-css swagger copy external-files GOESBUILD=on) \
|
||||
&& (cd /opt/build; make configuration npm email typescript bundle-css swagger copy INTERNAL=off GOESBUILD=on) \
|
||||
&& sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' /opt/build/build/data/html/setup.html
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ ENV GOARCH=$TARGETARCH
|
||||
|
||||
COPY --from=support /opt/build /opt/build
|
||||
|
||||
RUN (cd /opt/build; make compile UPDATER=docker)
|
||||
RUN (cd /opt/build; make compile INTERNAL=off UPDATER=docker)
|
||||
|
||||
FROM golang:latest
|
||||
|
||||
|
||||
111
Makefile
111
Makefile
@@ -11,11 +11,33 @@ VERSION := $(shell echo $(VERSION) | sed 's/v//g')
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD || echo unknown)
|
||||
|
||||
UPDATER ?= off
|
||||
BUILDFLAGS := -X main.version=$(VERSION) -X main.commit=$(COMMIT)
|
||||
LDFLAGS := -X main.version=$(VERSION) -X main.commit=$(COMMIT)
|
||||
ifeq ($(UPDATER), on)
|
||||
BUILDFLAGS := $(BUILDFLAGS) -X main.updater=binary
|
||||
LDFLAGS := $(LDFLAGS) -X main.updater=binary
|
||||
else ifneq ($(UPDATER), off)
|
||||
BUILDFLAGS := $(BUILDFLAGS) -X main.updater=$(UPDATER)
|
||||
LDFLAGS := $(LDFLAGS) -X main.updater=$(UPDATER)
|
||||
endif
|
||||
|
||||
INTERNAL ?= on
|
||||
ifeq ($(INTERNAL), on)
|
||||
TAGS :=
|
||||
DATA := data
|
||||
else
|
||||
DATA := build/data
|
||||
TAGS := -tags external
|
||||
endif
|
||||
|
||||
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:
|
||||
@@ -29,31 +51,24 @@ npm:
|
||||
|
||||
configuration:
|
||||
$(info Fixing config-base)
|
||||
-mkdir -p data
|
||||
python3 scripts/enumerate_config.py -i config/config-base.json -o data/config-base.json
|
||||
-mkdir -p $(DATA)
|
||||
python3 scripts/enumerate_config.py -i config/config-base.json -o $(DATA)/config-base.json
|
||||
$(info Generating config-default.ini)
|
||||
python3 scripts/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
||||
python3 scripts/generate_ini.py -i config/config-base.json -o $(DATA)/config-default.ini
|
||||
|
||||
email:
|
||||
$(info Generating email html)
|
||||
python3 scripts/compile_mjml.py -o data/
|
||||
python3 scripts/compile_mjml.py -o $(DATA)/
|
||||
|
||||
typescript:
|
||||
$(TYPECHECK)
|
||||
$(info compiling typescript)
|
||||
-mkdir -p data/web/js
|
||||
-$(ESBUILD) --bundle ts/admin.ts --outfile=./data/web/js/admin.js --minify
|
||||
-$(ESBUILD) --bundle ts/form.ts --outfile=./data/web/js/form.js --minify
|
||||
-$(ESBUILD) --bundle ts/setup.ts --outfile=./data/web/js/setup.js --minify
|
||||
|
||||
ts-debug:
|
||||
$(info compiling typescript w/ sourcemaps)
|
||||
-mkdir -p data/web/js
|
||||
-$(ESBUILD) --bundle ts/admin.ts --sourcemap --outfile=./data/web/js/admin.js
|
||||
-$(ESBUILD) --bundle ts/form.ts --sourcemap --outfile=./data/web/js/form.js
|
||||
-$(ESBUILD) --bundle ts/setup.ts --sourcemap --outfile=./data/web/js/setup.js
|
||||
-rm -r data/web/js/ts
|
||||
$(info copying typescript)
|
||||
cp -r ts data/web/js
|
||||
-mkdir -p $(DATA)/web/js
|
||||
-$(ESBUILD) --bundle ts/admin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/admin.js --minify
|
||||
-$(ESBUILD) --bundle ts/pwr.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr.js --minify
|
||||
-$(ESBUILD) --bundle ts/form.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/form.js --minify
|
||||
-$(ESBUILD) --bundle ts/setup.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/setup.js --minify
|
||||
$(COPYTS)
|
||||
|
||||
swagger:
|
||||
$(GOBINARY) get github.com/swaggo/swag/cmd/swag
|
||||
@@ -64,47 +79,47 @@ compile:
|
||||
$(GOBINARY) mod download
|
||||
$(info Building)
|
||||
mkdir -p build
|
||||
cd build && CGO_ENABLED=0 $(GOBINARY) build -ldflags="-s -w $(BUILDFLAGS)" -o ./jfa-go ../*.go
|
||||
|
||||
compile-debug:
|
||||
$(info Downloading deps)
|
||||
$(GOBINARY) mod download
|
||||
$(info Building)
|
||||
mkdir -p build
|
||||
cd build && CGO_ENABLED=0 $(GOBINARY) build -ldflags "$(BUILDFLAGS)" -o ./jfa-go ../*.go
|
||||
CGO_ENABLED=0 $(GOBINARY) build -ldflags="-s -w $(LDFLAGS)" $(TAGS) -o build/jfa-go
|
||||
|
||||
compress:
|
||||
upx --lzma build/jfa-go
|
||||
|
||||
bundle-css:
|
||||
-mkdir -p data/web/css
|
||||
-mkdir -p $(DATA)/web/css
|
||||
$(info bundling css)
|
||||
$(ESBUILD) --bundle css/base.css --outfile=data/web/css/bundle.css --external:remixicon.css --minify
|
||||
$(ESBUILD) --bundle css/base.css --outfile=$(DATA)/web/css/bundle.css --external:remixicon.css --minify
|
||||
|
||||
copy:
|
||||
$(info copying fonts)
|
||||
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 data/web/css/
|
||||
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
|
||||
$(info copying html)
|
||||
cp -r html data/
|
||||
cp -r html $(DATA)/
|
||||
$(info copying static data)
|
||||
-mkdir -p data/web
|
||||
cp -r static/* data/web/
|
||||
-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/
|
||||
cp -r lang $(DATA)/
|
||||
cp LICENSE $(DATA)/
|
||||
|
||||
internal-files:
|
||||
python3 scripts/embed.py internal
|
||||
|
||||
external-files:
|
||||
python3 scripts/embed.py external
|
||||
-mkdir -p build
|
||||
$(info copying internal data into build/)
|
||||
cp -r data build/
|
||||
# internal-files:
|
||||
# python3 scripts/embed.py internal
|
||||
#
|
||||
# external-files:
|
||||
# python3 scripts/embed.py external
|
||||
# -mkdir -p build
|
||||
# $(info copying internal data into build/)
|
||||
# cp -r data build/
|
||||
|
||||
install:
|
||||
cp -r build $(DESTDIR)/jfa-go
|
||||
|
||||
all: configuration npm email typescript bundle-css swagger copy internal-files compile
|
||||
all-external: configuration npm email typescript bundle-css swagger copy external-files compile
|
||||
debug: configuration npm email ts-debug bundle-css swagger copy external-files compile-debug
|
||||
clean:
|
||||
-rm -r $(DATA)
|
||||
-rm -r build
|
||||
-rm mail/*.html
|
||||
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
|
||||
go clean
|
||||
|
||||
all: configuration npm email typescript bundle-css swagger copy compile
|
||||
|
||||
19
README.md
19
README.md
@@ -8,26 +8,24 @@
|
||||
---
|
||||
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
|
||||
* Granular control over invites: Validity period as well as number of uses can be specified.
|
||||
* Account profiles: Assign settings profiles to invites so new users have your predefined permissions, homescreen layout, etc. applied to their account on creation.
|
||||
* Password validation: Ensure users choose a strong password.
|
||||
* ⌛ User expiry: Specify a validity period, and new user's accounts will be disabled/deleted after it. The period can be manually extended too.
|
||||
* ⌛ 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.
|
||||
* 📨 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 user's 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.
|
||||
* Notifications: Get notified when someone creates an account, or an invite expires.
|
||||
* 📣 Announcements: Bulk email 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
|
||||
|
||||
@@ -44,7 +42,7 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
|
||||
|
||||
#### Install
|
||||
|
||||
The [Docker](https://hub.docker.com/repository/docker/hrfee/jfa-go) image is your best bet.
|
||||
The [Docker](https://hub.docker.com/r/hrfee/jfa-go) image is your best bet.
|
||||
```sh
|
||||
docker create \
|
||||
--name "jfa-go" \ # Whatever you want to name it
|
||||
@@ -71,8 +69,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 +85,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)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// TimeoutHandler recovers from an http timeout.
|
||||
// TimeoutHandler recovers from an http timeout or panic.
|
||||
type TimeoutHandler func()
|
||||
|
||||
// NewTimeoutHandler returns a new Timeout handler.
|
||||
|
||||
@@ -64,6 +64,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")
|
||||
|
||||
@@ -121,6 +127,7 @@ 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.email = NewEmailer(app)
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -468,6 +468,27 @@
|
||||
"value": "/path/to/jellyfin",
|
||||
"description": "Path to the folder Jellyfin puts password-reset files."
|
||||
},
|
||||
"link_reset": {
|
||||
"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. Must be enabled to reset Ombi password at the same time as the Jellyfin password."
|
||||
},
|
||||
"language": {
|
||||
"name": "Default reset link language",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "link_reset",
|
||||
"type": "select",
|
||||
"options": [
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default language for password reset success screen."
|
||||
},
|
||||
"email_html": {
|
||||
"name": "Custom email (HTML)",
|
||||
"required": false,
|
||||
@@ -701,7 +722,7 @@
|
||||
"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": {
|
||||
@@ -878,6 +899,68 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable_enable": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Account Disabling/Enabling",
|
||||
"description": "Subject/email files for account disabling/enabling emails.",
|
||||
"depends_true": "email|method"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deletion": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -52,6 +52,11 @@
|
||||
padding: var(--spacing-4,1rem);
|
||||
}
|
||||
|
||||
.modal-content .banner {
|
||||
margin-left: calc(-1 * var(--spacing-4,1rem) - 0.5%); /* Not sure why this is necessary */
|
||||
margin-right: calc(-1 * var(--spacing-4,1rem) - 0.5%);
|
||||
}
|
||||
|
||||
div.card:contains(section.banner.footer) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
@@ -96,6 +101,10 @@ div.card:contains(section.banner.footer) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.pb-1 {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
280
email.go
280
email.go
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
textTemplate "text/template"
|
||||
@@ -18,8 +19,8 @@ import (
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/itchyny/timefmt-go"
|
||||
jEmail "github.com/jordan-wright/email"
|
||||
"github.com/knz/strtime"
|
||||
"github.com/mailgun/mailgun-go/v4"
|
||||
)
|
||||
|
||||
@@ -28,6 +29,13 @@ type emailClient interface {
|
||||
send(fromName, fromAddr string, email *Email, address ...string) error
|
||||
}
|
||||
|
||||
type dummyClient struct{}
|
||||
|
||||
func (dc *dummyClient) send(fromName, fromAddr string, email *Email, 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.
|
||||
type Mailgun struct {
|
||||
client *mailgun.MailgunImpl
|
||||
@@ -100,21 +108,21 @@ type Email struct {
|
||||
}
|
||||
|
||||
func (emailer *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern, timePattern string) (d, t, expiresIn string) {
|
||||
d, _ = strtime.Strftime(expiry, datePattern)
|
||||
t, _ = strtime.Strftime(expiry, timePattern)
|
||||
d = timefmt.Format(expiry, datePattern)
|
||||
t = timefmt.Format(expiry, timePattern)
|
||||
currentTime := time.Now()
|
||||
if tzaware {
|
||||
currentTime = currentTime.UTC()
|
||||
}
|
||||
_, _, days, hours, minutes, _ := timeDiff(expiry, currentTime)
|
||||
if days != 0 {
|
||||
expiresIn += fmt.Sprintf("%dd ", days)
|
||||
expiresIn += strconv.Itoa(days) + "d "
|
||||
}
|
||||
if hours != 0 {
|
||||
expiresIn += fmt.Sprintf("%dh ", hours)
|
||||
expiresIn += strconv.Itoa(hours) + "h "
|
||||
}
|
||||
if minutes != 0 {
|
||||
expiresIn += fmt.Sprintf("%dm ", minutes)
|
||||
expiresIn += strconv.Itoa(minutes) + "m "
|
||||
}
|
||||
expiresIn = strings.TrimSuffix(expiresIn, " ")
|
||||
return
|
||||
@@ -145,6 +153,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
|
||||
}
|
||||
@@ -264,13 +274,12 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
|
||||
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)
|
||||
@@ -331,18 +340,17 @@ func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext
|
||||
|
||||
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")),
|
||||
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)
|
||||
@@ -376,13 +384,12 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
|
||||
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)
|
||||
@@ -431,13 +438,12 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
|
||||
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)
|
||||
@@ -453,15 +459,21 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo
|
||||
message := app.config.Section("email").Key("message").String()
|
||||
template := map[string]interface{}{
|
||||
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
|
||||
"ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"),
|
||||
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
|
||||
"pinString": emailer.lang.PasswordReset.get("pin"),
|
||||
"link_reset": false,
|
||||
"message": "",
|
||||
"username": pwr.Username,
|
||||
"date": d,
|
||||
"time": t,
|
||||
"expiresInMinutes": expiresIn,
|
||||
}
|
||||
linkResetEnabled := app.config.Section("password_resets").Key("link_reset").MustBool(false)
|
||||
if linkResetEnabled {
|
||||
template["ifItWasYou"] = emailer.lang.PasswordReset.get("ifItWasYouLink")
|
||||
} else {
|
||||
template["ifItWasYou"] = emailer.lang.PasswordReset.get("ifItWasYou")
|
||||
}
|
||||
if noSub {
|
||||
template["helloUser"] = emailer.lang.Strings.get("helloUser")
|
||||
template["codeExpiry"] = emailer.lang.PasswordReset.get("codeExpiry")
|
||||
@@ -472,7 +484,22 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo
|
||||
} else {
|
||||
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": pwr.Username})
|
||||
template["codeExpiry"] = emailer.lang.PasswordReset.template("codeExpiry", tmpl{"date": d, "time": t, "expiresInMinutes": expiresIn})
|
||||
template["pin"] = pwr.Pin
|
||||
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
||||
if linkResetEnabled {
|
||||
if inviteLink != "" {
|
||||
// Strip /invite form end of this URL, ik its ugly.
|
||||
template["link_reset"] = true
|
||||
pinLink := fmt.Sprintf("%s/reset?pin=%s", strings.Replace(inviteLink, "/invite", "", 1), pwr.Pin)
|
||||
template["pin"] = pinLink
|
||||
// Only used in html email.
|
||||
template["pin_code"] = pwr.Pin
|
||||
} else {
|
||||
app.info.Println("Password Reset link disabled as no URL Base provided. Set in Settings > Invite Emails.")
|
||||
template["pin"] = pwr.Pin
|
||||
}
|
||||
} else {
|
||||
template["pin"] = pwr.Pin
|
||||
}
|
||||
template["message"] = message
|
||||
}
|
||||
return template
|
||||
@@ -485,13 +512,12 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub
|
||||
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)
|
||||
@@ -504,9 +530,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"}
|
||||
@@ -527,13 +553,12 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b
|
||||
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)
|
||||
@@ -544,16 +569,99 @@ 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("email").Key("message").String()
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructDisabled(reason string, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
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, 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("email").Key("message").String()
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
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, 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 + "}"
|
||||
}
|
||||
@@ -561,24 +669,43 @@ func (emailer *Emailer) welcomeValues(username string, app *appContext, noSub bo
|
||||
template["jellyfinURL"] = app.config.Section("jellyfin").Key("public_server").String()
|
||||
template["username"] = username
|
||||
template["message"] = app.config.Section("email").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) {
|
||||
func (emailer *Emailer) constructWelcome(username string, expiry time.Time, app *appContext, noSub bool) (*Email, error) {
|
||||
email := &Email{
|
||||
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)
|
||||
@@ -608,13 +735,12 @@ func (emailer *Emailer) constructUserExpired(app *appContext, noSub bool) (*Emai
|
||||
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)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
`scripts/embed.py [internal/external]` will copy the respective file into the main directory. If internal, `//go:embed` is used to embed the `data/` directory in the binary. If external, `os.DirFS` is used to access the `data/` directory, which should be placed next to the executable.
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build external
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
18
go.mod
18
go.mod
@@ -4,16 +4,14 @@ go 1.16
|
||||
|
||||
replace github.com/hrfee/jfa-go/docs => ./docs
|
||||
|
||||
replace github.com/hrfee/jfa-go/mediabrowser => ./mediabrowser
|
||||
|
||||
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/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/evanw/esbuild v0.8.50 // indirect
|
||||
github.com/fatih/color v1.10.0
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/gin-contrib/pprof v1.3.0
|
||||
@@ -21,30 +19,32 @@ require (
|
||||
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/golang/protobuf v1.4.3 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20210208175418-bda154fe17d8
|
||||
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/mediabrowser v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/jfa-go/logger v0.0.0-00010101000000-000000000000 // indirect
|
||||
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/knz/strtime v0.0.0-20200924090105-187c67f2bf5e
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // 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/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
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b // indirect
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c // indirect
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 // indirect
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
|
||||
34
go.sum
34
go.sum
@@ -17,8 +17,6 @@ github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJ
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -32,8 +30,6 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanw/esbuild v0.8.50 h1:97YxSC9Ni9zu82601vI93cSUS0C+WUcPPNIARuGcQtI=
|
||||
github.com/evanw/esbuild v0.8.50/go.mod h1:y2AFBAGVelPqPodpdtxWWqe6n2jYf5FrsJbligmRmuw=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
|
||||
@@ -86,8 +82,9 @@ github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/
|
||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY=
|
||||
github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng=
|
||||
github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
@@ -130,6 +127,10 @@ 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/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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
@@ -142,8 +143,6 @@ github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/knz/strtime v0.0.0-20200924090105-187c67f2bf5e h1:ViPE0JEOvtw5I0EGUiFSr2VNKGNU+3oBT+oHbDXHbxk=
|
||||
github.com/knz/strtime v0.0.0-20200924090105-187c67f2bf5e/go.mod h1:4ZxfWkxwtc7dBeifERVVWRy9F9rTU9p0yCDgeCtlius=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -190,8 +189,6 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCb
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
@@ -200,6 +197,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
@@ -268,10 +267,9 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c h1:KHUzaHIpjWVlVVNh65G3hhuj3KB1HnjY6Cq5cTvRQT8=
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -291,18 +289,12 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b h1:ggRgirZABFolTmi3sn6Ivd9SipZwLedQ5wR0aAKnFxU=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
215
html/admin.html
215
html/admin.html
@@ -40,11 +40,24 @@
|
||||
</div>
|
||||
<div id="modal-about" class="modal">
|
||||
<div class="modal-content content card">
|
||||
<span class="heading">{{ .strings.aboutProgram }} <span class="modal-close">×</span></span>
|
||||
<img src="{{ .urlBase }}/banner.svg" class="mt-1" alt="jfa-go banner">
|
||||
<img src="{{ .urlBase }}/banner.svg" class="banner header" alt="jfa-go banner">
|
||||
<span class="heading"><span class="modal-close">×</span></span>
|
||||
<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 +112,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,18 +156,24 @@
|
||||
</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>
|
||||
@@ -165,6 +202,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>
|
||||
@@ -276,7 +315,16 @@
|
||||
<span class="ml-1 chev"></span>
|
||||
</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral !low" id="lang-list">
|
||||
<div class="card ~neutral !low">
|
||||
<label class="switch pb-1">
|
||||
<input type="radio" name="lang-time" id="lang-12h">
|
||||
<span>{{ .strings.time12h }}</span>
|
||||
</label>
|
||||
<label class="switch pb-1">
|
||||
<input type="radio" name="lang-time" id="lang-24h">
|
||||
<span>{{ .strings.time24h }}</span>
|
||||
</label>
|
||||
<div id="lang-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
@@ -316,23 +364,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">
|
||||
@@ -343,27 +409,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>
|
||||
@@ -406,6 +492,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>
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
window.invalidPassword = "{{ .strings.reEnterPasswordInvalid }}";
|
||||
window.URLBase = "{{ .urlBase }}";
|
||||
window.code = "{{ .code }}";
|
||||
window.language = "{{ .langName }}";
|
||||
window.messages = JSON.parse({{ .notifications }});
|
||||
window.confirmation = {{ .confirmation }};
|
||||
window.userExpiryEnabled = {{ .userExpiry }};
|
||||
window.userExpiryMonths = {{ .userExpiryMonths }};
|
||||
window.userExpiryDays = {{ .userExpiryDays }};
|
||||
window.userExpiryHours = {{ .userExpiryHours }};
|
||||
window.userExpiryMinutes = {{ .userExpiryMinutes }};
|
||||
|
||||
45
html/password-reset.html
Normal file
45
html/password-reset.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>{{ .strings.passwordReset }} - jfa-go</title>
|
||||
</head>
|
||||
<body class="section">
|
||||
{{ if .success }}
|
||||
<div id="notification-box">
|
||||
<span id="copy-notification" class="unfocused">{{ .strings.copied }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="page-container">
|
||||
<div class="card ~neutral !normal mb-1">
|
||||
<span class="heading mb-1">
|
||||
{{ if .success }}
|
||||
{{ .strings.passwordReset }}
|
||||
{{ else }}
|
||||
{{ .strings.resetFailed }}
|
||||
{{ end }}
|
||||
</span>
|
||||
<p class="content mb-1">
|
||||
{{ if .success }}
|
||||
{{ if .ombiEnabled }}
|
||||
{{ .strings.youCanLoginOmbi }}
|
||||
{{ else }}
|
||||
{{ .strings.youCanLogin }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ .strings.tryAgain }}
|
||||
{{ end }}
|
||||
</p>
|
||||
{{ if .success }}
|
||||
<aside class="aside ~warning">
|
||||
{{ .strings.changeYourPassword }}
|
||||
</aside>
|
||||
<span class="button ~urge !normal full-width center supra p-1 mt-1" id="pin" title="{{ .strings.copy }}">{{ .pin }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<i class="content">{{ .contactMessage }}</i>
|
||||
</div>
|
||||
<script src="{{ .urlBase }}/js/pwr.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -249,8 +249,8 @@
|
||||
</label>
|
||||
<label class="row switch">
|
||||
<input type="checkbox" id="email-no_username"><span>{{ .lang.Email.useEmailAsUsername }}</span>
|
||||
<p class="support mb-1">{{ .lang.Email.useEmailAsUsernameNotice }}</p>
|
||||
</label>
|
||||
<p class="support mb-1">{{ .lang.Email.useEmailAsUsernameNotice }}</p>
|
||||
<label class="label">
|
||||
<span class="mt-half">{{ .lang.Email.fromAddress }}</span>
|
||||
<input type="email" class="input ~neutral !normal mt-half mb-1" id="email-address" placeholder="mail@jellyf.in">
|
||||
@@ -266,10 +266,10 @@
|
||||
</label>
|
||||
<div>
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="email-24h" value="true" checked><span>{{ .lang.Email.time24h }}</span>
|
||||
<input type="radio" name="email-24h" value="true" checked><span>{{ .lang.Strings.time24h }}</span>
|
||||
</label>
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="email-24h" value="false"><span>{{ .lang.Email.time12h }}</span>
|
||||
<input type="radio" name="email-24h" value="false"><span>{{ .lang.Strings.time12h }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !external
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -11,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
|
||||
//go:embed lang/common lang/admin lang/email lang/form lang/setup lang/pwreset
|
||||
var laFS embed.FS
|
||||
|
||||
var langFS rewriteFS
|
||||
73
lang.go
73
lang.go
@@ -1,11 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
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 {
|
||||
@@ -61,6 +59,23 @@ type formLang struct {
|
||||
validationStringsJSON string
|
||||
}
|
||||
|
||||
type pwrLangs map[string]pwrLang
|
||||
|
||||
func (ls *pwrLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
type pwrLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
}
|
||||
|
||||
type emailLangs map[string]emailLang
|
||||
|
||||
func (ls *emailLangs) getOptions() [][2]string {
|
||||
@@ -80,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"`
|
||||
@@ -123,10 +140,29 @@ type langSection map[string]string
|
||||
type tmpl map[string]string
|
||||
|
||||
func templateString(text string, vals tmpl) string {
|
||||
for key, val := range vals {
|
||||
text = strings.ReplaceAll(text, "{"+key+"}", val)
|
||||
start, previousEnd := -1, -1
|
||||
out := ""
|
||||
for i := range text {
|
||||
if text[i] == '{' {
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
if start != -1 && text[i] == '}' {
|
||||
varName := text[start+1 : i]
|
||||
val, ok := vals[varName]
|
||||
if !ok {
|
||||
start = -1
|
||||
continue
|
||||
}
|
||||
out += text[previousEnd+1:start] + val
|
||||
previousEnd = i
|
||||
start = -1
|
||||
}
|
||||
}
|
||||
return text
|
||||
if previousEnd != len(text)-1 {
|
||||
out += text[previousEnd+1:]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (el langSection) template(field string, vals tmpl) string {
|
||||
@@ -136,10 +172,27 @@ func (el langSection) template(field string, vals tmpl) string {
|
||||
|
||||
func (el langSection) format(field string, vals ...string) string {
|
||||
text := el.get(field)
|
||||
for _, val := range vals {
|
||||
text = strings.Replace(text, "{n}", val, 1)
|
||||
start, previous := -1, -3
|
||||
out := ""
|
||||
val := 0
|
||||
for i := range text {
|
||||
if i == len(text)-2 { // Check if there's even enough space for a {n}
|
||||
break
|
||||
}
|
||||
if text[i:i+3] == "{n}" {
|
||||
start = i
|
||||
out += text[previous+3:start] + vals[val]
|
||||
previous = start
|
||||
val++
|
||||
if val == len(vals) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return text
|
||||
if previous+2 != len(text)-1 {
|
||||
out += text[previous+3:]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (el langSection) get(field string) string {
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
"inviteNoUsersCreated": "Noch keine!",
|
||||
"inviteUsersCreated": "Erstellte Benutzer",
|
||||
"inviteNoProfile": "Kein Profil",
|
||||
"copy": "Kopieren",
|
||||
"inviteDateCreated": "Erstellt",
|
||||
"inviteRemainingUses": "Verbleibende Verwendungen",
|
||||
"inviteNoInvites": "Keine",
|
||||
@@ -75,7 +74,20 @@
|
||||
"announce": "Ankündigen",
|
||||
"subject": "E-Mail-Betreff",
|
||||
"message": "Nachricht",
|
||||
"markdownSupported": "Markdown wird unterstützt."
|
||||
"markdownSupported": "Markdown wird unterstützt.",
|
||||
"advancedSettings": "Erweiterte Einstellungen",
|
||||
"search": "Suchen",
|
||||
"userExpiry": "Benutzer Ablaufdatum",
|
||||
"inviteDuration": "Invite Dauer",
|
||||
"enabled": "Aktiviert",
|
||||
"userExpiryDescription": "Eine bestimmte Zeit nach der Anmeldung wird jfa-go das Konto löschen/deaktivieren. Du kannst dieses Verhalten in den Einstellungen ändern.",
|
||||
"disabled": "Deaktiviert",
|
||||
"admin": "Admin",
|
||||
"download": "Herunterladen",
|
||||
"update": "Aktualisieren",
|
||||
"updates": "Aktualisierungen",
|
||||
"expiry": "Ablaufdatum",
|
||||
"extendExpiry": "Ablaufdatum verlängern"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "E-Mail-Adresse von {n} geändert.",
|
||||
@@ -107,7 +119,12 @@
|
||||
"errorSendWelcomeEmail": "Fehler beim Senden der Willkommens-E-Mail (überprüfe die Konsole/Logs)",
|
||||
"saveEmail": "E-Mail gespeichert.",
|
||||
"errorSaveEmail": "Fehler beim Speichern der E-Mail.",
|
||||
"sentAnnouncement": "Ankündigung gesendet."
|
||||
"sentAnnouncement": "Ankündigung gesendet.",
|
||||
"updateApplied": "Aktualisierung angewendet, bitte neu starten.",
|
||||
"errorApplyUpdate": "Fehler beim Anwenden der Aktualisierung, versuche es manuell.",
|
||||
"errorCheckUpdate": "Fehler beim Suchen nach Aktualisierungen.",
|
||||
"updateAvailable": "Eine neue Aktualisierung ist verfügbar, überprüfe die Einstellungen.",
|
||||
"noUpdatesAvailable": "Keinen neuen Aktualisierungen verfügbar."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
@@ -137,6 +154,14 @@
|
||||
"announceTo": {
|
||||
"singular": "{n} Benutzer mitteilen",
|
||||
"plural": "{n} Benutzern mitteilen"
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Ablaufdatum für {n} Benutzer verlängern",
|
||||
"plural": "Ablaufdatum für {n} Benutzer verlängern"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Ablaufdatum für {n} Benutzer verlängern.",
|
||||
"plural": "Ablaufdatum für {n} Benutzer verlängern."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,14 +58,39 @@
|
||||
"inviteNoUsersCreated": "Τίποτα ακόμα!",
|
||||
"inviteUsersCreated": "Δημιουργηθέντες χρήστες",
|
||||
"inviteNoProfile": "Κανένα Προφίλ",
|
||||
"copy": "Αντιγραφή",
|
||||
"inviteDateCreated": "Δημιουργηθέντα",
|
||||
"inviteRemainingUses": "Εναπομείναντες χρήσεις",
|
||||
"inviteNoInvites": "Καμία",
|
||||
"inviteExpiresInTime": "Λήγει σε {n}",
|
||||
"notifyEvent": "Ενημέρωση όταν:",
|
||||
"notifyInviteExpiry": "Στην λήξη",
|
||||
"notifyUserCreation": "Στην δημιουργία χρήστη"
|
||||
"notifyUserCreation": "Στην δημιουργία χρήστη",
|
||||
"variables": "Μεταβλητές",
|
||||
"preview": "Προεπισκόπηση",
|
||||
"reset": "Επαναφορά",
|
||||
"edit": "Επεξεργασία",
|
||||
"customizeEmails": "Παραμετροποίηση Emails",
|
||||
"advancedSettings": "Προχωρημένες Ρυθμίσεις",
|
||||
"customizeEmailsDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",
|
||||
"updates": "Ενημερώσεις",
|
||||
"update": "Ενημέρωση",
|
||||
"download": "Λήψη",
|
||||
"search": "Αναζήτηση",
|
||||
"inviteDuration": "Διάρκεια Πρόσκλησης",
|
||||
"enabled": "Ενεργοποιημένο",
|
||||
"disabled": "Απενεργοποιημένο",
|
||||
"admin": "Διαχειριστής",
|
||||
"expiry": "Λήξη",
|
||||
"userExpiry": "Λήξη Χρήστη",
|
||||
"userExpiryDescription": "Μετά απο ένα καθορισμένο χρόνο μετά απο κάθε εγγραφή, το jfa-go θα διαγράφει/απενεργοποιεί τον λογαριασμό. Μπορείτε να αλλάξετε αυτή την συμπεριφορά στις ρυθμίσεις.",
|
||||
"announce": "Ανακοίνωση",
|
||||
"subject": "Θέμα Email",
|
||||
"message": "Μήνυμα",
|
||||
"extendExpiry": "Παράταση λήξης",
|
||||
"markdownSupported": "Το Markdown υποστυρίζεται.",
|
||||
"reEnable": "Επανα-ενεργοποίηση",
|
||||
"disable": "Απενεργοποίηση",
|
||||
"inviteMonths": "Μήνες"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Αλλαγή {n} διεύθυνσεων email.",
|
||||
@@ -94,7 +119,15 @@
|
||||
"errorFailureCheckLogs": "Αποτυχία (ελέγξτε κονσόλα/καταγραφές)",
|
||||
"errorPartialFailureCheckLogs": "Μερική αποτυχία (ελέγξτε κονσόλα/καταγραφές)",
|
||||
"errorUserCreated": "Αποτυχία δημιουργίας του χρήστη {n}.",
|
||||
"errorSendWelcomeEmail": "Αποτυχία αποστολής email καλωσορίσματος (ελέγξτε κονσόλα/καταγραφές)"
|
||||
"errorSendWelcomeEmail": "Αποτυχία αποστολής email καλωσορίσματος (ελέγξτε κονσόλα/καταγραφές)",
|
||||
"saveEmail": "Το email αποθηκεύτηκε.",
|
||||
"sentAnnouncement": "Ανακοίνωση εστάλη.",
|
||||
"updateApplied": "Η ενημέρωση εφαρμόστηκε, παρακαλώ επανεκκινήστε.",
|
||||
"errorSaveEmail": "Αποτυχία αποθήκευσης του email.",
|
||||
"errorApplyUpdate": "Αποτυχία εγκατάστασης ενημέρωσης, προσπαθήστε χειροκίνητα.",
|
||||
"errorCheckUpdate": "Αποτυχία ελέγχου για ενημερώσεις.",
|
||||
"updateAvailable": "Μια νέα ενημέρωση είναι διαθέσιμη, ελέγξτε τις ρυθμίσεις.",
|
||||
"noUpdatesAvailable": "Δεν υπάρχουν διαθέσιμες ενημερώσεις."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
@@ -120,6 +153,34 @@
|
||||
"appliedSettings": {
|
||||
"singular": "Εφαρμογή ρυθμίσεων σε {n} χρήστη.",
|
||||
"plural": "Εφαρμογή ρυθμίσεων σε {n} χρήστες."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Ανακοίνωση σε {n} χρήστη",
|
||||
"plural": "Ανακοίνωση σε {n} χρήστες"
|
||||
},
|
||||
"extendExpiry": {
|
||||
"plural": "Επέκταση λήξης σε {n} χρήστες",
|
||||
"singular": "Επέκταση λήξης σε {n} χρήστη"
|
||||
},
|
||||
"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} χρήστες."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
lang/admin/en-gb.json
Normal file
5
lang/admin/en-gb.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (GB)"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -46,9 +49,11 @@
|
||||
"subject": "Email Subject",
|
||||
"message": "Message",
|
||||
"variables": "Variables",
|
||||
"conditionals": "Conditionals",
|
||||
"preview": "Preview",
|
||||
"reset": "Reset",
|
||||
"edit": "Edit",
|
||||
"donate": "Donate",
|
||||
"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.",
|
||||
@@ -81,7 +86,6 @@
|
||||
"inviteNoUsersCreated": "None yet!",
|
||||
"inviteUsersCreated": "Created users",
|
||||
"inviteNoProfile": "No Profile",
|
||||
"copy": "Copy",
|
||||
"inviteDateCreated": "Created",
|
||||
"inviteRemainingUses": "Remaining uses",
|
||||
"inviteNoInvites": "None",
|
||||
@@ -136,6 +140,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"
|
||||
@@ -148,6 +160,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",
|
||||
"customizeEmails": "Personalizar emails",
|
||||
"customizeEmailsDescription": "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",
|
||||
@@ -56,7 +57,6 @@
|
||||
"inviteNoUsersCreated": "Aucun pour l'instant !",
|
||||
"inviteUsersCreated": "Utilisateurs créer",
|
||||
"inviteNoProfile": "Aucun profil",
|
||||
"copy": "Copier",
|
||||
"inviteDateCreated": "Créer",
|
||||
"inviteRemainingUses": "Utilisations restantes",
|
||||
"inviteNoInvites": "Aucune",
|
||||
@@ -76,7 +76,21 @@
|
||||
"preview": "Aperçu",
|
||||
"reset": "Réinitialiser",
|
||||
"edit": "Éditer",
|
||||
"customizeEmails": "Personnaliser les e-mails"
|
||||
"customizeEmails": "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}.",
|
||||
@@ -108,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": {
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
"inviteNoUsersCreated": "Belum ada!",
|
||||
"inviteUsersCreated": "Pengguna yang telah dibuat",
|
||||
"inviteNoProfile": "Tidak ada profil",
|
||||
"copy": "Salin",
|
||||
"inviteDateCreated": "Dibuat",
|
||||
"inviteRemainingUses": "Penggunaan yang tersisa",
|
||||
"inviteNoInvites": "Tidak ada",
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
"inviteNoUsersCreated": "Nog geen!",
|
||||
"inviteUsersCreated": "Aangemaakte gebruikers",
|
||||
"inviteNoProfile": "Geen profiel",
|
||||
"copy": "Kopiëer",
|
||||
"inviteDateCreated": "Aangemaakt",
|
||||
"inviteRemainingUses": "Resterend aantal keer te gebruiken",
|
||||
"inviteNoInvites": "Geen",
|
||||
@@ -88,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.",
|
||||
@@ -163,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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
"inviteNoUsersCreated": "Nenhum ainda!",
|
||||
"inviteUsersCreated": "Usuários criado",
|
||||
"inviteNoProfile": "Sem Perfil",
|
||||
"copy": "Copiar",
|
||||
"inviteDateCreated": "Criado",
|
||||
"inviteRemainingUses": "Uso restantes",
|
||||
"inviteNoInvites": "Nenhum",
|
||||
@@ -81,13 +80,18 @@
|
||||
"inviteDuration": "Duração do Convite",
|
||||
"enabled": "Habilitado",
|
||||
"admin": "Admin",
|
||||
"expiry": "Expiração",
|
||||
"expiry": "Expira",
|
||||
"userExpiry": "Vencimento do Usuário",
|
||||
"extendExpiry": "Extender o vencimento",
|
||||
"updates": "Atualizações",
|
||||
"update": "Atualizar",
|
||||
"download": "Download",
|
||||
"search": "Procurar"
|
||||
"search": "Procurar",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
"inviteNoUsersCreated": "Ingen än!",
|
||||
"inviteUsersCreated": "Skapade användare",
|
||||
"inviteNoProfile": "Ingen profil",
|
||||
"copy": "Kopiera",
|
||||
"inviteDateCreated": "Skapad",
|
||||
"inviteRemainingUses": "Återstående användningar",
|
||||
"inviteNoInvites": "Ingen",
|
||||
|
||||
@@ -8,8 +8,12 @@
|
||||
"password": "Passwort",
|
||||
"emailAddress": "E-Mail-Adresse",
|
||||
"submit": "Absenden",
|
||||
"success": "Erfolg",
|
||||
"success": "Erfolgreich",
|
||||
"error": "Fehler",
|
||||
"theme": "Thema"
|
||||
"copy": "Kopieren",
|
||||
"theme": "Thema",
|
||||
"time24h": "24h-Format",
|
||||
"time12h": "12h-Format",
|
||||
"copied": "Kopiert"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
"submit": "Καταχώρηση",
|
||||
"success": "Επιτυχία",
|
||||
"error": "Σφάλμα",
|
||||
"theme": "Θέμα"
|
||||
"copy": "Αντιγραφή",
|
||||
"theme": "Θέμα",
|
||||
"time24h": "24 Ώρες",
|
||||
"time12h": "12 Ώρες",
|
||||
"copied": "Αντιγράφηκε"
|
||||
}
|
||||
}
|
||||
|
||||
5
lang/common/en-gb.json
Normal file
5
lang/common/en-gb.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (GB)"
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,10 @@
|
||||
"submit": "Submit",
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"time24h": "24h Time",
|
||||
"time12h": "12h Time",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@
|
||||
"submit": "Soumettre",
|
||||
"success": "Succès",
|
||||
"error": "Erreur",
|
||||
"copy": "Copier",
|
||||
"time24h": "Temps 24h",
|
||||
"time12h": "Temps 12h",
|
||||
"theme": "Thème"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"submit": "Submit",
|
||||
"success": "Sukses",
|
||||
"error": "Error",
|
||||
"copy": "Salin",
|
||||
"time24h": "Waktu 24 jam",
|
||||
"time12h": "Waktu 12 jam",
|
||||
"theme": "Tema"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
"submit": "Verstuur",
|
||||
"success": "Success",
|
||||
"error": "Fout",
|
||||
"theme": "Thema"
|
||||
"copy": "Kopiëer",
|
||||
"theme": "Thema",
|
||||
"time24h": "24u-formaat",
|
||||
"time12h": "12u-formaat",
|
||||
"copied": "Gekopieerd"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
"submit": "Enviar",
|
||||
"success": "Sucesso",
|
||||
"error": "Erro",
|
||||
"theme": "Tema"
|
||||
"copy": "Copiar",
|
||||
"theme": "Tema",
|
||||
"time24h": "Horário 24h",
|
||||
"time12h": "Horário 12h",
|
||||
"copied": "Copiado"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"submit": "Skicka",
|
||||
"success": "Lyckades",
|
||||
"error": "Fel",
|
||||
"copy": "Kopiera",
|
||||
"time24h": "24 timmarsklocka",
|
||||
"time12h": "12 timmarsklocka",
|
||||
"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": {
|
||||
@@ -55,5 +55,11 @@
|
||||
"clickBelow": "Klicke den untenstehenden Link, um deine E-Mail-Adresse zu bestätigen, und fange an, Jellyfin zu benutzen.",
|
||||
"confirmEmail": "E-Mail bestätigen",
|
||||
"name": "Bestätigungs-E-Mail"
|
||||
},
|
||||
"userExpired": {
|
||||
"name": "Benutzer Ablaufdatum",
|
||||
"title": "Dein Konto ist abgelaufen - Jellyfin",
|
||||
"yourAccountHasExpired": "Dein Konto ist abgelaufen.",
|
||||
"contactTheAdmin": "Kontaktiere den Administrator für weitere Informationen."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,31 +4,36 @@
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Αν δεν ήσασταν εσείς, παρακαλώ αγνοήστε αυτό το email.",
|
||||
"reason": "Λόγος",
|
||||
"helloUser": "Γεία σου {username},"
|
||||
},
|
||||
"userCreated": {
|
||||
"title": "Σημείωση: Δημιουργήθηκε χρήστης",
|
||||
"aUserWasCreated": "Δημιουργήθηκε ένας χρήστης χρησιμοποιώντας τον κωδικό {code}.",
|
||||
"time": "Ώρα",
|
||||
"notificationNotice": "Σημείωση: Τα email ειδοποιήσεων μπορούν να ενεργοποιηθούν στον πίνακα ελέγχου διαχειριστή."
|
||||
"notificationNotice": "Σημείωση: Τα email ειδοποιήσεων μπορούν να ενεργοποιηθούν στον πίνακα ελέγχου διαχειριστή.",
|
||||
"name": "Δημιουργία χρήστη"
|
||||
},
|
||||
"inviteExpiry": {
|
||||
"title": "Σημείωση: Η πρόσκληση έληξε",
|
||||
"inviteExpired": "Η πρόσκληση έληξε.",
|
||||
"expiredAt": "Ο κωδικός {code} έληξε στις {time}.",
|
||||
"notificationNotice": "Σημείωση: Τα email ειδοποιήσεων μπορούν να ενεργοποιηθούν στον πίνακα ελέγχου διαχειριστή."
|
||||
"notificationNotice": "Σημείωση: Τα email ειδοποιήσεων μπορούν να ενεργοποιηθούν στον πίνακα ελέγχου διαχειριστή.",
|
||||
"name": "Λήξη πρόσκλησης"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Ζητήθηκε επαναφορά κωδικού πρόσβασης - Jellyfin",
|
||||
"someoneHasRequestedReset": "Κάποιος ζήτησε πρόσφατα επαναφορά κωδικού πρόσβασης στο Jellyfin.",
|
||||
"ifItWasYou": "Εάν ήσασταν εσείς, εισαγάγετε το πιν στο πεδίο.",
|
||||
"codeExpiry": "Ο κωδικός θα λήξει στις {date}, στις {time} UTC, το οποίο είναι σε {expiresInMinutes}.",
|
||||
"pin": "PIN"
|
||||
"pin": "PIN",
|
||||
"name": "Επαναφορά κωδικού πρόσβασης",
|
||||
"ifItWasYouLink": "Εάν ήσασταν εσείς, κάντε κλικ στον παρακάτω σύνδεσμο."
|
||||
},
|
||||
"userDeleted": {
|
||||
"title": "Ο λογαριασμός σας διαγράφηκε - Jellyfin",
|
||||
"yourAccountWasDeleted": "Ο λογαριασμός σας Jellyfin διαγράφηκε.",
|
||||
"reason": "Λόγος"
|
||||
"name": "Διαγραφή χρήστη"
|
||||
},
|
||||
"inviteEmail": {
|
||||
"title": "Πρόσκληση - Jellyfin",
|
||||
@@ -36,17 +41,37 @@
|
||||
"youHaveBeenInvited": "Έχετε προσκληθεί στο Jellyfin.",
|
||||
"toJoin": "Για να συμμετέχετε, ακολουθήστε τον παρακάτω σύνδεσμο.",
|
||||
"inviteExpiry": "Αυτή η πρόσκληση θα λήξει στις {date} στις {time}, που είναι σε {expiresInMinutes}, οπότε ενεργήστε γρήγορα.",
|
||||
"linkButton": "Ρυθμίστε τον λογαριασμό σας"
|
||||
"linkButton": "Ρυθμίστε τον λογαριασμό σας",
|
||||
"name": "Email πρόσκλησης"
|
||||
},
|
||||
"welcomeEmail": {
|
||||
"title": "Καλώς ήλθατε στο Jellyfin",
|
||||
"welcome": "Καλώς ήλθατε στο Jellyfin!",
|
||||
"youCanLoginWith": "Μπορείτε να συνδεθείτε με τα παρακάτω στοιχεία",
|
||||
"jellyfinURL": "URL"
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Email καλωσορίσματος",
|
||||
"yourAccountWillExpire": "Ο λογαριασμός σας θα λήξει στις {date}."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
"title": "Επιβεβαιώστε το email σας - Jellyfin",
|
||||
"clickBelow": "Κάντε κλικ στον παρακάτω σύνδεσμο για να επιβεβαιώσετε τη διεύθυνση email σας και ξεκινήστε να χρησιμοποιείτε το Jellyfin.",
|
||||
"confirmEmail": "Επιβεβαίωση Email"
|
||||
"confirmEmail": "Επιβεβαίωση Email",
|
||||
"name": "Email επιβεβαίωσης"
|
||||
},
|
||||
"userExpired": {
|
||||
"name": "Λήξη Χρήστη",
|
||||
"title": "Ο λογαριασμός σας έληξε - Jellyfin",
|
||||
"yourAccountHasExpired": "Ο λογαριασμός σας έχει λήξει.",
|
||||
"contactTheAdmin": "Επικοινωνήστε με τον διαχειριστή για περισσότερες πληροφορίες."
|
||||
},
|
||||
"userDisabled": {
|
||||
"name": "Ο χρήστης απενεργοποιήθηκε",
|
||||
"title": "Ο λογαριασμός σας έχει απενεργοποιηθεί - Jellyfin",
|
||||
"yourAccountWasDisabled": "Ο λογαριασμός σας απενεργοποιήθηκε."
|
||||
},
|
||||
"userEnabled": {
|
||||
"title": "Ο λογαριασμός σας έχει ενεργοποιηθεί ξανά - Jellyfin",
|
||||
"name": "Ο χρήστης ενεργοποιήθηκε",
|
||||
"yourAccountWasEnabled": "Ο λογαριασμός σας ενεργοποιήθηκε εκ νέου."
|
||||
}
|
||||
}
|
||||
|
||||
5
lang/email/en-gb.json
Normal file
5
lang/email/en-gb.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (GB)"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "If this wasn't you, please ignore this email.",
|
||||
"helloUser": "Hi {username},"
|
||||
"helloUser": "Hi {username},",
|
||||
"reason": "Reason"
|
||||
},
|
||||
"userCreated": {
|
||||
"name": "User creation",
|
||||
@@ -25,14 +26,24 @@
|
||||
"title": "Password reset requested - Jellyfin",
|
||||
"someoneHasRequestedReset": "Someone has recently requested a password reset on Jellyfin.",
|
||||
"ifItWasYou": "If this was you, enter the pin below into the prompt.",
|
||||
"ifItWasYouLink": "If this was you, click the link below.",
|
||||
"codeExpiry": "The code will expire on {date}, at {time} UTC, which is in {expiresInMinutes}.",
|
||||
"pin": "PIN"
|
||||
},
|
||||
"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",
|
||||
@@ -48,6 +59,7 @@
|
||||
"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": {
|
||||
@@ -23,15 +24,15 @@
|
||||
"passwordReset": {
|
||||
"title": "Wachtwoordreset aangevraagd - Jellyfin",
|
||||
"someoneHasRequestedReset": "Iemand heeft recentelijk een wachtwoordreset aangevraagd in Jellyfin.",
|
||||
"ifItWasYou": "Als jij dit was, voor dan onderstaande PIN in.",
|
||||
"ifItWasYou": "Als jij dit was, voer dan onderstaande PIN in.",
|
||||
"codeExpiry": "De code verloopt op {date}, op {time} UTC, dat is over {expiresInMinutes}.",
|
||||
"pin": "PIN",
|
||||
"name": "Wachtwoordreset"
|
||||
"name": "Wachtwoordreset",
|
||||
"ifItWasYouLink": "Als jij dit was, klik dan op onderstaande link."
|
||||
},
|
||||
"userDeleted": {
|
||||
"title": "Je account is verwijderd - Jellyfin",
|
||||
"yourAccountWasDeleted": "Je Jellyfin account is verwijderd.",
|
||||
"reason": "Reden",
|
||||
"name": "Gebruiker verwijderd"
|
||||
},
|
||||
"inviteEmail": {
|
||||
@@ -48,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",
|
||||
@@ -61,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": {
|
||||
@@ -26,12 +27,12 @@
|
||||
"ifItWasYou": "Se foi você, insira o PIN abaixo.",
|
||||
"codeExpiry": "O código irá expirar em {date}, ás {time}, que está em {expiresInMinutes}.",
|
||||
"pin": "PIN",
|
||||
"name": "Redefinir senha"
|
||||
"name": "Redefinir senha",
|
||||
"ifItWasYouLink": "Se foi você, clique no link abaixo."
|
||||
},
|
||||
"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": {
|
||||
@@ -48,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",
|
||||
@@ -61,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",
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
"reEnterPasswordInvalid": "Passwörter stimmen nicht überein.",
|
||||
"createAccountButton": "Konto erstellen",
|
||||
"passwordRequirementsHeader": "Passwortanforderungen",
|
||||
"successHeader": "Erfolg!",
|
||||
"successHeader": "Erfolgreich!",
|
||||
"successContinueButton": "Weiter",
|
||||
"confirmationRequired": "E-Mail-Bestätigung erforderlich",
|
||||
"confirmationRequiredMessage": "Bitte überprüfe dein Posteingang und bestätige deine E-Mail-Adresse."
|
||||
"confirmationRequiredMessage": "Bitte überprüfe dein Posteingang und bestätige deine E-Mail-Adresse.",
|
||||
"yourAccountIsValidUntil": "Dein Konto wird bis zum {date} gültig sein."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"successHeader": "Επιτυχία!",
|
||||
"successContinueButton": "Συνέχεια",
|
||||
"confirmationRequired": "Απαιτείται επιβεβαίωση Email",
|
||||
"confirmationRequiredMessage": "Παρακαλώ ελέγξτε το email σας για να επιβεβαιώσετε την διεύθυνση σας ."
|
||||
"confirmationRequiredMessage": "Παρακαλώ ελέγξτε το email σας για να επιβεβαιώσετε την διεύθυνση σας .",
|
||||
"yourAccountIsValidUntil": "Ο λογαριασμός σου θα ισχύει μέχρι {date}."
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "Ο χρήστης υπάρχει ήδη.",
|
||||
|
||||
5
lang/form/en-gb.json
Normal file
5
lang/form/en-gb.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (GB)"
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
"successContinueButton": "Continuar",
|
||||
"confirmationRequired": "Necessária confirmação de e-mail",
|
||||
"confirmationRequiredMessage": "Verifique sua caixa de email para finalizar o cadastro.",
|
||||
"yourAccountIsValidUntil": "Sua conta é válida até {data}."
|
||||
"yourAccountIsValidUntil": "Sua conta é válida até {date}."
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "Esse usuário já existe.",
|
||||
|
||||
13
lang/pwreset/en-us.json
Normal file
13
lang/pwreset/en-us.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (US)"
|
||||
},
|
||||
"strings": {
|
||||
"passwordReset": "Password reset",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
@@ -31,9 +31,9 @@
|
||||
"language": {
|
||||
"title": "Sprache",
|
||||
"description": "Gemeinschaftsübersetzungen sind für die meisten Teile von jfa-go verfügbar. Du kannst unten die Standardsprachen auswählen, aber Benutzer können dies immer noch ändern, wenn sie wollen. Wenn du helfen willst zu übersetzen, melde dich bei {n} an, um anzufangen, etwas beizutragen!",
|
||||
"defaultAdminLang": "Standardsprache Admin",
|
||||
"defaultFormLang": "Standardsprache Kontoerstellung",
|
||||
"defaultEmailLang": "Standardsprache E-Mail"
|
||||
"defaultAdminLang": "Admin Standardsprache",
|
||||
"defaultFormLang": "Kontoerstellung Standardsprache",
|
||||
"defaultEmailLang": "E-Mail Standardsprache"
|
||||
},
|
||||
"general": {
|
||||
"title": "Allgemein",
|
||||
@@ -82,8 +82,6 @@
|
||||
"senderName": "Absendername",
|
||||
"dateFormat": "Datumsformat",
|
||||
"dateFormatNotice": "Datum folgt dem strftime-Format. Für weitere Informationen, besuche {n}.",
|
||||
"time24h": "24h-Format",
|
||||
"time12h": "12h-Format",
|
||||
"encryption": "Verschlüsselung",
|
||||
"mailgunApiURL": "API-URL"
|
||||
},
|
||||
@@ -125,5 +123,12 @@
|
||||
"successMessageNotice": "Wird angezeigt, wenn ein Benutzer sein Konto erstellt.",
|
||||
"emailMessage": "E-Mailnachricht",
|
||||
"emailMessageNotice": "Wird am Ende von E-Mails angezeigt."
|
||||
},
|
||||
"updates": {
|
||||
"updateChannel": "Aktualisierungskanal",
|
||||
"unstable": "Unstable",
|
||||
"stable": "Stable",
|
||||
"title": "Aktualisierungen",
|
||||
"description": "Aktiviere, um informiert zu werden, wenn neue Aktualisierungen verfügbar sind. jfa-go wird {n} alle 30 Minuten überprüfen. Keine IP-Adressen oder personenbezogene Daten werden gesammelt."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,6 @@
|
||||
"senderName": "Ονομα αποστολέα",
|
||||
"dateFormat": "Μορφή ημερομηνίας",
|
||||
"dateFormatNotice": "Η ημερομηνία ακολουθεί τη μορφή strftime. Για περισσότερες πληροφορίες, επισκεφτείτε το {n}.",
|
||||
"time24h": "24 Ώρες",
|
||||
"time12h": "12 Ώρες",
|
||||
"encryption": "Κρυπτογράφηση",
|
||||
"mailgunApiURL": "Διεύθυνση API"
|
||||
},
|
||||
@@ -125,5 +123,12 @@
|
||||
"successMessageNotice": "Εμφανίζεται όταν ένας χρήστης δημιουργεί τον λογαριασμό του.",
|
||||
"emailMessage": "Μήνυμα Email",
|
||||
"emailMessageNotice": "Εμφανίζεται στο κάτω μέρος των email."
|
||||
},
|
||||
"updates": {
|
||||
"title": "Ενημερώσεις",
|
||||
"description": "Ενεργοποίηση ειδοποίησης όταν είναι διαθέσιμες νέες ενημερώσεις. Το jfa-go θα ελέγχει {n} κάθε 30 λεπτά. Δεν συλλέγονται IP ή προσωπικές πληροφορίες.",
|
||||
"updateChannel": "Κανάλι Ενημερώσεων",
|
||||
"stable": "Σταθερό",
|
||||
"unstable": "Ασταθές"
|
||||
}
|
||||
}
|
||||
|
||||
5
lang/setup/en-gb.json
Normal file
5
lang/setup/en-gb.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (GB)"
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
@@ -89,8 +89,6 @@
|
||||
"senderName": "Sender Name",
|
||||
"dateFormat": "Date Format",
|
||||
"dateFormatNotice": "Date follows the strftime format. For more info, visit {n}.",
|
||||
"time24h": "24h Time",
|
||||
"time12h": "12h Time",
|
||||
"encryption": "Encryption",
|
||||
"mailgunApiURL": "API URL"
|
||||
},
|
||||
@@ -110,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."
|
||||
}
|
||||
}
|
||||
@@ -82,8 +82,6 @@
|
||||
"senderName": "Nom de l'envoyeur",
|
||||
"dateFormat": "Format de la date",
|
||||
"dateFormatNotice": "La date suis le format srtftime. Pour plus d'informations, consultez {n}.",
|
||||
"time24h": "Temps 24h",
|
||||
"time12h": "Temps 12h",
|
||||
"encryption": "Chiffrement",
|
||||
"mailgunApiURL": "URL de l'API"
|
||||
},
|
||||
|
||||
@@ -82,8 +82,6 @@
|
||||
"senderName": "Nama Pengirim",
|
||||
"dateFormat": "Format Tanggal",
|
||||
"dateFormatNotice": "Tanggal mengikuti format strftime. Untuk info lebih lanjut, kunjungi {n}.",
|
||||
"time24h": "Waktu 24 jam",
|
||||
"time12h": "Waktu 12 jam",
|
||||
"encryption": "Enkripsi",
|
||||
"mailgunApiURL": "URL API"
|
||||
},
|
||||
|
||||
@@ -82,8 +82,6 @@
|
||||
"senderName": "Naam afzender",
|
||||
"dateFormat": "Datumformaat",
|
||||
"dateFormatNotice": "Datum volgend het strftime formaat. Meer info op {n}.",
|
||||
"time24h": "24u-formaat",
|
||||
"time12h": "12u-formaat",
|
||||
"encryption": "Versleuteling",
|
||||
"mailgunApiURL": "API-URL"
|
||||
},
|
||||
@@ -125,5 +123,12 @@
|
||||
"successMessageNotice": "Getoond wanneer een gebruiker een account aanmaakt.",
|
||||
"emailMessage": "E-mailtext",
|
||||
"emailMessageNotice": "Getoond onderaan e-mails."
|
||||
},
|
||||
"updates": {
|
||||
"unstable": "Instabiel",
|
||||
"updateChannel": "Update kanaal",
|
||||
"stable": "Stabiel",
|
||||
"title": "Updates",
|
||||
"description": "Vink aan om een melding te krijgen wanneer nieuwe updates beschikbaar zijn. jfa-go controleert {n} elke 30 minuten. Er worden geen IPs of persoonsgegevens verzameld."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,6 @@
|
||||
"senderName": "Nome do remetente",
|
||||
"dateFormat": "Formato da Data",
|
||||
"dateFormatNotice": "A data segue o formato strftime. Para obter mais informações, visite {n}.",
|
||||
"time24h": "Horário 24h",
|
||||
"time12h": "Horário 12h",
|
||||
"encryption": "Encriptação",
|
||||
"mailgunApiURL": "API URL"
|
||||
},
|
||||
@@ -125,5 +123,12 @@
|
||||
"successMessageNotice": "Exibido quando um usuário cria sua conta.",
|
||||
"emailMessage": "Mensagem de Email",
|
||||
"emailMessageNotice": "Exibido na parte inferior dos emails."
|
||||
},
|
||||
"updates": {
|
||||
"title": "Atualizações",
|
||||
"description": "Ative para ser notificado quando novas atualizações estiverem disponíveis. jfa-go verificará {n} a cada 30 minutos. Nenhum IP ou informação de identificação pessoal é coletada.",
|
||||
"updateChannel": "Canal de Atualização",
|
||||
"stable": "Estável",
|
||||
"unstable": "Instável"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,6 @@
|
||||
"senderName": "Avsändarens namn",
|
||||
"dateFormat": "Datumformat",
|
||||
"dateFormatNotice": "Datum följer strftime-formatet. Mer information finns på {n}.",
|
||||
"time24h": "24 timmarsklocka",
|
||||
"time12h": "12 timmarsklocka",
|
||||
"encryption": "Kryptering",
|
||||
"mailgunApiURL": "API URL"
|
||||
},
|
||||
|
||||
45
logger.go
45
logger.go
@@ -1,45 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
|
||||
c "github.com/fatih/color"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
Print(v ...interface{})
|
||||
Println(v ...interface{})
|
||||
Fatal(v ...interface{})
|
||||
Fatalf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
logger *log.Logger
|
||||
printer *c.Color
|
||||
}
|
||||
|
||||
func NewLogger(out io.Writer, prefix string, flag int, color c.Attribute) (l logger) {
|
||||
l.logger = log.New(out, prefix, flag)
|
||||
l.printer = c.New(color)
|
||||
return l
|
||||
}
|
||||
|
||||
func (l logger) Printf(format string, v ...interface{}) {
|
||||
l.logger.Print(l.printer.Sprintf(format, v...))
|
||||
}
|
||||
func (l logger) Print(v ...interface{}) { l.logger.Print(l.printer.Sprint(v...)) }
|
||||
func (l logger) Println(v ...interface{}) { l.logger.Print(l.printer.Sprintln(v...)) }
|
||||
func (l logger) Fatal(v ...interface{}) { l.logger.Fatal(l.printer.Sprint(v...)) }
|
||||
func (l logger) Fatalf(format string, v ...interface{}) {
|
||||
l.logger.Fatal(l.printer.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
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{}) {}
|
||||
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=
|
||||
109
logger/logger.go
Normal file
109
logger/logger.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Package logger provides a wrapper around log that adds color support with github.com/fatih/color.
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
c "github.com/fatih/color"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
Print(v ...interface{})
|
||||
Println(v ...interface{})
|
||||
Fatal(v ...interface{})
|
||||
Fatalf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
logger *log.Logger
|
||||
shortfile bool
|
||||
printer *c.Color
|
||||
}
|
||||
|
||||
func Lshortfile() string {
|
||||
// 0 = This function, 1 = Print/Printf/Println, 2 = Caller of Print/Printf/Println
|
||||
_, file, line, ok := runtime.Caller(2)
|
||||
lineString := strconv.Itoa(line)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if file == "" {
|
||||
return lineString
|
||||
}
|
||||
for i := len(file) - 1; i > 0; i-- {
|
||||
if file[i] == '/' || file[i] == '\\' {
|
||||
file = file[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
return file + ":" + lineString + ":"
|
||||
}
|
||||
|
||||
func NewLogger(out io.Writer, prefix string, flag int, color c.Attribute) (l logger) {
|
||||
// Use reimplemented Lshortfile since wrapping the log functions messes them up
|
||||
if flag&log.Lshortfile != 0 {
|
||||
flag -= log.Lshortfile
|
||||
l.shortfile = true
|
||||
}
|
||||
|
||||
l.logger = log.New(out, prefix, flag)
|
||||
l.printer = c.New(color)
|
||||
return l
|
||||
}
|
||||
|
||||
func (l logger) Printf(format string, v ...interface{}) {
|
||||
var out string
|
||||
if l.shortfile {
|
||||
out = Lshortfile()
|
||||
}
|
||||
out += " " + l.printer.Sprintf(format, v...)
|
||||
l.logger.Print(out)
|
||||
}
|
||||
|
||||
func (l logger) Print(v ...interface{}) {
|
||||
var out string
|
||||
if l.shortfile {
|
||||
out = Lshortfile()
|
||||
}
|
||||
out += " " + l.printer.Sprint(v...)
|
||||
l.logger.Print(out)
|
||||
}
|
||||
|
||||
func (l logger) Println(v ...interface{}) {
|
||||
var out string
|
||||
if l.shortfile {
|
||||
out = Lshortfile()
|
||||
}
|
||||
out += " " + l.printer.Sprintln(v...)
|
||||
l.logger.Print(out)
|
||||
}
|
||||
|
||||
func (l logger) Fatal(v ...interface{}) {
|
||||
var out string
|
||||
if l.shortfile {
|
||||
out = Lshortfile()
|
||||
}
|
||||
out += " " + l.printer.Sprint(v...)
|
||||
l.logger.Fatal(out)
|
||||
}
|
||||
|
||||
func (l logger) Fatalf(format string, v ...interface{}) {
|
||||
var out string
|
||||
if l.shortfile {
|
||||
out = Lshortfile()
|
||||
}
|
||||
out += " " + l.printer.Sprintf(format, v...)
|
||||
l.logger.Fatal(out)
|
||||
}
|
||||
|
||||
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{}) {}
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -66,7 +66,11 @@
|
||||
<p>{{ .codeExpiry }}</p>
|
||||
<p>{{ .ifItWasNotYou }}</p>
|
||||
</mj-text>
|
||||
<mj-raw>{{ if .link_reset }}</mj-raw>
|
||||
<mj-button mj-class="blue bold" href="{{ .pin }}"><mj-raw>{{ .pin_code }}</mj-raw></mj-button>
|
||||
<mj-raw>{{ else }}</mj-raw>
|
||||
<mj-button mj-class="blue bold"><mj-raw>{{ .pin }}</mj-raw></mj-button>
|
||||
<mj-raw>{{ end }}</mj-raw>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section mj-class="bg2">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
{{ .youCanLoginWith }}:
|
||||
|
||||
{{ .jellyfinURLString }}: {{ .jellyfinURL }}
|
||||
|
||||
{{ .usernameString }}: {{ .username }}
|
||||
|
||||
{{ .yourAccountWillExpire }}
|
||||
|
||||
{{ .message }}
|
||||
|
||||
197
main.go
197
main.go
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
@@ -24,8 +23,9 @@ import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
_ "github.com/hrfee/jfa-go/docs"
|
||||
"github.com/hrfee/jfa-go/mediabrowser"
|
||||
"github.com/hrfee/jfa-go/logger"
|
||||
"github.com/hrfee/jfa-go/ombi"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
@@ -38,6 +38,7 @@ var (
|
||||
DATA, CONFIG, HOST *string
|
||||
PORT *int
|
||||
DEBUG *bool
|
||||
PPROF *bool
|
||||
TEST bool
|
||||
SWAGGER *bool
|
||||
warning = color.New(color.FgYellow).SprintfFunc()
|
||||
@@ -79,7 +80,7 @@ type appContext struct {
|
||||
configBase settings
|
||||
dataPath string
|
||||
webFS httpFS
|
||||
cssClass string
|
||||
cssClass string // Default theme, "light-theme"|"dark-theme".
|
||||
jellyfinLogin bool
|
||||
users []User
|
||||
invalidTokens []string
|
||||
@@ -92,7 +93,7 @@ type appContext struct {
|
||||
storage Storage
|
||||
validator Validator
|
||||
email *Emailer
|
||||
info, debug, err Logger
|
||||
info, debug, err logger.Logger
|
||||
host string
|
||||
port int
|
||||
version string
|
||||
@@ -164,53 +165,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 and exposes 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 os.Getenv("SWAGGER") == "1" {
|
||||
*SWAGGER = true
|
||||
}
|
||||
if os.Getenv("DEBUG") == "1" {
|
||||
*DEBUG = 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) {
|
||||
@@ -237,8 +195,8 @@ 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)
|
||||
}
|
||||
app.version = app.config.Section("jellyfin").Key("version").String()
|
||||
// read from config...
|
||||
@@ -248,18 +206,19 @@ func start(asDaemon, firstCall bool) {
|
||||
debugMode = true
|
||||
}
|
||||
if debugMode {
|
||||
app.info.Print(warning("\n\nWARNING: Don't use debug mode in production, as it exposes pprof on the network.\n\n"))
|
||||
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"))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -267,7 +226,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() {
|
||||
@@ -277,13 +236,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])
|
||||
@@ -298,6 +257,7 @@ func start(asDaemon, firstCall bool) {
|
||||
app.storage.lang.FormPath = "form"
|
||||
app.storage.lang.AdminPath = "admin"
|
||||
app.storage.lang.EmailPath = "email"
|
||||
app.storage.lang.PasswordResetPath = "pwreset"
|
||||
externalLang := app.config.Section("files").Key("lang_files").MustString("")
|
||||
var err error
|
||||
if externalLang == "" {
|
||||
@@ -342,20 +302,33 @@ func start(asDaemon, firstCall bool) {
|
||||
app.debug.Println("Loading storage")
|
||||
|
||||
app.storage.invite_path = app.config.Section("files").Key("invites").String()
|
||||
app.storage.loadInvites()
|
||||
if err := app.storage.loadInvites(); err != nil {
|
||||
app.err.Printf("Failed to load Invites: %v", err)
|
||||
}
|
||||
app.storage.emails_path = app.config.Section("files").Key("emails").String()
|
||||
app.storage.loadEmails()
|
||||
if err := app.storage.loadEmails(); err != nil {
|
||||
app.err.Printf("Failed to load Emails: %v", err)
|
||||
}
|
||||
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
|
||||
app.storage.loadPolicy()
|
||||
if err := app.storage.loadPolicy(); err != nil {
|
||||
app.err.Printf("Failed to load Policy: %v", err)
|
||||
}
|
||||
app.storage.configuration_path = app.config.Section("files").Key("user_configuration").String()
|
||||
app.storage.loadConfiguration()
|
||||
if err := app.storage.loadConfiguration(); err != nil {
|
||||
app.err.Printf("Failed to load Configuration: %v", err)
|
||||
}
|
||||
app.storage.displayprefs_path = app.config.Section("files").Key("user_displayprefs").String()
|
||||
app.storage.loadDisplayprefs()
|
||||
if err := app.storage.loadDisplayprefs(); err != nil {
|
||||
app.err.Printf("Failed to load Displayprefs: %v", err)
|
||||
}
|
||||
app.storage.users_path = app.config.Section("files").Key("users").String()
|
||||
app.storage.loadUsers()
|
||||
if err := app.storage.loadUsers(); err != nil {
|
||||
app.err.Printf("Failed to load 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()
|
||||
@@ -394,7 +367,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)")
|
||||
}
|
||||
@@ -411,10 +384,10 @@ func start(asDaemon, firstCall bool) {
|
||||
server := app.config.Section("jellyfin").Key("server").String()
|
||||
cacheTimeout := int(app.config.Section("jellyfin").Key("cache_timeout").MustUint(30))
|
||||
stringServerType := app.config.Section("jellyfin").Key("type").String()
|
||||
timeoutHandler := common.NewTimeoutHandler("Jellyfin", server, true)
|
||||
timeoutHandler := mediabrowser.NewNamedTimeoutHandler("Jellyfin", server, true)
|
||||
if stringServerType == "emby" {
|
||||
serverType = mediabrowser.EmbyServer
|
||||
timeoutHandler = common.NewTimeoutHandler("Emby", server, true)
|
||||
timeoutHandler = mediabrowser.NewNamedTimeoutHandler("Emby", server, true)
|
||||
app.info.Println("Using Emby server type")
|
||||
fmt.Println(warning("WARNING: Emby compatibility is experimental, and support is limited.\nPassword resets are not available."))
|
||||
} else {
|
||||
@@ -431,10 +404,13 @@ func start(asDaemon, firstCall bool) {
|
||||
timeoutHandler,
|
||||
cacheTimeout,
|
||||
)
|
||||
if debugMode {
|
||||
app.jf.Verbose = true
|
||||
}
|
||||
var status int
|
||||
_, status, err = app.jf.Authenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String())
|
||||
if status != 200 || err != nil {
|
||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status)
|
||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err)
|
||||
}
|
||||
app.info.Printf("Authenticated with %s", server)
|
||||
/* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs.
|
||||
@@ -463,31 +439,47 @@ func start(asDaemon, firstCall bool) {
|
||||
}
|
||||
if noHyphens == app.jf.Hyphens {
|
||||
var newEmails map[string]interface{}
|
||||
var status int
|
||||
var err error
|
||||
var newUsers map[string]time.Time
|
||||
var status, status2 int
|
||||
var err, err2 error
|
||||
if app.jf.Hyphens {
|
||||
app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json file will be modified to match."))
|
||||
app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
|
||||
time.Sleep(time.Second * time.Duration(3))
|
||||
newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
|
||||
newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
|
||||
} else {
|
||||
app.info.Println(info("Your emails.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
|
||||
app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
|
||||
time.Sleep(time.Second * time.Duration(3))
|
||||
newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
|
||||
newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
|
||||
}
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
|
||||
app.err.Fatalf("Couldn't upgrade emails.json")
|
||||
}
|
||||
bakFile := app.storage.emails_path + ".bak"
|
||||
err = storeJSON(bakFile, app.storage.emails)
|
||||
if status2 != 200 || err2 != nil {
|
||||
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
|
||||
app.err.Fatalf("Couldn't upgrade users.json")
|
||||
}
|
||||
emailBakFile := app.storage.emails_path + ".bak"
|
||||
usersBakFile := app.storage.users_path + ".bak"
|
||||
err = storeJSON(emailBakFile, app.storage.emails)
|
||||
err2 = storeJSON(usersBakFile, app.storage.users)
|
||||
if err != nil {
|
||||
app.err.Fatalf("couldn't store emails.json backup: %s", err)
|
||||
app.err.Fatalf("couldn't store emails.json backup: %v", err)
|
||||
}
|
||||
if err2 != nil {
|
||||
app.err.Fatalf("couldn't store users.json backup: %v", err)
|
||||
}
|
||||
app.storage.emails = newEmails
|
||||
app.storage.users = newUsers
|
||||
err = app.storage.storeEmails()
|
||||
err2 = app.storage.storeUsers()
|
||||
if err != nil {
|
||||
app.err.Fatalf("couldn't store emails.json: %s", err)
|
||||
app.err.Fatalf("couldn't store emails.json: %v", err)
|
||||
}
|
||||
if err2 != nil {
|
||||
app.err.Fatalf("couldn't store users.json: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -504,6 +496,9 @@ func start(asDaemon, firstCall bool) {
|
||||
} else {
|
||||
app.debug.Println("Using Jellyfin for authentication")
|
||||
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
|
||||
if debugMode {
|
||||
app.authJf.Verbose = true
|
||||
}
|
||||
}
|
||||
|
||||
// Since email depends on language, the email reload in loadConfig won't work first time.
|
||||
@@ -531,11 +526,13 @@ func start(asDaemon, firstCall bool) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
inviteDaemon := newInviteDaemon(time.Duration(60*time.Second), app)
|
||||
go inviteDaemon.run()
|
||||
invDaemon := newInviteDaemon(time.Duration(60*time.Second), app)
|
||||
go invDaemon.run()
|
||||
defer invDaemon.shutdown()
|
||||
|
||||
userDaemon := newUserDaemon(time.Duration(60*time.Second), app)
|
||||
go userDaemon.run()
|
||||
defer userDaemon.shutdown()
|
||||
|
||||
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
|
||||
go app.StartPWR()
|
||||
@@ -692,6 +689,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)
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
package mediabrowser
|
||||
|
||||
// Almost identical to jfapi, with the most notable change being the password workaround.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func embyDeleteUser(emby *MediaBrowser, userID string) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s", emby.Server, userID)
|
||||
req, _ := http.NewRequest("DELETE", url, nil)
|
||||
for name, value := range emby.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := emby.httpClient.Do(req)
|
||||
defer emby.timeoutHandler()
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
func embyGetUsers(emby *MediaBrowser, public bool) ([]User, int, error) {
|
||||
var result []User
|
||||
var data string
|
||||
var status int
|
||||
var err error
|
||||
if time.Now().After(emby.CacheExpiry) {
|
||||
if public {
|
||||
url := fmt.Sprintf("%s/users/public", emby.Server)
|
||||
data, status, err = emby.get(url, nil)
|
||||
} else {
|
||||
url := fmt.Sprintf("%s/users", emby.Server)
|
||||
data, status, err = emby.get(url, emby.loginParams)
|
||||
}
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
json.Unmarshal([]byte(data), &result)
|
||||
emby.userCache = result
|
||||
emby.CacheExpiry = time.Now().Add(time.Minute * time.Duration(emby.cacheLength))
|
||||
if result[0].ID[8] == '-' {
|
||||
emby.Hyphens = true
|
||||
}
|
||||
return result, status, nil
|
||||
}
|
||||
return emby.userCache, 200, nil
|
||||
}
|
||||
|
||||
func embyUserByName(emby *MediaBrowser, username string, public bool) (User, int, error) {
|
||||
var match User
|
||||
find := func() (User, int, error) {
|
||||
users, status, err := emby.GetUsers(public)
|
||||
if err != nil || status != 200 {
|
||||
return User{}, status, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if user.Name == username {
|
||||
return user, status, err
|
||||
}
|
||||
}
|
||||
return User{}, status, err
|
||||
}
|
||||
match, status, err := find()
|
||||
if match.Name == "" {
|
||||
emby.CacheExpiry = time.Now()
|
||||
match, status, err = find()
|
||||
}
|
||||
return match, status, err
|
||||
}
|
||||
|
||||
func embyUserByID(emby *MediaBrowser, userID string, public bool) (User, int, error) {
|
||||
if emby.CacheExpiry.After(time.Now()) {
|
||||
for _, user := range emby.userCache {
|
||||
if user.ID == userID {
|
||||
return user, 200, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if public {
|
||||
users, status, err := emby.GetUsers(public)
|
||||
if err != nil || status != 200 {
|
||||
return User{}, status, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if user.ID == userID {
|
||||
return user, status, nil
|
||||
}
|
||||
}
|
||||
return User{}, status, err
|
||||
}
|
||||
var result User
|
||||
var data string
|
||||
var status int
|
||||
var err error
|
||||
url := fmt.Sprintf("%s/users/%s", emby.Server, userID)
|
||||
data, status, err = emby.get(url, emby.loginParams)
|
||||
if err != nil || status != 200 {
|
||||
return User{}, status, err
|
||||
}
|
||||
json.Unmarshal([]byte(data), &result)
|
||||
return result, status, nil
|
||||
}
|
||||
|
||||
// Since emby doesn't allow one to specify a password on user creation, we:
|
||||
// Create the account
|
||||
// Immediately disable it
|
||||
// Set password
|
||||
// Reeenable it
|
||||
func embyNewUser(emby *MediaBrowser, username, password string) (User, int, error) {
|
||||
url := fmt.Sprintf("%s/Users/New", emby.Server)
|
||||
data := map[string]interface{}{
|
||||
"Name": username,
|
||||
}
|
||||
response, status, err := emby.post(url, data, true)
|
||||
var recv User
|
||||
json.Unmarshal([]byte(response), &recv)
|
||||
if err != nil || !(status == 200 || status == 204) {
|
||||
return User{}, status, err
|
||||
}
|
||||
// Step 2: Set password
|
||||
id := recv.ID
|
||||
url = fmt.Sprintf("%s/Users/%s/Password", emby.Server, id)
|
||||
data = map[string]interface{}{
|
||||
"Id": id,
|
||||
"CurrentPw": "",
|
||||
"NewPw": password,
|
||||
}
|
||||
_, status, err = emby.post(url, data, false)
|
||||
// Step 3: If setting password errored, try to delete the account
|
||||
if err != nil || !(status == 200 || status == 204) {
|
||||
_, err = emby.DeleteUser(id)
|
||||
}
|
||||
return recv, status, nil
|
||||
}
|
||||
|
||||
func embySetPolicy(emby *MediaBrowser, userID string, policy Policy) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s/Policy", emby.Server, userID)
|
||||
_, status, err := emby.post(url, policy, false)
|
||||
if err != nil || status != 200 {
|
||||
return status, err
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func embySetConfiguration(emby *MediaBrowser, userID string, configuration Configuration) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s/Configuration", emby.Server, userID)
|
||||
_, status, err := emby.post(url, configuration, false)
|
||||
return status, err
|
||||
}
|
||||
|
||||
func embyGetDisplayPreferences(emby *MediaBrowser, userID string) (map[string]interface{}, int, error) {
|
||||
url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", emby.Server, userID)
|
||||
data, status, err := emby.get(url, nil)
|
||||
if err != nil || !(status == 204 || status == 200) {
|
||||
return nil, status, err
|
||||
}
|
||||
var displayprefs map[string]interface{}
|
||||
err = json.Unmarshal([]byte(data), &displayprefs)
|
||||
if err != nil {
|
||||
return nil, status, err
|
||||
}
|
||||
return displayprefs, status, nil
|
||||
}
|
||||
|
||||
func embySetDisplayPreferences(emby *MediaBrowser, userID string, displayprefs map[string]interface{}) (int, error) {
|
||||
url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", emby.Server, userID)
|
||||
_, status, err := emby.post(url, displayprefs, false)
|
||||
if err != nil || !(status == 204 || status == 200) {
|
||||
return status, err
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module github.com/hrfee/jfa-go/mediabrowser
|
||||
|
||||
go 1.15
|
||||
|
||||
replace github.com/hrfee/jfa-go/common => ../common
|
||||
|
||||
require github.com/hrfee/jfa-go/common v0.0.0-20210105184019-fdc97b4e86cc
|
||||
@@ -1,162 +0,0 @@
|
||||
package mediabrowser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func jfDeleteUser(jf *MediaBrowser, userID string) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s", jf.Server, userID)
|
||||
req, _ := http.NewRequest("DELETE", url, nil)
|
||||
for name, value := range jf.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer jf.timeoutHandler()
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
func jfGetUsers(jf *MediaBrowser, public bool) ([]User, int, error) {
|
||||
var result []User
|
||||
var data string
|
||||
var status int
|
||||
var err error
|
||||
if time.Now().After(jf.CacheExpiry) {
|
||||
if public {
|
||||
url := fmt.Sprintf("%s/users/public", jf.Server)
|
||||
data, status, err = jf.get(url, nil)
|
||||
} else {
|
||||
url := fmt.Sprintf("%s/users", jf.Server)
|
||||
data, status, err = jf.get(url, jf.loginParams)
|
||||
}
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
err := json.Unmarshal([]byte(data), &result)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, status, err
|
||||
}
|
||||
jf.userCache = result
|
||||
jf.CacheExpiry = time.Now().Add(time.Minute * time.Duration(jf.cacheLength))
|
||||
if result[0].ID[8] == '-' {
|
||||
jf.Hyphens = true
|
||||
}
|
||||
return result, status, nil
|
||||
}
|
||||
return jf.userCache, 200, nil
|
||||
}
|
||||
|
||||
func jfUserByName(jf *MediaBrowser, username string, public bool) (User, int, error) {
|
||||
var match User
|
||||
find := func() (User, int, error) {
|
||||
users, status, err := jf.GetUsers(public)
|
||||
if err != nil || status != 200 {
|
||||
return User{}, status, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if user.Name == username {
|
||||
return user, status, err
|
||||
}
|
||||
}
|
||||
return User{}, status, err
|
||||
}
|
||||
match, status, err := find()
|
||||
if match.Name == "" {
|
||||
jf.CacheExpiry = time.Now()
|
||||
match, status, err = find()
|
||||
}
|
||||
return match, status, err
|
||||
}
|
||||
|
||||
func jfUserByID(jf *MediaBrowser, userID string, public bool) (User, int, error) {
|
||||
if jf.CacheExpiry.After(time.Now()) {
|
||||
for _, user := range jf.userCache {
|
||||
if user.ID == userID {
|
||||
return user, 200, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if public {
|
||||
users, status, err := jf.GetUsers(public)
|
||||
if err != nil || status != 200 {
|
||||
return User{}, status, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if user.ID == userID {
|
||||
return user, status, nil
|
||||
}
|
||||
}
|
||||
return User{}, status, err
|
||||
}
|
||||
var result User
|
||||
var data string
|
||||
var status int
|
||||
var err error
|
||||
url := fmt.Sprintf("%s/users/%s", jf.Server, userID)
|
||||
data, status, err = jf.get(url, jf.loginParams)
|
||||
if err != nil || status != 200 {
|
||||
return User{}, status, err
|
||||
}
|
||||
json.Unmarshal([]byte(data), &result)
|
||||
return result, status, nil
|
||||
}
|
||||
|
||||
func jfNewUser(jf *MediaBrowser, username, password string) (User, int, error) {
|
||||
url := fmt.Sprintf("%s/Users/New", jf.Server)
|
||||
stringData := map[string]string{
|
||||
"Name": username,
|
||||
"Password": password,
|
||||
}
|
||||
data := make(map[string]interface{})
|
||||
for key, value := range stringData {
|
||||
data[key] = value
|
||||
}
|
||||
response, status, err := jf.post(url, data, true)
|
||||
var recv User
|
||||
json.Unmarshal([]byte(response), &recv)
|
||||
if err != nil || !(status == 200 || status == 204) {
|
||||
return User{}, status, err
|
||||
}
|
||||
return recv, status, nil
|
||||
}
|
||||
|
||||
func jfSetPolicy(jf *MediaBrowser, userID string, policy Policy) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s/Policy", jf.Server, userID)
|
||||
_, status, err := jf.post(url, policy, false)
|
||||
if err != nil || status != 200 {
|
||||
return status, err
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func jfSetConfiguration(jf *MediaBrowser, userID string, configuration Configuration) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s/Configuration", jf.Server, userID)
|
||||
_, status, err := jf.post(url, configuration, false)
|
||||
return status, err
|
||||
}
|
||||
|
||||
func jfGetDisplayPreferences(jf *MediaBrowser, userID string) (map[string]interface{}, int, error) {
|
||||
url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", jf.Server, userID)
|
||||
data, status, err := jf.get(url, nil)
|
||||
if err != nil || !(status == 204 || status == 200) {
|
||||
return nil, status, err
|
||||
}
|
||||
var displayprefs map[string]interface{}
|
||||
err = json.Unmarshal([]byte(data), &displayprefs)
|
||||
if err != nil {
|
||||
return nil, status, err
|
||||
}
|
||||
return displayprefs, status, nil
|
||||
}
|
||||
|
||||
func jfSetDisplayPreferences(jf *MediaBrowser, userID string, displayprefs map[string]interface{}) (int, error) {
|
||||
url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", jf.Server, userID)
|
||||
_, status, err := jf.post(url, displayprefs, false)
|
||||
if err != nil || !(status == 204 || status == 200) {
|
||||
return status, err
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
package mediabrowser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
)
|
||||
|
||||
type serverType int
|
||||
|
||||
const (
|
||||
JellyfinServer serverType = iota
|
||||
EmbyServer
|
||||
)
|
||||
|
||||
type serverInfo struct {
|
||||
LocalAddress string `json:"LocalAddress"`
|
||||
Name string `json:"ServerName"`
|
||||
Version string `json:"Version"`
|
||||
OS string `json:"OperatingSystem"`
|
||||
ID string `json:"Id"`
|
||||
}
|
||||
|
||||
// MediaBrowser is an api instance of Jellyfin/Emby.
|
||||
type MediaBrowser struct {
|
||||
Server string
|
||||
client string
|
||||
version string
|
||||
device string
|
||||
deviceID string
|
||||
useragent string
|
||||
auth string
|
||||
header map[string]string
|
||||
ServerInfo serverInfo
|
||||
Username string
|
||||
password string
|
||||
Authenticated bool
|
||||
AccessToken string
|
||||
userID string
|
||||
httpClient *http.Client
|
||||
loginParams map[string]string
|
||||
userCache []User
|
||||
CacheExpiry time.Time
|
||||
cacheLength int
|
||||
noFail bool
|
||||
Hyphens bool
|
||||
serverType serverType
|
||||
timeoutHandler common.TimeoutHandler
|
||||
}
|
||||
|
||||
// NewServer returns a new Jellyfin object.
|
||||
func NewServer(st serverType, server, client, version, device, deviceID string, timeoutHandler common.TimeoutHandler, cacheTimeout int) (*MediaBrowser, error) {
|
||||
mb := &MediaBrowser{}
|
||||
mb.serverType = st
|
||||
mb.Server = server
|
||||
mb.client = client
|
||||
mb.version = version
|
||||
mb.device = device
|
||||
mb.deviceID = deviceID
|
||||
mb.useragent = fmt.Sprintf("%s/%s", client, version)
|
||||
mb.timeoutHandler = timeoutHandler
|
||||
mb.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\"", client, device, deviceID, version)
|
||||
mb.header = map[string]string{
|
||||
"Accept": "application/json",
|
||||
"Content-type": "application/json; charset=UTF-8",
|
||||
"X-Application": mb.useragent,
|
||||
"Accept-Charset": "UTF-8,*",
|
||||
"Accept-Encoding": "gzip",
|
||||
"User-Agent": mb.useragent,
|
||||
"X-Emby-Authorization": mb.auth,
|
||||
}
|
||||
mb.httpClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
infoURL := fmt.Sprintf("%s/System/Info/Public", server)
|
||||
req, _ := http.NewRequest("GET", infoURL, nil)
|
||||
resp, err := mb.httpClient.Do(req)
|
||||
defer mb.timeoutHandler()
|
||||
if err == nil {
|
||||
data, _ := ioutil.ReadAll(resp.Body)
|
||||
json.Unmarshal(data, &mb.ServerInfo)
|
||||
}
|
||||
mb.cacheLength = cacheTimeout
|
||||
mb.CacheExpiry = time.Now()
|
||||
return mb, nil
|
||||
}
|
||||
|
||||
func (mb *MediaBrowser) get(url string, params map[string]string) (string, int, error) {
|
||||
var req *http.Request
|
||||
if params != nil {
|
||||
jsonParams, _ := json.Marshal(params)
|
||||
req, _ = http.NewRequest("GET", url, bytes.NewBuffer(jsonParams))
|
||||
} else {
|
||||
req, _ = http.NewRequest("GET", url, nil)
|
||||
}
|
||||
for name, value := range mb.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := mb.httpClient.Do(req)
|
||||
defer mb.timeoutHandler()
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 401 && mb.Authenticated {
|
||||
mb.Authenticated = false
|
||||
_, _, authErr := mb.Authenticate(mb.Username, mb.password)
|
||||
if authErr == nil {
|
||||
v1, v2, v3 := mb.get(url, params)
|
||||
return v1, v2, v3
|
||||
}
|
||||
}
|
||||
return "", resp.StatusCode, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var data io.Reader
|
||||
encoding := resp.Header.Get("Content-Encoding")
|
||||
switch encoding {
|
||||
case "gzip":
|
||||
data, _ = gzip.NewReader(resp.Body)
|
||||
default:
|
||||
data = resp.Body
|
||||
}
|
||||
buf := new(strings.Builder)
|
||||
io.Copy(buf, data)
|
||||
//var respData map[string]interface{}
|
||||
//json.NewDecoder(data).Decode(&respData)
|
||||
return buf.String(), resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (mb *MediaBrowser) post(url string, data interface{}, response bool) (string, int, error) {
|
||||
params, _ := json.Marshal(data)
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params))
|
||||
for name, value := range mb.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := mb.httpClient.Do(req)
|
||||
defer mb.timeoutHandler()
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 401 && mb.Authenticated {
|
||||
mb.Authenticated = false
|
||||
_, _, authErr := mb.Authenticate(mb.Username, mb.password)
|
||||
if authErr == nil {
|
||||
v1, v2, v3 := mb.post(url, data, response)
|
||||
return v1, v2, v3
|
||||
}
|
||||
}
|
||||
return "", resp.StatusCode, err
|
||||
}
|
||||
if response {
|
||||
defer resp.Body.Close()
|
||||
var outData io.Reader
|
||||
switch resp.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
outData, _ = gzip.NewReader(resp.Body)
|
||||
default:
|
||||
outData = resp.Body
|
||||
}
|
||||
buf := new(strings.Builder)
|
||||
io.Copy(buf, outData)
|
||||
return buf.String(), resp.StatusCode, nil
|
||||
}
|
||||
return "", resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// Authenticate attempts to authenticate using a username & password
|
||||
func (mb *MediaBrowser) Authenticate(username, password string) (User, int, error) {
|
||||
mb.Username = username
|
||||
mb.password = password
|
||||
mb.loginParams = map[string]string{
|
||||
"Username": username,
|
||||
"Pw": password,
|
||||
"Password": password,
|
||||
}
|
||||
buffer := &bytes.Buffer{}
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetEscapeHTML(false)
|
||||
err := encoder.Encode(mb.loginParams)
|
||||
if err != nil {
|
||||
return User{}, 0, err
|
||||
}
|
||||
// loginParams, _ := json.Marshal(jf.loginParams)
|
||||
url := fmt.Sprintf("%s/Users/authenticatebyname", mb.Server)
|
||||
req, err := http.NewRequest("POST", url, buffer)
|
||||
defer mb.timeoutHandler()
|
||||
if err != nil {
|
||||
return User{}, 0, err
|
||||
}
|
||||
for name, value := range mb.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := mb.httpClient.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return User{}, resp.StatusCode, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var d io.Reader
|
||||
switch resp.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
d, _ = gzip.NewReader(resp.Body)
|
||||
default:
|
||||
d = resp.Body
|
||||
}
|
||||
data, err := io.ReadAll(d)
|
||||
if err != nil {
|
||||
return User{}, 0, err
|
||||
}
|
||||
var respData map[string]interface{}
|
||||
json.Unmarshal(data, &respData)
|
||||
mb.AccessToken = respData["AccessToken"].(string)
|
||||
var user User
|
||||
ju, err := json.Marshal(respData["User"])
|
||||
if err != nil {
|
||||
return User{}, 0, err
|
||||
}
|
||||
json.Unmarshal(ju, &user)
|
||||
mb.userID = user.ID
|
||||
mb.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\", Token=\"%s\"", mb.client, mb.device, mb.deviceID, mb.version, mb.AccessToken)
|
||||
mb.header["X-Emby-Authorization"] = mb.auth
|
||||
mb.Authenticated = true
|
||||
return user, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// DeleteUser deletes the user corresponding to the provided ID.
|
||||
func (mb *MediaBrowser) DeleteUser(userID string) (int, error) {
|
||||
if mb.serverType == JellyfinServer {
|
||||
return jfDeleteUser(mb, userID)
|
||||
}
|
||||
return embyDeleteUser(mb, userID)
|
||||
}
|
||||
|
||||
// GetUsers returns all (visible) users on the Emby instance.
|
||||
func (mb *MediaBrowser) GetUsers(public bool) ([]User, int, error) {
|
||||
if mb.serverType == JellyfinServer {
|
||||
return jfGetUsers(mb, public)
|
||||
}
|
||||
return embyGetUsers(mb, public)
|
||||
}
|
||||
|
||||
// UserByName returns the user corresponding to the provided username.
|
||||
func (mb *MediaBrowser) UserByName(username string, public bool) (User, int, error) {
|
||||
if mb.serverType == JellyfinServer {
|
||||
return jfUserByName(mb, username, public)
|
||||
}
|
||||
return embyUserByName(mb, username, public)
|
||||
}
|
||||
|
||||
// UserByID returns the user corresponding to the provided ID.
|
||||
func (mb *MediaBrowser) UserByID(userID string, public bool) (User, int, error) {
|
||||
if mb.serverType == JellyfinServer {
|
||||
return jfUserByID(mb, userID, public)
|
||||
}
|
||||
return embyUserByID(mb, userID, public)
|
||||
}
|
||||
|
||||
// NewUser creates a new user with the provided username and password.
|
||||
func (mb *MediaBrowser) NewUser(username, password string) (User, int, error) {
|
||||
if mb.serverType == JellyfinServer {
|
||||
return jfNewUser(mb, username, password)
|
||||
}
|
||||
return embyNewUser(mb, username, password)
|
||||
}
|
||||
|
||||
// SetPolicy sets the access policy for the user corresponding to the provided ID.
|
||||
func (mb *MediaBrowser) SetPolicy(userID string, policy Policy) (int, error) {
|
||||
if mb.serverType == JellyfinServer {
|
||||
return jfSetPolicy(mb, userID, policy)
|
||||
}
|
||||
return embySetPolicy(mb, userID, policy)
|
||||
}
|
||||
|
||||
// SetConfiguration sets the configuration (part of homescreen layout) for the user corresponding to the provided ID.
|
||||
func (mb *MediaBrowser) SetConfiguration(userID string, configuration Configuration) (int, error) {
|
||||
if mb.serverType == JellyfinServer {
|
||||
return jfSetConfiguration(mb, userID, configuration)
|
||||
}
|
||||
return embySetConfiguration(mb, userID, configuration)
|
||||
}
|
||||
|
||||
// GetDisplayPreferences gets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID.
|
||||
func (mb *MediaBrowser) GetDisplayPreferences(userID string) (map[string]interface{}, int, error) {
|
||||
if mb.serverType == JellyfinServer {
|
||||
return jfGetDisplayPreferences(mb, userID)
|
||||
}
|
||||
return embyGetDisplayPreferences(mb, userID)
|
||||
}
|
||||
|
||||
// SetDisplayPreferences sets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID.
|
||||
func (mb *MediaBrowser) SetDisplayPreferences(userID string, displayprefs map[string]interface{}) (int, error) {
|
||||
if mb.serverType == JellyfinServer {
|
||||
return jfSetDisplayPreferences(mb, userID, displayprefs)
|
||||
}
|
||||
return embySetDisplayPreferences(mb, userID, displayprefs)
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package mediabrowser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type magicParse struct {
|
||||
Parsed time.Time `json:"parseme"`
|
||||
}
|
||||
|
||||
type Time struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
func (t *Time) UnmarshalJSON(b []byte) (err error) {
|
||||
str := strings.TrimSuffix(strings.TrimPrefix(string(b), "\""), "\"")
|
||||
// Trim nanoseconds to always have 6 digits, so overall length is always the same.
|
||||
if str[len(str)-1] == 'Z' {
|
||||
str = str[:26] + "Z"
|
||||
} else {
|
||||
str = str[:26]
|
||||
}
|
||||
// decent method
|
||||
t.Time, err = time.Parse("2006-01-02T15:04:05.000000Z", str)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
t.Time, err = time.Parse("2006-01-02T15:04:05.000000", str)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
// emby method
|
||||
t.Time, err = time.Parse("2006-01-02T15:04:05.0000000+00:00", str)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
fmt.Println("THIRDERR", err)
|
||||
// magic method
|
||||
// some stored dates from jellyfin have no timezone at the end, if not we assume UTC
|
||||
if str[len(str)-1] != 'Z' {
|
||||
str += "Z"
|
||||
}
|
||||
timeJSON := []byte("{ \"parseme\": \"" + str + "\" }")
|
||||
var parsed magicParse
|
||||
// Magically turn it into a time.Time
|
||||
err = json.Unmarshal(timeJSON, &parsed)
|
||||
t.Time = parsed.Parsed
|
||||
return
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `json:"Name"`
|
||||
ServerID string `json:"ServerId"`
|
||||
ID string `json:"Id"`
|
||||
HasPassword bool `json:"HasPassword"`
|
||||
HasConfiguredPassword bool `json:"HasConfiguredPassword"`
|
||||
HasConfiguredEasyPassword bool `json:"HasConfiguredEasyPassword"`
|
||||
EnableAutoLogin bool `json:"EnableAutoLogin"`
|
||||
LastLoginDate Time `json:"LastLoginDate"`
|
||||
LastActivityDate Time `json:"LastActivityDate"`
|
||||
Configuration Configuration `json:"Configuration"`
|
||||
Policy Policy `json:"Policy"`
|
||||
}
|
||||
|
||||
type SessionInfo struct {
|
||||
RemoteEndpoint string `json:"RemoteEndPoint"`
|
||||
UserID string `json:"UserId"`
|
||||
}
|
||||
|
||||
type AuthenticationResult struct {
|
||||
User User `json:"User"`
|
||||
AccessToken string `json:"AccessToken"`
|
||||
ServerID string `json:"ServerId"`
|
||||
SessionInfo SessionInfo `json:"SessionInfo"`
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
PlayDefaultAudioTrack bool `json:"PlayDefaultAudioTrack"`
|
||||
SubtitleLanguagePreference string `json:"SubtitleLanguagePreference"`
|
||||
DisplayMissingEpisodes bool `json:"DisplayMissingEpisodes"`
|
||||
GroupedFolders []interface{} `json:"GroupedFolders"`
|
||||
SubtitleMode string `json:"SubtitleMode"`
|
||||
DisplayCollectionsView bool `json:"DisplayCollectionsView"`
|
||||
EnableLocalPassword bool `json:"EnableLocalPassword"`
|
||||
OrderedViews []interface{} `json:"OrderedViews"`
|
||||
LatestItemsExcludes []interface{} `json:"LatestItemsExcludes"`
|
||||
MyMediaExcludes []interface{} `json:"MyMediaExcludes"`
|
||||
HidePlayedInLatest bool `json:"HidePlayedInLatest"`
|
||||
RememberAudioSelections bool `json:"RememberAudioSelections"`
|
||||
RememberSubtitleSelections bool `json:"RememberSubtitleSelections"`
|
||||
EnableNextEpisodeAutoPlay bool `json:"EnableNextEpisodeAutoPlay"`
|
||||
}
|
||||
type Policy struct {
|
||||
IsAdministrator bool `json:"IsAdministrator"`
|
||||
IsHidden bool `json:"IsHidden"`
|
||||
IsDisabled bool `json:"IsDisabled"`
|
||||
BlockedTags []interface{} `json:"BlockedTags"`
|
||||
EnableUserPreferenceAccess bool `json:"EnableUserPreferenceAccess"`
|
||||
AccessSchedules []interface{} `json:"AccessSchedules"`
|
||||
BlockUnratedItems []interface{} `json:"BlockUnratedItems"`
|
||||
EnableRemoteControlOfOtherUsers bool `json:"EnableRemoteControlOfOtherUsers"`
|
||||
EnableSharedDeviceControl bool `json:"EnableSharedDeviceControl"`
|
||||
EnableRemoteAccess bool `json:"EnableRemoteAccess"`
|
||||
EnableLiveTvManagement bool `json:"EnableLiveTvManagement"`
|
||||
EnableLiveTvAccess bool `json:"EnableLiveTvAccess"`
|
||||
EnableMediaPlayback bool `json:"EnableMediaPlayback"`
|
||||
EnableAudioPlaybackTranscoding bool `json:"EnableAudioPlaybackTranscoding"`
|
||||
EnableVideoPlaybackTranscoding bool `json:"EnableVideoPlaybackTranscoding"`
|
||||
EnablePlaybackRemuxing bool `json:"EnablePlaybackRemuxing"`
|
||||
ForceRemoteSourceTranscoding bool `json:"ForceRemoteSourceTranscoding"`
|
||||
EnableContentDeletion bool `json:"EnableContentDeletion"`
|
||||
EnableContentDeletionFromFolders []interface{} `json:"EnableContentDeletionFromFolders"`
|
||||
EnableContentDownloading bool `json:"EnableContentDownloading"`
|
||||
EnableSyncTranscoding bool `json:"EnableSyncTranscoding"`
|
||||
EnableMediaConversion bool `json:"EnableMediaConversion"`
|
||||
EnabledDevices []interface{} `json:"EnabledDevices"`
|
||||
EnableAllDevices bool `json:"EnableAllDevices"`
|
||||
EnabledChannels []interface{} `json:"EnabledChannels"`
|
||||
EnableAllChannels bool `json:"EnableAllChannels"`
|
||||
EnabledFolders []string `json:"EnabledFolders"`
|
||||
EnableAllFolders bool `json:"EnableAllFolders"`
|
||||
InvalidLoginAttemptCount int `json:"InvalidLoginAttemptCount"`
|
||||
LoginAttemptsBeforeLockout int `json:"LoginAttemptsBeforeLockout"`
|
||||
MaxActiveSessions int `json:"MaxActiveSessions"`
|
||||
EnablePublicSharing bool `json:"EnablePublicSharing"`
|
||||
BlockedMediaFolders []interface{} `json:"BlockedMediaFolders"`
|
||||
BlockedChannels []interface{} `json:"BlockedChannels"`
|
||||
RemoteClientBitrateLimit int `json:"RemoteClientBitrateLimit"`
|
||||
AuthenticationProviderID string `json:"AuthenticationProviderId"`
|
||||
PasswordResetProviderID string `json:"PasswordResetProviderId"`
|
||||
SyncPlayAccess string `json:"SyncPlayAccess"`
|
||||
}
|
||||
61
models.go
61
models.go
@@ -29,11 +29,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
|
||||
@@ -72,23 +81,25 @@ type newProfileDTO struct {
|
||||
}
|
||||
|
||||
type inviteDTO struct {
|
||||
Code string `json:"code" example:"sajdlj23423j23"` // Invite code
|
||||
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
|
||||
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
|
||||
Created string `json:"created" example:"01/01/20 12:00"` // Date of creation
|
||||
Profile string `json:"profile" example:"DefaultProfile"` // Profile used on this invite
|
||||
UsedBy [][]string `json:"used-by,omitempty"` // Users who have used this invite
|
||||
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
|
||||
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
|
||||
Email string `json:"email,omitempty"` // Email the invite was sent to (if applicable)
|
||||
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
||||
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
||||
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
|
||||
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
|
||||
Created int64 `json:"created" example:"1617737207510"` // Date of creation
|
||||
Profile string `json:"profile" example:"DefaultProfile"` // Profile used on this invite
|
||||
UsedBy map[string]int64 `json:"used-by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time
|
||||
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
|
||||
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
|
||||
Email string `json:"email,omitempty"` // Email the invite was sent to (if applicable)
|
||||
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
||||
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
||||
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
|
||||
}
|
||||
|
||||
type getInvitesDTO struct {
|
||||
@@ -112,9 +123,9 @@ 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 string `json:"last_active"` // Time of last activity on Jellyfin
|
||||
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 string `json:"expiry" example:"01/02/21 12:00"` // Expiry time of user, if applicable.
|
||||
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.
|
||||
}
|
||||
|
||||
@@ -203,15 +214,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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
16
router.go
16
router.go
@@ -85,7 +85,7 @@ func (app *appContext) loadRouter(address string, debug bool) *gin.Engine {
|
||||
app.loadHTML(router)
|
||||
router.Use(static.Serve("/", app.webFS))
|
||||
router.NoRoute(app.NoRouteHandler)
|
||||
if debug {
|
||||
if *PPROF {
|
||||
app.debug.Println("Loading pprof")
|
||||
pprof.Register(router)
|
||||
}
|
||||
@@ -105,6 +105,11 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
router.GET(p+"/lang/:page", app.GetLanguages)
|
||||
router.Use(static.Serve(p+"/", app.webFS))
|
||||
router.GET(p+"/", app.AdminPage)
|
||||
|
||||
if app.config.Section("password_resets").Key("link_reset").MustBool(false) {
|
||||
router.GET(p+"/reset", app.ResetPassword)
|
||||
}
|
||||
|
||||
router.GET(p+"/accounts", app.AdminPage)
|
||||
router.GET(p+"/settings", app.AdminPage)
|
||||
router.GET(p+"/lang/:page/:file", app.ServeLang)
|
||||
@@ -127,6 +132,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)
|
||||
@@ -142,10 +148,10 @@ 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)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
external = ["false", "external", "no", "n"]
|
||||
|
||||
with open("embed.go", "w") as f:
|
||||
choice = ""
|
||||
try:
|
||||
choice = sys.argv[1]
|
||||
except IndexError:
|
||||
pass
|
||||
folder = Path("embed")
|
||||
if choice in external:
|
||||
embed = False
|
||||
shutil.copy(folder / "external.go", "embed.go")
|
||||
print("Embedding disabled. \"data\" must be placed alongside the executable.")
|
||||
else:
|
||||
shutil.copy(folder / "internal.go", "embed.go")
|
||||
print("Embedding enabled.")
|
||||
|
||||
79
setup.go
79
setup.go
@@ -7,8 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
"github.com/hrfee/jfa-go/mediabrowser"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
)
|
||||
|
||||
func (app *appContext) ServeSetup(gc *gin.Context) {
|
||||
@@ -60,7 +59,7 @@ func (app *appContext) TestJF(gc *gin.Context) {
|
||||
if req.ServerType == "emby" {
|
||||
serverType = mediabrowser.EmbyServer
|
||||
}
|
||||
tempjf, _ := mediabrowser.NewServer(serverType, req.Server, "jfa-go-setup", app.version, "auth", "auth", common.NewTimeoutHandler("authJF", req.Server, true), 30)
|
||||
tempjf, _ := mediabrowser.NewServer(serverType, req.Server, "jfa-go-setup", app.version, "auth", "auth", mediabrowser.NewNamedTimeoutHandler("authJF", req.Server, true), 30)
|
||||
_, status, err := tempjf.Authenticate(req.Username, req.Password)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.info.Printf("Auth failed with code %d (%s)", status, err)
|
||||
@@ -74,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))
|
||||
@@ -85,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 {
|
||||
@@ -111,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
496
storage.go
496
storage.go
@@ -11,7 +11,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/mediabrowser"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
@@ -26,7 +26,7 @@ type Storage struct {
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
lang Lang
|
||||
invitesLock sync.Mutex
|
||||
invitesLock, usersLock sync.Mutex
|
||||
}
|
||||
|
||||
type customEmails struct {
|
||||
@@ -34,6 +34,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 +43,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
|
||||
@@ -59,37 +62,42 @@ type Profile struct {
|
||||
}
|
||||
|
||||
type Invite struct {
|
||||
Created time.Time `json:"created"`
|
||||
NoLimit bool `json:"no-limit"`
|
||||
RemainingUses int `json:"remaining-uses"`
|
||||
ValidTill time.Time `json:"valid_till"`
|
||||
UserExpiry bool `json:"user-duration"`
|
||||
UserDays int `json:"user-days,omitempty"`
|
||||
UserHours int `json:"user-hours,omitempty"`
|
||||
UserMinutes int `json:"user-minutes,omitempty"`
|
||||
Email string `json:"email"`
|
||||
UsedBy [][]string `json:"used-by"`
|
||||
Notify map[string]map[string]bool `json:"notify"`
|
||||
Profile string `json:"profile"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Keys []string `json"keys,omitempty"`
|
||||
Created time.Time `json:"created"`
|
||||
NoLimit bool `json:"no-limit"`
|
||||
RemainingUses int `json:"remaining-uses"`
|
||||
ValidTill time.Time `json:"valid_till"`
|
||||
UserExpiry bool `json:"user-duration"`
|
||||
UserMonths int `json:"user-months,omitempty"`
|
||||
UserDays int `json:"user-days,omitempty"`
|
||||
UserHours int `json:"user-hours,omitempty"`
|
||||
UserMinutes int `json:"user-minutes,omitempty"`
|
||||
Email string `json:"email"`
|
||||
// Used to be stored as formatted time, now as Unix.
|
||||
UsedBy [][]string `json:"used-by"`
|
||||
Notify map[string]map[string]bool `json:"notify"`
|
||||
Profile string `json:"profile"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Keys []string `json:"keys,omitempty"`
|
||||
}
|
||||
|
||||
type Lang struct {
|
||||
chosenFormLang string
|
||||
chosenAdminLang string
|
||||
chosenEmailLang string
|
||||
AdminPath string
|
||||
Admin adminLangs
|
||||
AdminJSON map[string]string
|
||||
FormPath string
|
||||
Form formLangs
|
||||
EmailPath string
|
||||
Email emailLangs
|
||||
CommonPath string
|
||||
Common commonLangs
|
||||
SetupPath string
|
||||
Setup setupLangs
|
||||
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
|
||||
}
|
||||
|
||||
func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
||||
@@ -105,55 +113,94 @@ func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = st.loadLangPWR(filesystems...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = st.loadLangEmail(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) {
|
||||
for n, ev := range *english {
|
||||
qs, ok := (*other)[n]
|
||||
if !ok {
|
||||
(*other)[n] = ev
|
||||
return
|
||||
} else if qs.Singular == "" {
|
||||
qs.Singular = ev.Singular
|
||||
} else if (*other)[n].Plural == "" {
|
||||
qs.Plural = ev.Plural
|
||||
func patchQuantityStrings(to *map[string]quantityString, from ...*map[string]quantityString) {
|
||||
if *to == nil {
|
||||
*to = map[string]quantityString{}
|
||||
}
|
||||
for n, ev := range *from[len(from)-1] {
|
||||
qs, ok := (*to)[n]
|
||||
if !ok || qs.Singular == "" || qs.Plural == "" {
|
||||
i := 0
|
||||
subOk := false
|
||||
for i < len(from)-1 {
|
||||
ev, subOk = (*from[i])[n]
|
||||
if subOk && ev.Singular != "" && ev.Plural != "" {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if !ok {
|
||||
(*to)[n] = ev
|
||||
continue
|
||||
} else if qs.Singular == "" {
|
||||
qs.Singular = ev.Singular
|
||||
} else if qs.Plural == "" {
|
||||
qs.Plural = ev.Plural
|
||||
}
|
||||
(*to)[n] = qs
|
||||
}
|
||||
(*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))
|
||||
@@ -168,34 +215,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,7 +273,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))
|
||||
@@ -223,11 +290,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 {
|
||||
@@ -239,27 +322,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,7 +359,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))
|
||||
@@ -287,11 +376,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 {
|
||||
@@ -308,27 +413,106 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !formLoaded {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *Storage) loadLangPWR(filesystems ...fs.FS) error {
|
||||
st.lang.PasswordReset = map[string]pwrLang{}
|
||||
var english pwrLang
|
||||
loadedLangs := make([]map[string]bool, len(filesystems))
|
||||
var load loadLangFunc
|
||||
load = func(fsIndex int, fname string) error {
|
||||
filesystem := filesystems[fsIndex]
|
||||
index := strings.TrimSuffix(fname, filepath.Ext(fname))
|
||||
lang := pwrLang{}
|
||||
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.PasswordResetPath, fname))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if substituteStrings != "" {
|
||||
f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
|
||||
}
|
||||
err = json.Unmarshal(f, &lang)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.lang.Common.patchCommon(&lang.Strings, index)
|
||||
if fname != "en-us.json" {
|
||||
if lang.Meta.Fallback != "" {
|
||||
fallback, ok := st.lang.PasswordReset[lang.Meta.Fallback]
|
||||
err = nil
|
||||
if !ok {
|
||||
err = load(fsIndex, lang.Meta.Fallback+".json")
|
||||
fallback = st.lang.PasswordReset[lang.Meta.Fallback]
|
||||
}
|
||||
if err == nil {
|
||||
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
||||
}
|
||||
}
|
||||
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
||||
patchLang(&lang.Strings, &english.Strings)
|
||||
}
|
||||
}
|
||||
st.lang.PasswordReset[index] = lang
|
||||
return nil
|
||||
}
|
||||
engFound := false
|
||||
var err error
|
||||
for i := range filesystems {
|
||||
loadedLangs[i] = map[string]bool{}
|
||||
err = load(i, "en-us.json")
|
||||
if err == nil {
|
||||
engFound = true
|
||||
}
|
||||
loadedLangs[i]["en-us.json"] = true
|
||||
}
|
||||
if !engFound {
|
||||
return err
|
||||
}
|
||||
english = st.lang.PasswordReset["en-us"]
|
||||
formLoaded := false
|
||||
for i := range filesystems {
|
||||
files, err := fs.ReadDir(filesystems[i], st.lang.PasswordResetPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, f := range files {
|
||||
if !loadedLangs[i][f.Name()] {
|
||||
err = load(i, f.Name())
|
||||
if err == nil {
|
||||
formLoaded = true
|
||||
loadedLangs[i][f.Name()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,7 +526,10 @@ func (st *Storage) loadLangForm(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))
|
||||
@@ -356,41 +543,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)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -416,7 +635,22 @@ func (st *Storage) storeInvites() error {
|
||||
}
|
||||
|
||||
func (st *Storage) loadUsers() error {
|
||||
return loadJSON(st.users_path, &st.users)
|
||||
st.usersLock.Lock()
|
||||
defer st.usersLock.Unlock()
|
||||
if st.users == nil {
|
||||
st.users = map[string]time.Time{}
|
||||
}
|
||||
temp := map[string]time.Time{}
|
||||
err := loadJSON(st.users_path, &temp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for id, t1 := range temp {
|
||||
if _, ok := st.users[id]; !ok {
|
||||
st.users[id] = t1
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *Storage) storeUsers() error {
|
||||
@@ -559,7 +793,7 @@ func hyphenate(userID string) string {
|
||||
return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
|
||||
}
|
||||
|
||||
func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||
func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||
jfUsers, status, err := app.jf.GetUsers(false)
|
||||
if status != 200 || err != nil {
|
||||
return nil, status, err
|
||||
@@ -568,15 +802,15 @@ func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[
|
||||
for _, user := range jfUsers {
|
||||
unHyphenated := user.ID
|
||||
hyphenated := hyphenate(unHyphenated)
|
||||
email, ok := old[hyphenated]
|
||||
val, ok := old[hyphenated]
|
||||
if ok {
|
||||
newEmails[unHyphenated] = email
|
||||
newEmails[unHyphenated] = val
|
||||
}
|
||||
}
|
||||
return newEmails, status, err
|
||||
}
|
||||
|
||||
func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||
func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||
jfUsers, status, err := app.jf.GetUsers(false)
|
||||
if status != 200 || err != nil {
|
||||
return nil, status, err
|
||||
@@ -585,10 +819,50 @@ func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[st
|
||||
for _, user := range jfUsers {
|
||||
unstripped := user.ID
|
||||
stripped := strings.ReplaceAll(unstripped, "-", "")
|
||||
email, ok := old[stripped]
|
||||
val, ok := old[stripped]
|
||||
if ok {
|
||||
newEmails[unstripped] = email
|
||||
newEmails[unstripped] = val
|
||||
}
|
||||
}
|
||||
return newEmails, status, err
|
||||
}
|
||||
|
||||
func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||
return app.hyphenateStorage(old)
|
||||
}
|
||||
|
||||
func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||
return app.deHyphenateStorage(old)
|
||||
}
|
||||
|
||||
func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
|
||||
asInterface := map[string]interface{}{}
|
||||
for k, v := range old {
|
||||
asInterface[k] = v
|
||||
}
|
||||
fixed, status, err := app.hyphenateStorage(asInterface)
|
||||
if err != nil {
|
||||
return nil, status, err
|
||||
}
|
||||
out := map[string]time.Time{}
|
||||
for k, v := range fixed {
|
||||
out[k] = v.(time.Time)
|
||||
}
|
||||
return out, status, err
|
||||
}
|
||||
|
||||
func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
|
||||
asInterface := map[string]interface{}{}
|
||||
for k, v := range old {
|
||||
asInterface[k] = v
|
||||
}
|
||||
fixed, status, err := app.deHyphenateStorage(asInterface)
|
||||
if err != nil {
|
||||
return nil, status, err
|
||||
}
|
||||
out := map[string]time.Time{}
|
||||
for k, v := range fixed {
|
||||
out[k] = v.(time.Time)
|
||||
}
|
||||
return out, status, err
|
||||
}
|
||||
|
||||
69
stripmd.go
69
stripmd.go
@@ -6,50 +6,41 @@ import (
|
||||
stripmd "github.com/writeas/go-strip-markdown"
|
||||
)
|
||||
|
||||
func stripMarkdown(md string) string {
|
||||
// Search for markdown-formatted urls, and replace them with just the url, then use a library to strip any traces of markdown. You'll need some eyebleach after this.
|
||||
foundOpenSquare := false
|
||||
openSquare := -1
|
||||
openBracket := -1
|
||||
closeBracket := -1
|
||||
openSquares := []int{}
|
||||
closeBrackets := []int{}
|
||||
links := []string{}
|
||||
foundOpen := false
|
||||
for i, c := range md {
|
||||
if !foundOpenSquare && !foundOpen && c != '[' && c != ']' {
|
||||
// StripAltText removes Markdown alt text from links and images and replaces them with just the URL.
|
||||
// Currently uses the deepest alt text when links/images are nested.
|
||||
func StripAltText(md string) string {
|
||||
altTextStart := -1 // Start of alt text (between '[' & ']')
|
||||
URLStart := -1 // Start of url (between '(' & ')')
|
||||
URLEnd := -1
|
||||
previousURLEnd := -2
|
||||
out := ""
|
||||
for i := range md {
|
||||
if altTextStart != -1 && URLStart != -1 && md[i] == ')' {
|
||||
URLEnd = i - 1
|
||||
out += md[previousURLEnd+2:altTextStart-1] + md[URLStart:URLEnd+1]
|
||||
previousURLEnd = URLEnd
|
||||
altTextStart, URLStart, URLEnd = -1, -1, -1
|
||||
continue
|
||||
}
|
||||
if c == '[' && md[i-1] != '!' {
|
||||
foundOpenSquare = true
|
||||
openSquare = i
|
||||
} else if c == ']' {
|
||||
if md[i+1] == '(' {
|
||||
foundOpenSquare = false
|
||||
foundOpen = true
|
||||
openBracket = i + 1
|
||||
continue
|
||||
if md[i] == '[' && altTextStart == -1 {
|
||||
altTextStart = i + 1
|
||||
if i > 0 && md[i-1] == '!' {
|
||||
altTextStart--
|
||||
}
|
||||
} else if c == ')' {
|
||||
closeBracket = i
|
||||
openSquares = append(openSquares, openSquare)
|
||||
closeBrackets = append(closeBrackets, closeBracket)
|
||||
links = append(links, md[openBracket+1:closeBracket])
|
||||
openBracket = -1
|
||||
closeBracket = -1
|
||||
openSquare = -1
|
||||
foundOpenSquare = false
|
||||
foundOpen = false
|
||||
}
|
||||
if i > 0 && md[i-1] == ']' && md[i] == '(' && URLStart == -1 {
|
||||
URLStart = i + 1
|
||||
}
|
||||
}
|
||||
fullLinks := make([]string, len(openSquares))
|
||||
for i := range openSquares {
|
||||
if openSquares[i] != -1 && closeBrackets[i] != -1 {
|
||||
fullLinks[i] = md[openSquares[i] : closeBrackets[i]+1]
|
||||
}
|
||||
if previousURLEnd+1 != len(md)-1 {
|
||||
out += md[previousURLEnd+2:]
|
||||
}
|
||||
for i, _ := range openSquares {
|
||||
md = strings.Replace(md, fullLinks[i], links[i], 1)
|
||||
if out == "" {
|
||||
return md
|
||||
}
|
||||
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(md), "</p>"), "<p>")
|
||||
return out
|
||||
}
|
||||
|
||||
func stripMarkdown(md string) string {
|
||||
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(StripAltText(md)), "</p>"), "<p>")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Modal } from "./modules/modal.js";
|
||||
import { _get, _post, toggleLoader } from "./modules/common.js";
|
||||
import { _get, _post, toggleLoader, toDateString } from "./modules/common.js";
|
||||
import { loadLangSelector } from "./modules/lang.js";
|
||||
|
||||
interface formWindow extends Window {
|
||||
@@ -11,6 +11,7 @@ interface formWindow extends Window {
|
||||
confirmation: boolean;
|
||||
confirmationModal: Modal
|
||||
userExpiryEnabled: boolean;
|
||||
userExpiryMonths: number;
|
||||
userExpiryDays: number;
|
||||
userExpiryHours: number;
|
||||
userExpiryMinutes: number;
|
||||
@@ -43,10 +44,11 @@ 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);
|
||||
messageEl.textContent = window.userExpiryMessage.replace("{date}", time.toDateString() + " " + time.toLocaleTimeString());
|
||||
messageEl.textContent = window.userExpiryMessage.replace("{date}", toDateString(time));
|
||||
setTimeout(calculateTime, 1000);
|
||||
};
|
||||
calculateTime();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user