1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-29 12:30:11 +00:00

Compare commits

...

26 Commits

Author SHA1 Message Date
9eb803388e
add it-it email back 2021-02-17 16:54:09 +00:00
eb81515f46
fix ordering of steps in dockerfile 2021-02-17 16:53:15 +00:00
52461c0356
add it-it email back 2021-02-17 16:36:39 +00:00
6b3800abf6
remove branch trigger 2021-02-17 16:34:04 +00:00
09fc81d7f4
fix tag recognition by drone 2021-02-17 16:26:14 +00:00
82034a2586
use python3 in makefile for embed 2021-02-17 16:11:01 +00:00
5e001bed60
temporarily modify lang for release
Fixed name on Indonesian and removed Italian emails.
2021-02-17 16:04:25 +00:00
5d7972db56
rename embed/noembed to internal-files/external-files 2021-02-17 14:41:44 +00:00
403ad58274
move all scripts to scripts/ 2021-02-17 14:32:03 +00:00
a1a233e74f
fix sed path in Dockerfile 2021-02-17 11:33:41 +00:00
8dd72c95ab
switch image for drone builds 2021-02-17 11:19:51 +00:00
f794322392
Merge branch 'go1.16'
merge go1.16 changes

This includes embedded files for releases (no extra 'data' directory!)
and support for a custom language file directory, allowing one to
customize the text accross the app.
2021-02-17 11:15:21 +00:00
c3fb00a307
wrong go version container 2021-02-12 15:37:19 +00:00
988829a6db
dont build docker on go1.16 branch 2021-02-12 15:22:21 +00:00
a6a7710a79
use filepath.Join wrapper for different embed and os path styles
If using internal, "/" is used as a separator always, and with external,
filepath.Join is used.
2021-02-12 14:59:35 +00:00
873afb47cd
strip debug symbols in makefile 2021-02-12 14:59:35 +00:00
ea99966057
refactor, move route loading to router.go 2021-02-12 14:59:16 +00:00
aaed272bf2
use embed.fs wrapper on data 2021-02-12 14:35:16 +00:00
e6775cd2d1
use embed.fs wrapper for langFS so lang/ is not needed in paths
[files]lang_files is now the path to the lang directory, not path to a
directory containing it.
2021-02-12 14:35:16 +00:00
98a9e20cc0
Fix docker build, add GOBINARY flag for make
GOBINARY defaults to "go", but if you want to build on a normal system,
you'll likely set it to go1.16rc1 with "make all GOBINARY=go1.16rc1".
2021-02-12 14:35:13 +00:00
ee37588959
drone image 2021-02-12 14:32:57 +00:00
cb12c6f441
update goreleaser 2021-02-12 14:32:57 +00:00
72cf3e2240
add external/internal data options
"make all" will build with internal data, whereas "make debug"/"make
all-external" will make an external "data/" directory.
2021-02-12 14:32:48 +00:00
815bdc35ac
fully self-contained
paths are pretty janky, but it works. Also, [files]/lang_files now must
be the path to a directory CONTAINING a "lang/" directory. I'll work
around this at a later date.
2021-02-12 14:28:09 +00:00
0330540f87
Use fs for language, add lang_files option
The local app translations are loaded, and then if [files]/lang_files
is provided (a directory containing custom translations), any found
inside it are loaded over top. This makes customizing much easier.
2021-02-12 14:28:09 +00:00
fefe2d82a4
rebase 12/02, use go1.16rc1 in make, remove ioutil, start switching to io/fs for file i/o
ioutil's contents are now in io and os.
Eventually jfa-go's files will be embedded in the binary with go1.16's
new embed feature. Using io/fs will provide abstraction for accessing
these files, and allow for both embedded and non-embedded versions.
Also, internal paths to things like email templates, etc. will be
prefixed with "jfa-go:" to indicate to use the app's own Filesystem
instead of reading the file normally. This also allows for custom files
to continue to be used as they are currently.
2021-02-12 14:27:01 +00:00
32 changed files with 718 additions and 642 deletions

View File

@ -21,9 +21,7 @@ steps:
- curl -sL https://git.io/goreleaser | bash
trigger:
event:
- tag
branch:
- main
- tag
---
name: docker-buildx
kind: pipeline
@ -50,9 +48,7 @@ steps:
- /mnt/buildx/jfa-go/build.sh stable
trigger:
event:
- tag
branch:
- main
- tag
volumes:
- name: ssh_key
host:
@ -144,4 +140,3 @@ trigger:
event:
include:
- pull_request

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ dist/
build/
data/
version.go
embed.go
notes
docs/*
lang/langtostruct.py

View File

@ -17,15 +17,16 @@ before:
- cp node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 data/web/css/
- cp -r html data/
- cp -r lang data/
- python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json
- python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini
- python3 mail/generate.py -o data/
- python3 version.py {{.Version}} version.go
- python3 scripts/enumerate_config.py -i config/config-base.json -o data/config-base.json
- python3 scripts/generate_ini.py -i config/config-base.json -o data/config-default.ini
- python3 scripts/compile_mjml.py -o data/
- python3 scripts/version.py {{.Version}}
- npx esbuild --bundle ts/admin.ts --outfile=./data/web/js/admin.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:
@ -40,14 +41,10 @@ builds:
- arm64
archives:
- replacements:
darwin: Darwin
darwin: macOS
linux: Linux
windows: Windows
amd64: x86_64
files:
- data/*
- data/**/*
- data/**/**/*
checksum:
name_template: 'checksums.txt'
snapshot:

View File

@ -6,8 +6,8 @@ RUN apt update -y \
&& apt install build-essential python3-pip curl software-properties-common sed -y \
&& (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \
&& apt install nodejs \
&& (cd /opt/build; make configuration npm email version typescript bundle-css swagger copy 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
&& (cd /opt/build; make configuration npm email version typescript bundle-css swagger copy external-files GOESBUILD=on) \
&& sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' /opt/build/data/html/setup.html
FROM --platform=$BUILDPLATFORM golang:latest AS build
@ -23,6 +23,7 @@ FROM golang:latest
COPY --from=build /opt/build/build /opt/jfa-go
EXPOSE 8056
EXPOSE 8057
CMD [ "/opt/jfa-go/jfa-go", "-data", "/data" ]

View File

@ -4,6 +4,7 @@ ifeq ($(GOESBUILD), on)
else
ESBUILD := npx esbuild
endif
GOBINARY ?= go
npm:
$(info installing npm dependencies)
@ -16,68 +17,84 @@ npm:
configuration:
$(info Fixing config-base)
-mkdir -p build/data
python3 config/fixconfig.py -i config/config-base.json -o build/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 config/generate_ini.py -i config/config-base.json -o build/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 mail/generate.py -o build/data/
python3 scripts/compile_mjml.py -o data/
typescript:
$(info compiling typescript)
-mkdir -p build/data/web/js
-$(ESBUILD) --bundle ts/admin.ts --outfile=./build/data/web/js/admin.js --minify
-$(ESBUILD) --bundle ts/form.ts --outfile=./build/data/web/js/form.js --minify
-$(ESBUILD) --bundle ts/setup.ts --outfile=./build/data/web/js/setup.js --minify
-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 build/data/web/js
-$(ESBUILD) --bundle ts/admin.ts --sourcemap --outfile=./build/data/web/js/admin.js
-$(ESBUILD) --bundle ts/form.ts --sourcemap --outfile=./build/data/web/js/form.js
-$(ESBUILD) --bundle ts/setup.ts --sourcemap --outfile=./build/data/web/js/setup.js
-rm -r build/data/web/js/ts
-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 build/data/web/js
cp -r ts data/web/js
swagger:
go get github.com/swaggo/swag/cmd/swag
$(GOBINARY) get github.com/swaggo/swag/cmd/swag
swag init -g main.go
version:
python3 version.py auto version.go
python3 scripts/version.py auto
compile:
$(info Downloading deps)
go mod download
$(GOBINARY) mod download
$(info Building)
mkdir -p build
CGO_ENABLED=0 go build -o build/jfa-go *.go
cd build && CGO_ENABLED=0 $(GOBINARY) build -ldflags="-s -w" -o ./jfa-go ../*.go
compile-debug:
$(info Downloading deps)
$(GOBINARY) mod download
$(info Building)
mkdir -p build
cd build && CGO_ENABLED=0 $(GOBINARY) build -o ./jfa-go ../*.go
compress:
upx --lzma build/jfa-go
bundle-css:
-mkdir -p build/data/web/css
-mkdir -p data/web/css
$(info bundling css)
$(ESBUILD) --bundle css/base.css --outfile=build/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 build/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 build/data/
cp -r html data/
$(info copying static data)
-mkdir -p build/data/web
cp -r static/* build/data/web/
-mkdir -p data/web
cp -r static/* data/web/
$(info copying language files)
cp -r lang build/data/
cp -r lang 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/
install:
cp -r build $(DESTDIR)/jfa-go
all: configuration npm email version typescript bundle-css swagger compile copy
debug: configuration npm email version ts-debug bundle-css swagger compile copy
all: configuration npm email version typescript bundle-css swagger copy internal-files compile
all-external: configuration npm email version typescript bundle-css swagger copy external-files compile
debug: configuration npm email version ts-debug bundle-css swagger copy external-files compile-debug

View File

@ -39,9 +39,8 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/) or [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/).
For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.pw/view/hrfee/jfa-go)), and extract `jfa-go` and `data` to the same directory.
* For linux users, you can place them inside `/opt/jfa-go` and then run
`sudo ln -s /opt/jfa-go/jfa-go /usr/bin/jfa-go` to place it in your PATH.
For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.pw/view/hrfee/jfa-go)), and extract the `jfa-go` executable to somewhere useful.
* For \*nix/macOS users, `chmod +x jfa-go` then place it somewhere in your PATH like `/usr/bin`.
Run the executable to start.

3
api.go
View File

@ -1330,6 +1330,9 @@ func (app *appContext) GetLanguages(gc *gin.Context) {
gc.JSON(200, resp)
}
// @Summary Restarts the program. No response means success.
// @Router /restart [post]
// @tags Other
func (app *appContext) restart(gc *gin.Context) {
app.info.Println("Restarting...")
err := app.Restart()

View File

@ -2,6 +2,7 @@ package main
import (
"fmt"
"io/fs"
"path/filepath"
"strconv"
"strings"
@ -11,6 +12,14 @@ import (
var emailEnabled = false
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
val := app.config.Section(sect).Key(key).MustString("")
if strings.HasPrefix(val, "jfa-go:") {
return localFS, strings.TrimPrefix(val, "jfa-go:")
}
return app.systemFS, val
}
func (app *appContext) loadConfig() error {
var err error
app.config, err = ini.Load(app.configPath)
@ -31,26 +40,26 @@ func (app *appContext) loadConfig() error {
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false)))
app.config.Section("password_resets").Key("email_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString(filepath.Join(app.localPath, "email.html")))
app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString(filepath.Join(app.localPath, "email.txt")))
app.config.Section("password_resets").Key("email_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString("jfa-go:" + "email.html"))
app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString("jfa-go:" + "email.txt"))
app.config.Section("invite_emails").Key("email_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(app.localPath, "invite-email.html")))
app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(app.localPath, "invite-email.txt")))
app.config.Section("invite_emails").Key("email_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString("jfa-go:" + "invite-email.html"))
app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString("jfa-go:" + "invite-email.txt"))
app.config.Section("email_confirmation").Key("email_html").SetValue(app.config.Section("email_confirmation").Key("email_html").MustString(filepath.Join(app.localPath, "confirmation.html")))
app.config.Section("email_confirmation").Key("email_text").SetValue(app.config.Section("email_confirmation").Key("email_text").MustString(filepath.Join(app.localPath, "confirmation.txt")))
app.config.Section("email_confirmation").Key("email_html").SetValue(app.config.Section("email_confirmation").Key("email_html").MustString("jfa-go:" + "confirmation.html"))
app.config.Section("email_confirmation").Key("email_text").SetValue(app.config.Section("email_confirmation").Key("email_text").MustString("jfa-go:" + "confirmation.txt"))
app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(app.localPath, "expired.html")))
app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(app.localPath, "expired.txt")))
app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString("jfa-go:" + "expired.html"))
app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString("jfa-go:" + "expired.txt"))
app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.localPath, "created.html")))
app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.localPath, "created.txt")))
app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString("jfa-go:" + "created.html"))
app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString("jfa-go:" + "created.txt"))
app.config.Section("deletion").Key("email_html").SetValue(app.config.Section("deletion").Key("email_html").MustString(filepath.Join(app.localPath, "deleted.html")))
app.config.Section("deletion").Key("email_text").SetValue(app.config.Section("deletion").Key("email_text").MustString(filepath.Join(app.localPath, "deleted.txt")))
app.config.Section("deletion").Key("email_html").SetValue(app.config.Section("deletion").Key("email_html").MustString("jfa-go:" + "deleted.html"))
app.config.Section("deletion").Key("email_text").SetValue(app.config.Section("deletion").Key("email_text").MustString("jfa-go:" + "deleted.txt"))
app.config.Section("welcome_email").Key("email_html").SetValue(app.config.Section("welcome_email").Key("email_html").MustString(filepath.Join(app.localPath, "welcome.html")))
app.config.Section("welcome_email").Key("email_text").SetValue(app.config.Section("welcome_email").Key("email_text").MustString(filepath.Join(app.localPath, "welcome.txt")))
app.config.Section("welcome_email").Key("email_html").SetValue(app.config.Section("welcome_email").Key("email_html").MustString("jfa-go:" + "welcome.html"))
app.config.Section("welcome_email").Key("email_text").SetValue(app.config.Section("welcome_email").Key("email_text").MustString("jfa-go:" + "welcome.txt"))
app.config.Section("jellyfin").Key("version").SetValue(VERSION)
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")

View File

@ -65,7 +65,7 @@
["emby", "Emby"]
],
"value": "jellyfin",
"description": "Note: Emby integration works is missing some features, such as Password Resets."
"description": "Note: Emby integration works but is missing some features, such as Password Resets."
},
"substitute_jellyfin_strings": {
"name": "Substitute occurrences of \"Jellyfin\"",
@ -821,30 +821,6 @@
"value": "",
"description": "Location of stored Ombi user template."
},
"user_template": {
"name": "User Template (Deprecated)",
"required": false,
"requires_restart": true,
"type": "text",
"value": "",
"description": "Deprecated in favor of User Profiles. Location of stored user policy template (json)."
},
"user_configuration": {
"name": "userConfiguration (Deprecated)",
"required": false,
"requires_restart": true,
"type": "text",
"value": "",
"description": "Deprecated in favor of User Profiles. Location of stored user configuration template (used for setting homescreen layout) (json)"
},
"user_displayprefs": {
"name": "displayPreferences (Deprecated)",
"required": false,
"requires_restart": true,
"type": "text",
"value": "",
"description": "Deprecated in favor of User Profiles. Location of stored displayPreferences template (also used for homescreen layout) (json)"
},
"user_profiles": {
"name": "User Profiles",
"required": false,
@ -860,6 +836,14 @@
"type": "text",
"value": "",
"description": "Path to directory containing custom versions of web ui pages. See wiki for more info."
},
"lang_files": {
"name": "Custom language files directory",
"required": false,
"requires_restart": true,
"type": "text",
"value": "",
"description": "The path to a directory which following the same form as the internal 'lang/' directory. See GitHub for more info."
}
}
}

262
email.go
View File

@ -153,6 +153,28 @@ func (emailer *Emailer) NewSMTP(server string, port int, username, password stri
}
}
func (emailer *Emailer) construct(app *appContext, section, keyFragment string, data map[string]interface{}) (html, text string, err error) {
var tpl *template.Template
for _, key := range []string{"html", "text"} {
filesystem, fpath := app.GetPath(section, keyFragment+key)
tpl, err = template.ParseFS(filesystem, fpath)
if err != nil {
return
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, data)
if err != nil {
return
}
if key == "html" {
html = tplData.String()
} else {
text = tplData.String()
}
}
return
}
func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext) (*Email, error) {
email := &Email{
subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")),
@ -160,30 +182,17 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
message := app.config.Section("email").Key("message").String()
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key)
for _, key := range []string{"html", "text"} {
fpath := app.config.Section("email_confirmation").Key("email_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"helloUser": emailer.lang.Strings.format("helloUser", username),
"clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
"urlVal": inviteLink,
"confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"),
"message": message,
})
if err != nil {
return nil, err
}
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
var err error
email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", map[string]interface{}{
"helloUser": emailer.lang.Strings.format("helloUser", username),
"clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
"urlVal": inviteLink,
"confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"),
"message": message,
})
if err != nil {
return nil, err
}
return email, nil
}
@ -197,31 +206,18 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
message := app.config.Section("email").Key("message").String()
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
inviteLink = fmt.Sprintf("%s/%s", inviteLink, code)
for _, key := range []string{"html", "text"} {
fpath := app.config.Section("invite_emails").Key("email_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"hello": emailer.lang.InviteEmail.get("hello"),
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
"toJoin": emailer.lang.InviteEmail.get("toJoin"),
"inviteExpiry": emailer.lang.InviteEmail.format("inviteExpiry", d, t, expiresIn),
"linkButton": emailer.lang.InviteEmail.get("linkButton"),
"invite_link": inviteLink,
"message": message,
})
if err != nil {
return nil, err
}
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
var err error
email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", map[string]interface{}{
"hello": emailer.lang.InviteEmail.get("hello"),
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
"toJoin": emailer.lang.InviteEmail.get("toJoin"),
"inviteExpiry": emailer.lang.InviteEmail.format("inviteExpiry", d, t, expiresIn),
"linkButton": emailer.lang.InviteEmail.get("linkButton"),
"invite_link": inviteLink,
"message": message,
})
if err != nil {
return nil, err
}
return email, nil
}
@ -231,26 +227,14 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont
subject: emailer.lang.InviteExpiry.get("title"),
}
expiry := app.formatDatetime(invite.ValidTill)
for _, key := range []string{"html", "text"} {
fpath := app.config.Section("notifications").Key("expiry_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"),
"expiredAt": emailer.lang.InviteExpiry.format("expiredAt", "\""+code+"\"", expiry),
"notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"),
})
if err != nil {
return nil, err
}
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
var err error
email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", map[string]interface{}{
"inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"),
"expiredAt": emailer.lang.InviteExpiry.format("expiredAt", "\""+code+"\"", expiry),
"notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"),
})
if err != nil {
return nil, err
}
return email, nil
}
@ -266,31 +250,19 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite
} else {
tplAddress = address
}
for _, key := range []string{"html", "text"} {
fpath := app.config.Section("notifications").Key("created_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"aUserWasCreated": emailer.lang.UserCreated.format("aUserWasCreated", "\""+code+"\""),
"name": emailer.lang.Strings.get("name"),
"address": emailer.lang.Strings.get("emailAddress"),
"time": emailer.lang.UserCreated.get("time"),
"nameVal": username,
"addressVal": tplAddress,
"timeVal": created,
"notificationNotice": emailer.lang.UserCreated.get("notificationNotice"),
})
if err != nil {
return nil, err
}
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
var err error
email.html, email.text, err = emailer.construct(app, "notifications", "created_", map[string]interface{}{
"aUserWasCreated": emailer.lang.UserCreated.format("aUserWasCreated", "\""+code+"\""),
"name": emailer.lang.Strings.get("name"),
"address": emailer.lang.Strings.get("emailAddress"),
"time": emailer.lang.UserCreated.get("time"),
"nameVal": username,
"addressVal": tplAddress,
"timeVal": created,
"notificationNotice": emailer.lang.UserCreated.get("notificationNotice"),
})
if err != nil {
return nil, err
}
return email, nil
}
@ -301,31 +273,19 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Ema
}
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
message := app.config.Section("email").Key("message").String()
for _, key := range []string{"html", "text"} {
fpath := app.config.Section("password_resets").Key("email_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"helloUser": emailer.lang.Strings.format("helloUser", pwr.Username),
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
"ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"),
"codeExpiry": emailer.lang.PasswordReset.format("codeExpiry", d, t, expiresIn),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
"pin": emailer.lang.PasswordReset.get("pin"),
"pinVal": pwr.Pin,
"message": message,
})
if err != nil {
return nil, err
}
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
var err error
email.html, email.text, err = emailer.construct(app, "password_resets", "email_", map[string]interface{}{
"helloUser": emailer.lang.Strings.format("helloUser", pwr.Username),
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
"ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"),
"codeExpiry": emailer.lang.PasswordReset.format("codeExpiry", d, t, expiresIn),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
"pin": emailer.lang.PasswordReset.get("pin"),
"pinVal": pwr.Pin,
"message": message,
})
if err != nil {
return nil, err
}
return email, nil
}
@ -334,26 +294,14 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email
email := &Email{
subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
}
for _, key := range []string{"html", "text"} {
fpath := app.config.Section("deletion").Key("email_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
"reason": emailer.lang.UserDeleted.get("reason"),
"reasonVal": reason,
})
if err != nil {
return nil, err
}
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
var err error
email.html, email.text, err = emailer.construct(app, "deletion", "email_", map[string]interface{}{
"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
"reason": emailer.lang.UserDeleted.get("reason"),
"reasonVal": reason,
})
if err != nil {
return nil, err
}
return email, nil
}
@ -362,30 +310,18 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext) (*Ema
email := &Email{
subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")),
}
for _, key := range []string{"html", "text"} {
fpath := app.config.Section("welcome_email").Key("email_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
"welcome": emailer.lang.WelcomeEmail.get("welcome"),
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"),
"jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"),
"jellyfinURLVal": app.config.Section("jellyfin").Key("public_server").String(),
"username": emailer.lang.Strings.get("username"),
"usernameVal": username,
"message": app.config.Section("email").Key("message").String(),
})
if err != nil {
return nil, err
}
if key == "html" {
email.html = tplData.String()
} else {
email.text = tplData.String()
}
var err error
email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", map[string]interface{}{
"welcome": emailer.lang.WelcomeEmail.get("welcome"),
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"),
"jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"),
"jellyfinURLVal": app.config.Section("jellyfin").Key("public_server").String(),
"username": emailer.lang.Strings.get("username"),
"usernameVal": username,
"message": app.config.Section("email").Key("message").String(),
})
if err != nil {
return nil, err
}
return email, nil
}

1
embed/README.md Normal file
View File

@ -0,0 +1 @@
`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.

20
embed/external.go Normal file
View File

@ -0,0 +1,20 @@
package main
import (
"io/fs"
"log"
"os"
"path/filepath"
)
var localFS fs.FS
var langFS fs.FS
func FSJoin(elem ...string) string { return filepath.Join(elem...) }
func loadFilesystems() {
log.Println("Using external storage")
executable, _ := os.Executable()
localFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data"))
langFS = os.DirFS(filepath.Join(filepath.Dir(executable), "data", "lang"))
}

38
embed/internal.go Normal file
View File

@ -0,0 +1,38 @@
package main
import (
"embed"
"io/fs"
"log"
)
//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
var laFS embed.FS
var langFS rewriteFS
var localFS rewriteFS
type rewriteFS struct {
fs embed.FS
prefix string
}
func (l rewriteFS) Open(name string) (fs.File, error) { return l.fs.Open(l.prefix + name) }
func (l rewriteFS) ReadDir(name string) ([]fs.DirEntry, error) { return l.fs.ReadDir(l.prefix + name) }
func (l rewriteFS) ReadFile(name string) ([]byte, error) { return l.fs.ReadFile(l.prefix + name) }
func FSJoin(elem ...string) string {
out := ""
for _, v := range elem {
out += v + "/"
}
return out[:len(out)-1]
}
func loadFilesystems() {
langFS = rewriteFS{laFS, "lang/"}
localFS = rewriteFS{loFS, "data/"}
log.Println("Using internal storage")
}

8
go.mod
View File

@ -1,6 +1,6 @@
module github.com/hrfee/jfa-go
go 1.14
go 1.16
replace github.com/hrfee/jfa-go/docs => ./docs
@ -17,8 +17,7 @@ require (
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
github.com/gin-gonic/gin v1.6.3
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-openapi/spec v0.20.1 // indirect
github.com/go-openapi/swag v0.19.13 // indirect
github.com/go-openapi/spec v0.20.3 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/golang/protobuf v1.4.3 // indirect
github.com/google/uuid v1.1.2 // indirect
@ -31,6 +30,7 @@ require (
github.com/lithammer/shortuuid/v3 v3.0.4
github.com/logrusorgru/aurora/v3 v3.0.0
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/smartystreets/goconvey v1.6.4 // indirect
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
@ -38,9 +38,7 @@ require (
github.com/swaggo/swag v1.7.0 // indirect
github.com/ugorji/go v1.2.0 // indirect
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/text v0.3.5 // indirect
golang.org/x/tools v0.1.0 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/ini.v1 v1.62.0

80
go.sum
View File

@ -1,4 +1,6 @@
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
@ -9,18 +11,24 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
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/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=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=
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/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=
@ -48,42 +56,32 @@ github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmC
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4=
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg=
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo=
github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/spec v0.19.14 h1:r4fbYFo6N4ZelmSX8G6p+cv/hZRXzcuqQIADGT1iNKM=
github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
github.com/go-openapi/spec v0.20.1 h1:5WNKTzPguDN+79wbJw2UE2q+eX+gUmEFsIKSvnSQJlc=
github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ=
github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ=
github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.11 h1:RFTu/dlFySpyVvJDfp/7674JY4SDglYWKztbiIGFpmc=
github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY=
github.com/go-openapi/swag v0.19.12 h1:Bc0bnY2c3AoF7Gc+IMIAQQsD8fLHjHpc19wXvYuayQI=
github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M=
github.com/go-openapi/swag v0.19.13 h1:233UVgMy1DlmCYYfOiFpta6e2urloh+sEs5id6lyzog=
github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
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-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=
@ -92,16 +90,16 @@ github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTM
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@ -117,8 +115,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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=
@ -131,7 +129,6 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -139,8 +136,10 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
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=
github.com/kr/pty v1.1.5 h1:hyz3dwM5QLc1Rfoz4FuWJQG5BN7tc6K1MndAUnGpQr4=
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -157,33 +156,33 @@ github.com/mailgun/mailgun-go/v4 v4.3.0/go.mod h1:fWuBI2iaS/pSSyo6+EBpHjatQO3lV8
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
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/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=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@ -192,6 +191,7 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
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/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=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -200,46 +200,43 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0=
github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI=
github.com/swaggo/gin-swagger v1.3.0 h1:eOmp7r57oUgZPw2dJOjcGNMse9cvXcI4tTqBcnZtPsI=
github.com/swaggo/gin-swagger v1.3.0/go.mod h1:oy1BRA6WvgtCp848lhxce7BnWH4C8Bxa0m5SkWx+cS0=
github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
github.com/swaggo/swag v1.6.7 h1:e8GC2xDllJZr3omJkm9YfmK0Y56+rMO3cg0JBKNz09s=
github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=
github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E=
github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.1.13/go.mod h1:jxau1n+/wyTGLQoCkjok9r5zFa/FxT6eI5HiHKQszjc=
github.com/ugorji/go v1.2.0 h1:6eXlzYLLwZwXroJx9NyqbYcbv/d93twiOzQLDewE6qM=
github.com/ugorji/go v1.2.0/go.mod h1:1ny++pKMXhLWrwWV5Nf+CbOuZJhMoaFD+0GMFfd8fEc=
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCBFCq1OeuU=
github.com/ugorji/go/codec v1.2.0 h1:As6RccOIlbm9wHuWYMlB30dErcI+4WiKWsYsmPkyrUw=
github.com/ugorji/go/codec v1.2.0/go.mod h1:dXvG35r7zTX6QImXOSFhGMmKtX+wJ7VTWzGvYQGIjBs=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@ -254,21 +251,18 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
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 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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/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=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -279,23 +273,17 @@ golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
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/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=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -307,10 +295,8 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59 h1:QjA/9ArTfVTLfEhClDCG7SGrZkZixxWpwNCDiwJfh88=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e h1:t96dS3DO8DGjawSLJL/HIdz8CycAd2v07XxqB3UPTi0=
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
@ -320,12 +306,15 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@ -333,7 +322,6 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
@ -342,17 +330,18 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
@ -360,4 +349,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -1,6 +1,6 @@
{
"meta": {
"name": "Inggris (Amerika Serikat)"
"name": "Bahasa Indonesia (ID)"
},
"strings": {
"invites": "Undangan",

View File

@ -1,6 +1,6 @@
{
"meta": {
"name": "Inggris (Amerika Serikat)"
"name": "Bahasa Indonesia (ID)"
},
"strings": {
"username": "Nama pengguna",

View File

@ -1,6 +1,6 @@
{
"meta": {
"name": "Inggris (Amerika Serikat)"
"name": "Bahasa Indonesia (ID)"
},
"strings": {
"ifItWasNotYou": "Jika ini bukan kamu, silahkan mengabaikan email ini.",

View File

@ -1,6 +1,6 @@
{
"meta": {
"name": "Inggris (Amerika Serikat)"
"name": "Bahasa Indonesia (ID)"
},
"strings": {
"pageTitle": "Buat Akun Jellyfin",

View File

@ -1,6 +1,6 @@
{
"meta": {
"name": "Inggris (Amerika Serikat)"
"name": "Bahasa Indonesia (ID)"
},
"strings": {
"pageTitle": "Persiapan - jfa-go",

225
main.go
View File

@ -8,7 +8,7 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"io/fs"
"log"
"mime"
"net"
@ -22,20 +22,27 @@ import (
"strings"
"time"
"github.com/gin-contrib/pprof"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"github.com/hrfee/jfa-go/common"
_ "github.com/hrfee/jfa-go/docs"
"github.com/hrfee/jfa-go/mediabrowser"
"github.com/hrfee/jfa-go/ombi"
"github.com/lithammer/shortuuid/v3"
"github.com/logrusorgru/aurora/v3"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"gopkg.in/ini.v1"
)
var (
PLATFORM string = runtime.GOOS
SOCK string = "jfa-go.sock"
SRV *http.Server
RESTART chan bool
DATA, CONFIG, HOST *string
PORT *int
DEBUG *bool
TEST bool
SWAGGER *bool
)
var serverTypes = map[string]string{
"jellyfin": "Jellyfin",
"emby": "Emby (experimental)",
@ -50,7 +57,7 @@ type User struct {
Password string `json:"password"`
}
// contains everything the application needs, essentially. Wouldn't do this in the future.
// contains (almost) everything the application needs, essentially. This was a dumb design decision imo.
type appContext struct {
// defaults *Config
config *ini.File
@ -58,7 +65,8 @@ type appContext struct {
configBasePath string
configBase settings
dataPath string
localPath string
systemFS fs.FS
webFS httpFS
cssClass string
jellyfinLogin bool
users []User
@ -80,27 +88,6 @@ type appContext struct {
URLBase string
}
func (app *appContext) loadHTML(router *gin.Engine) {
customPath := app.config.Section("files").Key("html_templates").MustString("")
templatePath := filepath.Join(app.localPath, "html")
htmlFiles, err := ioutil.ReadDir(templatePath)
if err != nil {
app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath)
return
}
loadFiles := make([]string, len(htmlFiles))
for i, f := range htmlFiles {
if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) {
app.debug.Printf("Using default \"%s\"", f.Name())
loadFiles[i] = filepath.Join(templatePath, f.Name())
} else {
app.info.Printf("Using custom \"%s\"", f.Name())
loadFiles[i] = filepath.Join(filepath.Join(customPath, f.Name()))
}
}
router.LoadHTMLFiles(loadFiles...)
}
func generateSecret(length int) (string, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
@ -110,46 +97,6 @@ func generateSecret(length int) (string, error) {
return base64.URLEncoding.EncodeToString(bytes), err
}
func setGinLogger(router *gin.Engine, debugMode bool) {
if debugMode {
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("[GIN/DEBUG] %s: %s(%s) => %d in %s; %s\n",
param.TimeStamp.Format("15:04:05"),
param.Method,
param.Path,
param.StatusCode,
param.Latency,
func() string {
if param.ErrorMessage != "" {
return "Error: " + param.ErrorMessage
}
return ""
}(),
)
}))
} else {
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("[GIN] %s(%s) => %d\n",
param.Method,
param.Path,
param.StatusCode,
)
}))
}
}
var (
PLATFORM string = runtime.GOOS
SOCK string = "jfa-go.sock"
SRV *http.Server
RESTART chan bool
DATA, CONFIG, HOST *string
PORT *int
DEBUG *bool
TEST bool
SWAGGER *bool
)
func test(app *appContext) {
fmt.Printf("\n\n----\n\n")
settings := map[string]interface{}{
@ -186,15 +133,20 @@ func start(asDaemon, firstCall bool) {
app := new(appContext)
/*
set default config, data and local paths
also, confusing naming here. data_path is not the internal 'data' directory, rather the users .config/jfa-go folder.
local_path is the internal 'data' directory.
set default config and data paths
data: Contains invites.json, emails.json, user_profile.json, etc.
config: config.ini. Usually in data, but can be changed via -config.
localFS is jfa-go's internal data. On external builds, the directory is named "data" and placed next to the executable.
*/
userConfigDir, _ := os.UserConfigDir()
app.dataPath = filepath.Join(userConfigDir, "jfa-go")
app.configPath = filepath.Join(app.dataPath, "config.ini")
executable, _ := os.Executable()
app.localPath = filepath.Join(filepath.Dir(executable), "data")
app.systemFS = os.DirFS("/")
// gin-static doesn't just take a plain http.FileSystem, so we implement it's ServeFileSystem. See static.go.
app.webFS = httpFS{
hfs: http.FS(localFS),
fs: localFS,
}
app.info = log.New(os.Stdout, "[INFO] ", log.Ltime)
app.err = log.New(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile)
@ -251,23 +203,19 @@ func start(asDaemon, firstCall bool) {
}
if _, err := os.Stat(app.configPath); os.IsNotExist(err) {
firstRun = true
dConfigPath := filepath.Join(app.localPath, "config-default.ini")
var dConfig *os.File
dConfig, err = os.Open(dConfigPath)
dConfig, err := fs.ReadFile(localFS, "config-default.ini")
if err != nil {
app.err.Fatalf("Couldn't find default config file \"%s\"", dConfigPath)
app.err.Fatalf("Couldn't find default config file")
}
defer dConfig.Close()
var nConfig *os.File
nConfig, err := os.Create(app.configPath)
if err != nil {
app.err.Printf("Couldn't open config file for writing: \"%s\"", app.configPath)
app.err.Fatalf("Error: %s", err)
}
defer nConfig.Close()
_, err = io.Copy(nConfig, dConfig)
_, err = nConfig.Write(dConfig)
if err != nil {
app.err.Fatalf("Couldn't copy default config. To do this manually, copy\n%s\nto\n%s", dConfigPath, app.configPath)
app.err.Fatalf("Couldn't copy default config.")
}
app.info.Printf("Copied default configuration to \"%s\"", app.configPath)
}
@ -288,7 +236,7 @@ func start(asDaemon, firstCall bool) {
app.info.Print(aurora.Magenta("\n\nWARNING: Don't use debug mode in production, as it exposes pprof on the network.\n\n"))
app.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile)
} else {
app.debug = log.New(ioutil.Discard, "", 0)
app.debug = log.New(io.Discard, "", 0)
}
if asDaemon {
@ -330,11 +278,17 @@ func start(asDaemon, firstCall bool) {
}()
}
app.storage.lang.CommonPath = filepath.Join(app.localPath, "lang", "common")
app.storage.lang.FormPath = filepath.Join(app.localPath, "lang", "form")
app.storage.lang.AdminPath = filepath.Join(app.localPath, "lang", "admin")
app.storage.lang.EmailPath = filepath.Join(app.localPath, "lang", "email")
err := app.storage.loadLang()
app.storage.lang.CommonPath = "common"
app.storage.lang.FormPath = "form"
app.storage.lang.AdminPath = "admin"
app.storage.lang.EmailPath = "email"
externalLang := app.config.Section("files").Key("lang_files").MustString("")
var err error
if externalLang == "" {
err = app.storage.loadLang(langFS)
} else {
err = app.storage.loadLang(langFS, os.DirFS(externalLang))
}
if err != nil {
app.info.Fatalf("Failed to load language files: %+v\n", err)
}
@ -413,8 +367,8 @@ func start(asDaemon, firstCall bool) {
}
app.configBasePath = filepath.Join(app.localPath, "config-base.json")
configBase, _ := ioutil.ReadFile(app.configBasePath)
app.configBasePath = "config-base.json"
configBase, _ := fs.ReadFile(localFS, app.configBasePath)
json.Unmarshal(configBase, &app.configBase)
themes := map[string]string{
@ -562,8 +516,8 @@ func start(asDaemon, firstCall bool) {
} else {
debugMode = false
address = "0.0.0.0:8056"
app.storage.lang.SetupPath = filepath.Join(app.localPath, "lang", "setup")
err := app.storage.loadLangSetup()
app.storage.lang.SetupPath = "setup"
err := app.storage.loadLangSetup(langFS)
if err != nil {
app.info.Fatalf("Failed to load language files: %+v\n", err)
}
@ -572,89 +526,15 @@ func start(asDaemon, firstCall bool) {
// workaround for potentially broken windows mime types
mime.AddExtensionType(".js", "application/javascript")
app.info.Println("Initializing router")
router := app.loadRouter(address, debugMode)
app.info.Println("Loading routes")
if debugMode {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
setGinLogger(router, debugMode)
router.Use(gin.Recovery())
routePrefixes := []string{app.URLBase}
if app.URLBase != "" {
routePrefixes = append(routePrefixes, "")
}
for _, p := range routePrefixes {
router.Use(static.Serve(p+"/", static.LocalFile(filepath.Join(app.localPath, "web"), false)))
}
app.loadHTML(router)
router.NoRoute(app.NoRouteHandler)
if debugMode {
app.debug.Println("Loading pprof")
pprof.Register(router)
}
for _, p := range routePrefixes {
router.GET(p+"/lang/:page", app.GetLanguages)
}
if !firstRun {
for _, p := range routePrefixes {
router.GET(p+"/", app.AdminPage)
router.GET(p+"/accounts", app.AdminPage)
router.GET(p+"/settings", app.AdminPage)
router.GET(p+"/lang/:page/:file", app.ServeLang)
router.GET(p+"/token/login", app.getTokenLogin)
router.GET(p+"/token/refresh", app.getTokenRefresh)
router.POST(p+"/newUser", app.NewUser)
router.Use(static.Serve(p+"/invite/", static.LocalFile(filepath.Join(app.localPath, "web"), false)))
router.GET(p+"/invite/:invCode", app.InviteProxy)
}
if *SWAGGER {
app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
for _, p := range routePrefixes {
router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
}
api := router.Group("/", app.webAuth())
for _, p := range routePrefixes {
router.POST(p+"/logout", app.Logout)
api.DELETE(p+"/users", app.DeleteUser)
api.GET(p+"/users", app.GetUsers)
api.POST(p+"/users", app.NewUserAdmin)
api.POST(p+"/invites", app.GenerateInvite)
api.GET(p+"/invites", app.GetInvites)
api.DELETE(p+"/invites", app.DeleteInvite)
api.POST(p+"/invites/profile", app.SetProfile)
api.GET(p+"/profiles", app.GetProfiles)
api.POST(p+"/profiles/default", app.SetDefaultProfile)
api.POST(p+"/profiles", app.CreateProfile)
api.DELETE(p+"/profiles", app.DeleteProfile)
api.POST(p+"/invites/notify", app.SetNotify)
api.POST(p+"/users/emails", app.ModifyEmails)
// api.POST(p + "/setDefaults", app.SetDefaults)
api.POST(p+"/users/settings", app.ApplySettings)
api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart)
if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET(p+"/ombi/users", app.OmbiUsers)
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)
}
}
app.info.Printf("Starting router @ %s", address)
app.loadRoutes(router)
} else {
router.GET("/", app.ServeSetup)
router.POST("/jellyfin/test", app.TestJF)
router.POST("/config", app.ModifyConfig)
app.loadSetup(router)
app.info.Printf("Loading setup @ %s", address)
}
SRV = &http.Server{
Addr: address,
Handler: router,
}
go func() {
if app.config.Section("advanced").Key("tls").MustBool(false) {
cert := app.config.Section("advanced").Key("tls_cert").MustString("")
@ -676,9 +556,9 @@ func start(asDaemon, firstCall bool) {
}
}()
for range RESTART {
cntx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
if err := SRV.Shutdown(cntx); err != nil {
if err := SRV.Shutdown(ctx); err != nil {
app.err.Fatalf("Server shutdown error: %s", err)
}
return
@ -757,6 +637,7 @@ func main() {
if flagPassed("test") {
TEST = true
}
loadFilesystems()
if flagPassed("start") {
args := []string{}
for i, f := range os.Args {

View File

@ -2,7 +2,6 @@ package main
import (
"encoding/json"
"io/ioutil"
"os"
"strings"
"time"
@ -53,7 +52,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
}
if event.Op&fsnotify.Write == fsnotify.Write && strings.Contains(event.Name, "passwordreset") {
var pwr PasswordReset
data, err := ioutil.ReadFile(event.Name)
data, err := os.ReadFile(event.Name)
if err != nil {
return
}

156
router.go Normal file
View File

@ -0,0 +1,156 @@
package main
import (
"fmt"
"html/template"
"io/fs"
"net/http"
"os"
"path/filepath"
"github.com/gin-contrib/pprof"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"github.com/logrusorgru/aurora/v3"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
// loads HTML templates. If [files]/html_templates is set, alternative files inside the directory are loaded in place of the internal templates.
func (app *appContext) loadHTML(router *gin.Engine) {
customPath := app.config.Section("files").Key("html_templates").MustString("")
templatePath := "html"
htmlFiles, err := fs.ReadDir(localFS, templatePath)
if err != nil {
app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath)
return
}
loadFiles := make([]string, len(htmlFiles))
for i, f := range htmlFiles {
if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) {
app.debug.Printf("Using default \"%s\"", f.Name())
loadFiles[i] = FSJoin(templatePath, f.Name())
} else {
app.info.Printf("Using custom \"%s\"", f.Name())
loadFiles[i] = filepath.Join(filepath.Join(customPath, f.Name()))
}
}
tmpl, err := template.ParseFS(localFS, loadFiles...)
if err != nil {
app.err.Fatalf("Failed to load templates: %v", err)
}
router.SetHTMLTemplate(tmpl)
}
// sets gin logger.
func setGinLogger(router *gin.Engine, debugMode bool) {
if debugMode {
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("[GIN/DEBUG] %s: %s(%s) => %d in %s; %s\n",
param.TimeStamp.Format("15:04:05"),
param.Method,
param.Path,
param.StatusCode,
param.Latency,
func() string {
if param.ErrorMessage != "" {
return "Error: " + param.ErrorMessage
}
return ""
}(),
)
}))
} else {
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("[GIN] %s(%s) => %d\n",
param.Method,
param.Path,
param.StatusCode,
)
}))
}
}
func (app *appContext) loadRouter(address string, debug bool) *gin.Engine {
if debug {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
setGinLogger(router, debug)
router.Use(gin.Recovery())
app.loadHTML(router)
router.Use(static.Serve("/", app.webFS))
router.NoRoute(app.NoRouteHandler)
if debug {
app.debug.Println("Loading pprof")
pprof.Register(router)
}
SRV = &http.Server{
Addr: address,
Handler: router,
}
return router
}
func (app *appContext) loadRoutes(router *gin.Engine) {
routePrefixes := []string{app.URLBase}
if app.URLBase != "" {
routePrefixes = append(routePrefixes, "")
}
for _, p := range routePrefixes {
router.GET(p+"/lang/:page", app.GetLanguages)
router.Use(static.Serve(p+"/", app.webFS))
router.GET(p+"/", app.AdminPage)
router.GET(p+"/accounts", app.AdminPage)
router.GET(p+"/settings", app.AdminPage)
router.GET(p+"/lang/:page/:file", app.ServeLang)
router.GET(p+"/token/login", app.getTokenLogin)
router.GET(p+"/token/refresh", app.getTokenRefresh)
router.POST(p+"/newUser", app.NewUser)
router.Use(static.Serve(p+"/invite/", app.webFS))
router.GET(p+"/invite/:invCode", app.InviteProxy)
}
if *SWAGGER {
app.info.Print(aurora.Magenta("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
for _, p := range routePrefixes {
router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
}
api := router.Group("/", app.webAuth())
for _, p := range routePrefixes {
router.POST(p+"/logout", app.Logout)
api.DELETE(p+"/users", app.DeleteUser)
api.GET(p+"/users", app.GetUsers)
api.POST(p+"/users", app.NewUserAdmin)
api.POST(p+"/invites", app.GenerateInvite)
api.GET(p+"/invites", app.GetInvites)
api.DELETE(p+"/invites", app.DeleteInvite)
api.POST(p+"/invites/profile", app.SetProfile)
api.GET(p+"/profiles", app.GetProfiles)
api.POST(p+"/profiles/default", app.SetDefaultProfile)
api.POST(p+"/profiles", app.CreateProfile)
api.DELETE(p+"/profiles", app.DeleteProfile)
api.POST(p+"/invites/notify", app.SetNotify)
api.POST(p+"/users/emails", app.ModifyEmails)
// api.POST(p + "/setDefaults", app.SetDefaults)
api.POST(p+"/users/settings", app.ApplySettings)
api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart)
if app.config.Section("ombi").Key("enabled").MustBool(false) {
api.GET(p+"/ombi/users", app.OmbiUsers)
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)
}
}
}
func (app *appContext) loadSetup(router *gin.Engine) {
router.GET("/lang/:page", app.GetLanguages)
router.GET("/", app.ServeSetup)
router.POST("/jellyfin/test", app.TestJF)
router.POST("/config", app.ModifyConfig)
}

View File

@ -16,7 +16,7 @@ def runcmd(cmd):
return proc.communicate()
local_path = Path(__file__).resolve().parent
local_path = Path("mail")
for mjml in [f for f in local_path.iterdir() if f.is_file() and "mjml" in f.suffix]:
print(f"Compiling {mjml.name}")

22
scripts/embed.py Executable file
View File

@ -0,0 +1,22 @@
#!/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.")

View File

@ -1,6 +1,6 @@
import subprocess
import sys
import os
try:
version = sys.argv[1].replace('v', '')
except IndexError:

View File

@ -2,7 +2,7 @@ package main
import (
"encoding/json"
"io/ioutil"
"io/fs"
"path/filepath"
"strings"
@ -70,13 +70,14 @@ func (app *appContext) TestJF(gc *gin.Context) {
gc.JSON(200, map[string]bool{"success": true})
}
func (st *Storage) loadLangSetup() error {
// The first filesystem passed should be the localFS, to ensure the local lang files are loaded first.
func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
st.lang.Setup = map[string]setupLang{}
var english setupLang
load := func(fname string) error {
load := func(filesystem fs.FS, fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := setupLang{}
f, err := ioutil.ReadFile(filepath.Join(st.lang.SetupPath, fname))
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.SetupPath, fname))
if err != nil {
return err
}
@ -107,22 +108,35 @@ func (st *Storage) loadLangSetup() error {
st.lang.Setup[index] = lang
return nil
}
err := load("en-us.json")
if err != nil {
engFound := false
var err error
for _, filesystem := range filesystems {
err = load(filesystem, "en-us.json")
if err == nil {
engFound = true
}
}
if !engFound {
return err
}
english = st.lang.Setup["en-us"]
files, err := ioutil.ReadDir(st.lang.SetupPath)
if err != nil {
return err
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(f.Name())
if err != nil {
return err
setupLoaded := false
for _, filesystem := range filesystems {
files, err := fs.ReadDir(filesystem, st.lang.SetupPath)
if err != nil {
return err
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(filesystem, f.Name())
if err == nil {
setupLoaded = true
}
}
}
}
if !setupLoaded {
return err
}
return nil
}

32
static.go Normal file
View File

@ -0,0 +1,32 @@
package main
import (
"io/fs"
"net/http"
"strings"
)
// Since the gin-static middleware uses a version of http.Filesystem with an extra Exists() func, we extend it here.
type httpFS struct {
hfs http.FileSystem // Created by converting fs.FS using http.FS()
fs fs.FS
}
func (f httpFS) Open(name string) (http.File, error) {
return f.hfs.Open("web" + name)
}
func (f httpFS) Exists(prefix string, filepath string) bool {
if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
stats, err := fs.Stat(f.fs, "web/"+p)
if err != nil {
return false
}
if stats.IsDir() {
return false
}
return true
}
return false
}

View File

@ -2,8 +2,9 @@ package main
import (
"encoding/json"
"io/ioutil"
"io/fs"
"log"
"os"
"path/filepath"
"strconv"
"strings"
@ -42,7 +43,7 @@ type Invite struct {
Notify map[string]map[string]bool `json:"notify"`
Profile string `json:"profile"`
Label string `json:"label,omitempty"`
Keys []string `json"keys,omitempty"`
Keys []string `json"keys,omitempty"`
}
type Lang struct {
@ -62,20 +63,20 @@ type Lang struct {
Setup setupLangs
}
func (st *Storage) loadLang() (err error) {
err = st.loadLangCommon()
func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
err = st.loadLangCommon(filesystems...)
if err != nil {
return
}
err = st.loadLangAdmin()
err = st.loadLangAdmin(filesystems...)
if err != nil {
return
}
err = st.loadLangForm()
err = st.loadLangForm(filesystems...)
if err != nil {
return
}
err = st.loadLangEmail()
err = st.loadLangEmail(filesystems...)
return
}
@ -120,13 +121,13 @@ func patchQuantityStrings(english, other *map[string]quantityString) {
}
}
func (st *Storage) loadLangCommon() error {
func (st *Storage) loadLangCommon(filesystems ...fs.FS) error {
st.lang.Common = map[string]commonLang{}
var english commonLang
load := func(fname string) error {
load := func(filesystem fs.FS, fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := commonLang{}
f, err := ioutil.ReadFile(filepath.Join(st.lang.CommonPath, fname))
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.CommonPath, fname))
if err != nil {
return err
}
@ -143,33 +144,46 @@ func (st *Storage) loadLangCommon() error {
st.lang.Common[index] = lang
return nil
}
err := load("en-us.json")
if err != nil {
engFound := false
var err error
for _, filesystem := range filesystems {
err = load(filesystem, "en-us.json")
if err == nil {
engFound = true
}
}
if !engFound {
return err
}
english = st.lang.Common["en-us"]
files, err := ioutil.ReadDir(st.lang.CommonPath)
if err != nil {
return err
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(f.Name())
if err != nil {
return err
commonLoaded := false
for _, filesystem := range filesystems {
files, err := fs.ReadDir(filesystem, st.lang.CommonPath)
if err != nil {
continue
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(filesystem, f.Name())
if err == nil {
commonLoaded = true
}
}
}
}
if !commonLoaded {
return err
}
return nil
}
func (st *Storage) loadLangAdmin() error {
func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
st.lang.Admin = map[string]adminLang{}
var english adminLang
load := func(fname string) error {
load := func(filesystem fs.FS, fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := adminLang{}
f, err := ioutil.ReadFile(filepath.Join(st.lang.AdminPath, fname))
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.AdminPath, fname))
if err != nil {
return err
}
@ -194,33 +208,46 @@ func (st *Storage) loadLangAdmin() error {
st.lang.Admin[index] = lang
return nil
}
err := load("en-us.json")
if err != nil {
engFound := false
var err error
for _, filesystem := range filesystems {
err = load(filesystem, "en-us.json")
if err == nil {
engFound = true
}
}
if !engFound {
return err
}
english = st.lang.Admin["en-us"]
files, err := ioutil.ReadDir(st.lang.AdminPath)
if err != nil {
return err
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(f.Name())
if err != nil {
return err
adminLoaded := false
for _, filesystem := range filesystems {
files, err := fs.ReadDir(filesystem, st.lang.AdminPath)
if err != nil {
continue
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(filesystem, f.Name())
if err == nil {
adminLoaded = true
}
}
}
}
if !adminLoaded {
return err
}
return nil
}
func (st *Storage) loadLangForm() error {
func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
st.lang.Form = map[string]formLang{}
var english formLang
load := func(fname string) error {
load := func(filesystem fs.FS, fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := formLang{}
f, err := ioutil.ReadFile(filepath.Join(st.lang.FormPath, fname))
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.FormPath, fname))
if err != nil {
return err
}
@ -250,33 +277,46 @@ func (st *Storage) loadLangForm() error {
st.lang.Form[index] = lang
return nil
}
err := load("en-us.json")
if err != nil {
engFound := false
var err error
for _, filesystem := range filesystems {
err = load(filesystem, "en-us.json")
if err == nil {
engFound = true
}
}
if !engFound {
return err
}
english = st.lang.Form["en-us"]
files, err := ioutil.ReadDir(st.lang.FormPath)
if err != nil {
return err
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(f.Name())
if err != nil {
return err
formLoaded := false
for _, filesystem := range filesystems {
files, err := fs.ReadDir(filesystem, st.lang.FormPath)
if err != nil {
continue
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(filesystem, f.Name())
if err == nil {
formLoaded = true
}
}
}
}
if !formLoaded {
return err
}
return nil
}
func (st *Storage) loadLangEmail() error {
func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
st.lang.Email = map[string]emailLang{}
var english emailLang
load := func(fname string) error {
load := func(filesystem fs.FS, fname string) error {
index := strings.TrimSuffix(fname, filepath.Ext(fname))
lang := emailLang{}
f, err := ioutil.ReadFile(filepath.Join(st.lang.EmailPath, fname))
f, err := fs.ReadFile(filesystem, FSJoin(st.lang.EmailPath, fname))
if err != nil {
return err
}
@ -299,23 +339,36 @@ func (st *Storage) loadLangEmail() error {
st.lang.Email[index] = lang
return nil
}
err := load("en-us.json")
if err != nil {
engFound := false
var err error
for _, filesystem := range filesystems {
err = load(filesystem, "en-us.json")
if err == nil {
engFound = true
}
}
if !engFound {
return err
}
english = st.lang.Email["en-us"]
files, err := ioutil.ReadDir(st.lang.EmailPath)
if err != nil {
return err
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(f.Name())
if err != nil {
return err
emailLoaded := false
for _, filesystem := range filesystems {
files, err := fs.ReadDir(filesystem, st.lang.EmailPath)
if err != nil {
continue
}
for _, f := range files {
if f.Name() != "en-us.json" {
err = load(filesystem, f.Name())
if err == nil {
emailLoaded = true
}
}
}
}
if !emailLoaded {
return err
}
return nil
}
@ -329,76 +382,6 @@ func (st *Storage) storeInvites() error {
return storeJSON(st.invite_path, st.invites)
}
// func (st *Storage) loadLang() error {
// loadData := func(path string, stringJson bool) (map[string]string, map[string]map[string]interface{}, error) {
// files, err := ioutil.ReadDir(path)
// outString := map[string]string{}
// out := map[string]map[string]interface{}{}
// if err != nil {
// return nil, nil, err
// }
// for _, f := range files {
// index := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))
// var data map[string]interface{}
// var file []byte
// var err error
// file, err = ioutil.ReadFile(filepath.Join(path, f.Name()))
// if err != nil {
// file = []byte("{}")
// }
// // Replace Jellyfin with something if necessary
// if substituteStrings != "" {
// fileString := strings.ReplaceAll(string(file), "Jellyfin", substituteStrings)
// file = []byte(fileString)
// }
// err = json.Unmarshal(file, &data)
// if err != nil {
// log.Printf("ERROR: Failed to read \"%s\": %s", path, err)
// return nil, nil, err
// }
// if stringJson {
// stringJSON, err := json.Marshal(data)
// if err != nil {
// return nil, nil, err
// }
// outString[index] = string(stringJSON)
// }
// out[index] = data
//
// }
// return outString, out, nil
// }
// _, form, err := loadData(st.lang.FormPath, false)
// if err != nil {
// return err
// }
// for index, lang := range form {
// validationStrings := lang["validationStrings"].(map[string]interface{})
// vS, err := json.Marshal(validationStrings)
// if err != nil {
// return err
// }
// lang["validationStrings"] = string(vS)
// form[index] = lang
// }
// st.lang.Form = form
// adminJSON, admin, err := loadData(st.lang.AdminPath, true)
// st.lang.Admin = admin
// st.lang.AdminJSON = adminJSON
//
// _, emails, err := loadData(st.lang.EmailPath, false)
// fixedEmails := map[string]map[string]map[string]interface{}{}
// for lang, e := range emails {
// f := map[string]map[string]interface{}{}
// for field, vals := range e {
// f[field] = vals.(map[string]interface{})
// }
// fixedEmails[lang] = f
// }
// st.lang.Email = fixedEmails
// return err
// }
func (st *Storage) loadEmails() error {
return loadJSON(st.emails_path, &st.emails)
}
@ -495,7 +478,7 @@ func (st *Storage) migrateToProfile() error {
func loadJSON(path string, obj interface{}) error {
var file []byte
var err error
file, err = ioutil.ReadFile(path)
file, err = os.ReadFile(path)
if err != nil {
file = []byte("{}")
}
@ -511,7 +494,7 @@ func storeJSON(path string, obj interface{}) error {
if err != nil {
return err
}
err = ioutil.WriteFile(path, data, 0644)
err = os.WriteFile(path, data, 0644)
if err != nil {
log.Printf("ERROR: Failed to write to \"%s\": %s", path, err)
}

View File

@ -52,14 +52,19 @@ func (app *appContext) pushResources(gc *gin.Context, admin bool) {
gc.Header("Link", cssHeader)
}
func (app *appContext) AdminPage(gc *gin.Context) {
app.pushResources(gc, true)
func (app *appContext) getLang(gc *gin.Context, chosen string) string {
lang := gc.Query("lang")
if lang == "" {
lang = app.storage.lang.chosenAdminLang
lang = chosen
} else if _, ok := app.storage.lang.Admin[lang]; !ok {
lang = app.storage.lang.chosenAdminLang
lang = chosen
}
return lang
}
func (app *appContext) AdminPage(gc *gin.Context) {
app.pushResources(gc, true)
lang := app.getLang(gc, app.storage.lang.chosenAdminLang)
emailEnabled, _ := app.config.Section("invite_emails").Key("enabled").Bool()
notificationsEnabled, _ := app.config.Section("notifications").Key("enabled").Bool()
ombiEnabled := app.config.Section("ombi").Key("enabled").MustBool(false)
@ -82,12 +87,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
func (app *appContext) InviteProxy(gc *gin.Context) {
app.pushResources(gc, false)
code := gc.Param("invCode")
lang := gc.Query("lang")
if lang == "" {
lang = app.storage.lang.chosenFormLang
} else if _, ok := app.storage.lang.Form[lang]; !ok {
lang = app.storage.lang.chosenFormLang
}
lang := app.getLang(gc, app.storage.lang.chosenFormLang)
/* Don't actually check if the invite is valid, just if it exists, just so the page loads quicker. Invite is actually checked on submit anyway. */
// if app.checkInvite(code, false, "") {
inv, ok := app.storage.invites[code]
@ -98,7 +98,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
})
return
}
if key := gc.Query("key"); key != "" {
if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
validKey := false
keyIndex := -1
for i, k := range inv.Keys {