Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0af76364a | ||
|
|
169622bf95 | ||
|
|
78b5136b9a | ||
|
|
e546f50141 | ||
|
|
e35fe8d425 | ||
|
|
6ed2f7aaa6 | ||
|
|
084e8ec432 | ||
|
|
fd7f74682b | ||
|
|
9950c158a1 | ||
|
|
21125033ff | ||
|
|
1dc0b2234a | ||
|
|
0ea5c7fdc0 | ||
|
|
b538922c05 | ||
|
|
f0f4e8118e | ||
|
|
2f501697db | ||
|
|
0a71d5b216 | ||
|
|
0014db44f0 | ||
|
|
885d2ebf0f | ||
|
|
6d089a9818 | ||
|
|
de81c7e29f | ||
|
|
e49996c401 | ||
|
|
aa40a72075 | ||
|
|
19b7341e80 | ||
|
|
73645a7569 | ||
|
|
a9dac8c04c | ||
|
|
43fbbbcd16 | ||
|
|
fc57a8c97f | ||
|
|
1fb2ef5675 | ||
|
|
e0d994c35c | ||
|
|
cab30eb628 | ||
|
|
71df011556 | ||
|
|
b2828110e3 | ||
|
|
50eb05776a | ||
|
|
19715f25f6 | ||
|
|
d41a281d53 | ||
|
|
a8229631bd | ||
|
|
0a2cf6132f | ||
|
|
d7ab01063a | ||
|
|
6fb8f1ed7f | ||
|
|
a9b11012bc | ||
|
|
e7cb1f516b | ||
|
|
555d5abf59 | ||
|
|
93937ec3b5 | ||
|
|
93c7a6e31b | ||
|
|
a9cabe3d74 | ||
|
|
d6fd1d6894 | ||
|
|
375022ba95 | ||
|
|
75fdf6ec3d | ||
|
|
561c461a18 | ||
|
|
59ebf52fe2 | ||
|
|
89fb3fa619 | ||
|
|
9bd6abadf4 | ||
|
|
953a66ec47 | ||
|
|
4e826f4167 | ||
|
|
e97b90d4d7 | ||
|
|
fb6256d1ed | ||
|
|
7035a3fe9c | ||
|
|
62c29d55cc | ||
|
|
a83dbcf3ab | ||
|
|
e48bdcc45b | ||
|
|
0b473ef01f | ||
|
|
e03525a1d1 | ||
|
|
087172c79e | ||
|
|
8fd919bf04 | ||
|
|
2ad84db482 | ||
|
|
85536ff79e | ||
|
|
8b62c91d13 | ||
|
|
e7d1693517 | ||
|
|
e78b4882b3 | ||
|
|
e01144950b | ||
|
|
86ef665b12 | ||
|
|
f419a57e6d | ||
|
|
d7e8ec95de | ||
|
|
5a9bc1c66f | ||
|
|
1f9af8df89 | ||
|
|
0676b6c41f | ||
|
|
ac842e6273 | ||
|
|
ce8cdced4d | ||
|
|
b8e3fc636c | ||
|
|
519a5615cc | ||
|
|
168b217553 | ||
|
|
7d698d63e3 | ||
|
|
035dbde819 | ||
|
|
c373d8b2d6 | ||
|
|
8698c3c6a4 | ||
|
|
0edd2ba68b | ||
|
|
b91f0b5a18 | ||
|
|
24fa841c0d | ||
|
|
44558b8109 | ||
|
|
478b40d0ff | ||
|
|
8b816dc725 | ||
|
|
81a58f628b | ||
|
|
e98c9b46f1 | ||
|
|
b3ce7acfcb | ||
|
|
9fac79b1f0 | ||
|
|
591e3c5ca1 | ||
|
|
35d407afef | ||
|
|
a6447165b7 | ||
|
|
23800bb892 | ||
|
|
b47cb91f55 | ||
|
|
2d9e3fbc1d | ||
|
|
bf67e27737 | ||
|
|
3427c97e3e | ||
|
|
81e69a7166 | ||
|
|
564098b9d8 | ||
|
|
ec659174fb | ||
|
|
1a42d8280c | ||
|
|
b14f10d79d | ||
|
|
ee8facd1bf | ||
|
|
811657b553 | ||
|
|
95936f7c29 | ||
|
|
613d4cd9af | ||
|
|
7beb3d9974 | ||
|
|
6f2bb7f0b5 | ||
|
|
315b5fda91 | ||
|
|
a6aa89e502 | ||
|
|
3bf722c5fe | ||
|
|
e931c09a34 | ||
|
|
f8f5f35cc1 | ||
|
|
524941da0c | ||
|
|
22bba922f9 | ||
|
|
d928df7ab2 | ||
|
|
4b11bbe21f | ||
|
|
18bcd55972 | ||
|
|
057f306ed9 | ||
|
|
76bbb3f147 | ||
|
|
0f3ad8bb69 | ||
|
|
1d47b9074f | ||
|
|
5167fde080 | ||
|
|
a62648ee68 | ||
|
|
5dee414596 | ||
|
|
8cf9b1f905 | ||
|
|
6bf1920160 | ||
|
|
33f8070e57 | ||
|
|
4d2a018032 | ||
|
|
ca7fb540ee | ||
|
|
beb0712ce9 | ||
|
|
a081b14794 | ||
|
|
e416acf6bd | ||
|
|
bf94f76509 | ||
|
|
ac239a309c | ||
|
|
0f12586166 | ||
|
|
b1b50ce561 | ||
|
|
8e2bf48ab4 | ||
|
|
6ec5022a0d | ||
|
|
ef97e0ac76 | ||
|
|
30736a055d | ||
|
|
d0905a29be | ||
|
|
fe5cf69b7a |
36
.drone.yml
@@ -9,23 +9,28 @@ steps:
|
||||
commands:
|
||||
- git fetch --tags
|
||||
- name: release
|
||||
image: golang:latest
|
||||
image: hrfee/jfa-go-build-docker:latest
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
path: /id_rsa
|
||||
environment:
|
||||
BUILDRONE_KEY:
|
||||
from_secret: BUILDRONE_KEY
|
||||
GITHUB_TOKEN:
|
||||
from_secret: github_token
|
||||
commands:
|
||||
- apt-get update -y
|
||||
- apt-get install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||
- apt-get install nodejs
|
||||
- curl -sL https://git.io/goreleaser > ../goreleaser
|
||||
- chmod +x ../goreleaser
|
||||
- ./scripts/version.sh ../goreleaser
|
||||
- wget https://builds.hrfee.pw/upload.py -P ../
|
||||
- pip3 install requests
|
||||
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
|
||||
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
|
||||
- bash -c 'python3 ../upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
host:
|
||||
path: /root/.ssh/id_rsa_packaging
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
@@ -71,22 +76,29 @@ type: docker
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: golang:latest
|
||||
image: hrfee/jfa-go-build-docker:latest
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
path: /id_rsa
|
||||
commands:
|
||||
- apt-get update -y
|
||||
- apt-get install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||
- apt-get install nodejs
|
||||
- curl -sL https://git.io/goreleaser > goreleaser
|
||||
- chmod +x goreleaser
|
||||
- ./scripts/version.sh ./goreleaser --snapshot --skip-publish --rm-dist
|
||||
- wget https://builds.hrfee.pw/upload.py
|
||||
- pip3 install requests
|
||||
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.tar.gz --tag internal-git=true'
|
||||
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
|
||||
# - bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "reprepro -Vb /repo remove trusty-unstable jfa-go"'
|
||||
# - bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "reprepro -Vb /repo remove trusty-unstable jfa-go-tray"'
|
||||
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
|
||||
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.zip ./dist/*.rpm ./dist/*.apk --tag internal-git=true'
|
||||
environment:
|
||||
BUILDRONE_KEY:
|
||||
from_secret: BUILDRONE_KEY
|
||||
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
host:
|
||||
path: /root/.ssh/id_rsa_packaging
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
@@ -144,7 +156,7 @@ steps:
|
||||
image: golang:latest
|
||||
commands:
|
||||
- apt-get update -y
|
||||
- apt-get install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- apt-get install build-essential python3-pip curl software-properties-common sed upx gcc libgtk-3-dev libappindicator3-dev gcc-mingw-w64-x86-64 -y
|
||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||
- apt-get install nodejs
|
||||
- curl -sL https://git.io/goreleaser > goreleaser
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -19,8 +19,11 @@ What to do to reproduce the problem.
|
||||
|
||||
**Logs**
|
||||
|
||||
**If you're using a build with a tray icon, right-click on it and press "Open logs" to access your logs.**
|
||||
|
||||
When you notice the problem, check the output of `jfa-go`. If the problem is not obvious (e.g a panic (red text) or 'ERROR' log), re-run jfa-go with the `-debug` argument and reproduce the problem. You should then take a screenshot of the output, or paste it here, preferably between \`\`\` tags (e.g \`\`\``Log here`\`\`\`). Remember to censor any personal information.
|
||||
|
||||
|
||||
If nothing catches your eye in the log, access the admin page via your browser, go into the console (Right click > Inspect Element > Console), refresh, reproduce the problem then paste the output here in the same way as above.
|
||||
|
||||
**Configuration**
|
||||
|
||||
118
.goreleaser.yml
@@ -25,24 +25,81 @@ before:
|
||||
- npx esbuild --bundle ts/pwr.ts --outfile=./data/web/js/pwr.js --minify
|
||||
- npx esbuild --bundle ts/form.ts --outfile=./data/web/js/form.js --minify
|
||||
- npx esbuild --bundle ts/setup.ts --outfile=./data/web/js/setup.js --minify
|
||||
- npx esbuild --bundle ts/crash.ts --outfile=./data/crash.js --minify
|
||||
- cp html/crash.html data/
|
||||
- npx uncss data/crash.html --csspath web/css --output data/bundle.css
|
||||
- npx inline-source --root data data/crash.html data/crash.html
|
||||
- rm data/bundle.css
|
||||
- mv data/crash.html data/html/
|
||||
- go get -u github.com/swaggo/swag/cmd/swag
|
||||
- swag init -g main.go
|
||||
builds:
|
||||
- dir: ./
|
||||
- id: notray
|
||||
dir: ./
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- amd64
|
||||
- id: windows-tray
|
||||
dir: ./
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=x86_64-w64-mingw32-gcc
|
||||
- CXX=x86_64-w64-mingw32-g++
|
||||
flags:
|
||||
- -tags=tray
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary -H=windowsgui
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- id: linux-tray
|
||||
dir: ./
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
flags:
|
||||
- -tags=tray
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
archives:
|
||||
- replacements:
|
||||
- id: windows-tray
|
||||
builds:
|
||||
- windows-tray
|
||||
format: zip
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_TrayIcon_{{ .Os }}_{{ .Arch }}"
|
||||
replacements:
|
||||
darwin: macOS
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
amd64: x86_64
|
||||
- id: linux-tray
|
||||
builds:
|
||||
- linux-tray
|
||||
format: zip
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_TrayIcon_{{ .Os }}_{{ .Arch }}"
|
||||
replacements:
|
||||
darwin: macOS
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
amd64: x86_64
|
||||
- id: notray
|
||||
builds:
|
||||
- notray
|
||||
format: zip
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||
replacements:
|
||||
darwin: macOS
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
@@ -50,10 +107,61 @@ archives:
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "git-{{.ShortCommit}}"
|
||||
name_template: "0.0.0-{{ .Env.JFA_GO_NFPM_EPOCH }}"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
nfpms:
|
||||
- id: notray
|
||||
file_name_template: '{{ .ProjectName }}{{ if .IsSnapshot }}-git{{ end }}_{{ .Arch }}_{{ if .IsSnapshot }}{{ .ShortCommit }}{{ else }}v{{ .Version }}{{ end }}'
|
||||
package_name: jfa-go
|
||||
homepage: https://github.com/hrfee/jfa-go
|
||||
description: A web app for managing users on Jellyfin
|
||||
maintainer: Harvey Tindall <hrfee@hrfee.dev>
|
||||
license: MIT
|
||||
vendor: hrfee.dev
|
||||
version_metadata: git
|
||||
builds:
|
||||
- notray
|
||||
contents:
|
||||
- src: ./LICENSE
|
||||
dst: /usr/share/licenses/jfa-go
|
||||
formats:
|
||||
- apk
|
||||
- deb
|
||||
- rpm
|
||||
- id: tray
|
||||
file_name_template: '{{ .ProjectName }}{{ if .IsSnapshot }}-git{{ end }}_TrayIcon_{{ .Arch }}_{{ if .IsSnapshot }}{{ .ShortCommit }}{{ else }}v{{ .Version }}{{ end }}'
|
||||
package_name: jfa-go-tray
|
||||
homepage: https://github.com/hrfee/jfa-go
|
||||
description: A web app for managing users on Jellyfin
|
||||
maintainer: Harvey Tindall <hrfee@hrfee.dev>
|
||||
license: MIT
|
||||
vendor: hrfee.dev
|
||||
version_metadata: git
|
||||
builds:
|
||||
- linux-tray
|
||||
contents:
|
||||
- src: ./LICENSE
|
||||
dst: /usr/share/licenses/jfa-go
|
||||
formats:
|
||||
- apk
|
||||
- deb
|
||||
- rpm
|
||||
overrides:
|
||||
deb:
|
||||
conflicts:
|
||||
- jfa-go
|
||||
replaces:
|
||||
- jfa-go
|
||||
dependencies:
|
||||
- libappindicator3-1
|
||||
rpm:
|
||||
dependencies:
|
||||
- libappindicator-gtk3
|
||||
apk:
|
||||
dependencies:
|
||||
- libappindicator
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#### Code
|
||||
I use 4 spaces for indentation. Go should ideally be formatted with `goimports` and/or `gofmt`. I don't use a formatter on typescript, so don't worry about that.
|
||||
|
||||
Code in Go should ideally use `PascalCase` for exported values, and `camelCase` for non-exported, JSON for transferring data should use `snake_case`, and Typescript should use `camelCase`. Forgive me for my many inconsistencies in this, and feel free to fix them if you want.
|
||||
|
||||
Functions in Go that need to access `*appContext` should be generally be receivers, except when the behaviour could be seen as somewhat independent from it (`email.go` is the best example, its behaviour is broadly independent from the main app except from a couple config values).
|
||||
|
||||
|
||||
#### Compiling
|
||||
|
||||
Prefix each of these with `make DEBUG=on INTERNAL=off `:
|
||||
|
||||
35
Makefile
@@ -27,17 +27,38 @@ else
|
||||
TAGS := -tags external
|
||||
endif
|
||||
|
||||
TRAY ?= off
|
||||
ifeq ($(INTERNAL)$(TRAY), offon)
|
||||
TAGS := $(TAGS) tray
|
||||
else ifeq ($(INTERNAL)$(TRAY), onon)
|
||||
TAGS := -tags tray
|
||||
endif
|
||||
|
||||
OS := $(shell go env GOOS)
|
||||
ifeq ($(TRAY)$(OS), onwindows)
|
||||
LDFLAGS := $(LDFLAGS) -H=windowsgui
|
||||
endif
|
||||
|
||||
DEBUG ?= off
|
||||
ifeq ($(DEBUG), on)
|
||||
LDFLAGS := -s -w $(LDFLAGS)
|
||||
SOURCEMAP := --sourcemap
|
||||
TYPECHECK := tsc -noEmit --project ts/tsconfig.json
|
||||
# jank
|
||||
COPYTS := rm -r $(DATA)/web/js/ts; cp -r ts $(DATA)/web/js
|
||||
UNCSS := cp $(DATA)/web/css/bundle.css $(DATA)/bundle.css
|
||||
else
|
||||
LDFLAGS := -s -w $(LDFLAGS)
|
||||
SOURCEMAP :=
|
||||
COPYTS :=
|
||||
TYPECHECK :=
|
||||
UNCSS := npx uncss $(DATA)/crash.html --csspath web/css --output $(DATA)/bundle.css
|
||||
endif
|
||||
|
||||
RACE ?= off
|
||||
ifeq ($(RACE), on)
|
||||
RACEDETECTOR := -race
|
||||
else
|
||||
RACEDETECTOR :=
|
||||
endif
|
||||
|
||||
npm:
|
||||
@@ -68,6 +89,7 @@ typescript:
|
||||
-$(ESBUILD) --bundle ts/pwr.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr.js --minify
|
||||
-$(ESBUILD) --bundle ts/form.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/form.js --minify
|
||||
-$(ESBUILD) --bundle ts/setup.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/setup.js --minify
|
||||
-$(ESBUILD) --bundle ts/crash.ts --outfile=./$(DATA)/crash.js --minify
|
||||
$(COPYTS)
|
||||
|
||||
swagger:
|
||||
@@ -79,7 +101,7 @@ compile:
|
||||
$(GOBINARY) mod download
|
||||
$(info Building)
|
||||
mkdir -p build
|
||||
CGO_ENABLED=0 $(GOBINARY) build -ldflags="-s -w $(LDFLAGS)" $(TAGS) -o build/jfa-go
|
||||
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o build/jfa-go
|
||||
|
||||
compress:
|
||||
upx --lzma build/jfa-go
|
||||
@@ -89,11 +111,18 @@ bundle-css:
|
||||
$(info bundling css)
|
||||
$(ESBUILD) --bundle css/base.css --outfile=$(DATA)/web/css/bundle.css --external:remixicon.css --minify
|
||||
|
||||
inline:
|
||||
cp html/crash.html $(DATA)/crash.html
|
||||
$(UNCSS)
|
||||
npx inline-source --root $(DATA) $(DATA)/crash.html $(DATA)/crash.html
|
||||
rm $(DATA)/bundle.css
|
||||
|
||||
copy:
|
||||
$(info copying fonts)
|
||||
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
|
||||
$(info copying html)
|
||||
cp -r html $(DATA)/
|
||||
mv $(DATA)/crash.html $(DATA)/html/
|
||||
$(info copying static data)
|
||||
-mkdir -p $(DATA)/web
|
||||
cp -r static/* $(DATA)/web/
|
||||
@@ -122,4 +151,4 @@ clean:
|
||||
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
|
||||
go clean
|
||||
|
||||
all: configuration npm email typescript bundle-css swagger copy compile
|
||||
all: configuration npm email typescript bundle-css inline swagger copy compile
|
||||
|
||||
74
README.md
@@ -4,12 +4,15 @@
|
||||
[](https://weblate.hrfee.pw/engage/jfa-go/)
|
||||
|
||||
##### Downloads:
|
||||
##### [dockerhub](https://hub.docker.com/r/hrfee/jfa-go) | [stable](https://github.com/hrfee/jfa-go/releases) | [nightly](https://builds.hrfee.pw/view/hrfee/jfa-go) | [aur stable](https://aur.archlinux.org/packages/jfa-go) | [aur binary](https://aur.archlinux.org/packages/jfa-go-bin) | [aur nightly](https://aur.archlinux.org/packages/jfa-go-git)
|
||||
##### [docker](#docker) | [debian/ubuntu](#debian) | [arch (aur)](#aur) | [other platforms](#other-platforms)
|
||||
---
|
||||
jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) (and now [Emby](https://emby.media/)) that provides invite-based account creation as well as other features that make one's instance much easier to manage.
|
||||
|
||||
a rewrite of [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) (original naming for both, ik
|
||||
😂).
|
||||
|
||||
#### Features
|
||||
* 🧑 Invite based account creation: Sends invites to your friends or family, and let them choose their own username and password without relying on you.
|
||||
* 🧑 Invite based account creation: Send invites to your friends or family, and let them choose their own username and password without relying on you.
|
||||
* Send invites via a link and/or email
|
||||
* Granular control over invites: Validity period as well as number of uses can be specified.
|
||||
* Account profiles: Assign settings profiles to invites so new users have your predefined permissions, homescreen layout, etc. applied to their account on creation.
|
||||
@@ -17,7 +20,7 @@ jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jelly
|
||||
* ⌛ User expiry: Specify a validity period, and new users accounts will be disabled/deleted after it. The period can be manually extended too.
|
||||
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
|
||||
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
|
||||
* Telegram Integration: Verify users via telegram, and send Password Resets, Announcements, etc. through it.
|
||||
* Telegram/Discord/Matrix Integration: Verify users via a chat bot, and send Password Resets, Announcements, etc. through it.
|
||||
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
||||
* Email addresses can optionally be used instead of usernames
|
||||
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email/telegram.
|
||||
@@ -43,7 +46,9 @@ jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jelly
|
||||
|
||||
#### Install
|
||||
|
||||
The [Docker](https://hub.docker.com/r/hrfee/jfa-go) image is your best bet.
|
||||
**Note**: `TrayIcon` builds include a tray icon to start/stop/restart, and an option to automatically start when you log-in to your computer. For Linux users, these builds depend on the `libappindicator3-1`/`libappindicator-gtk3`/`libappindicator` package for Debian/Ubuntu, Fedora, and Alpine respectively.
|
||||
|
||||
##### [Docker](https://hub.docker.com/r/hrfee/jfa-go)
|
||||
```sh
|
||||
docker create \
|
||||
--name "jfa-go" \ # Whatever you want to name it
|
||||
@@ -54,9 +59,41 @@ docker create \
|
||||
-v /etc/localtime:/etc/localtime:ro \ # Makes sure time is correct
|
||||
hrfee/jfa-go # hrfee/jfa-go:unstable for latest build from git
|
||||
```
|
||||
Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/), [jfa-go-bin](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.dev/view/hrfee/jfa-go)), and extract the `jfa-go` executable to somewhere useful.
|
||||
##### [Debian/Ubuntu](https://apt.hrfee.dev)
|
||||
```sh
|
||||
sudo apt-get update && sudo apt-get install curl apt-transport-https gnupg
|
||||
curl https://apt.hrfee.dev/hrfee.pubkey.gpg | sudo apt-key add -
|
||||
|
||||
# For stable releases
|
||||
echo "deb https://apt.hrfee.dev trusty main" | sudo tee /etc/apt/sources.list.d/hrfee.list
|
||||
# ------
|
||||
# For unstable releases
|
||||
echo "deb https://apt.hrfee.dev trusty-unstable main" | sudo tee /etc/apt/sources.list.d/hrfee.list
|
||||
# ------
|
||||
|
||||
sudo apt-get update
|
||||
|
||||
# For servers
|
||||
sudo apt-get install jfa-go
|
||||
# ------
|
||||
# For desktops/servers with GUI (has dependencies)
|
||||
sudo apt-get install jfa-go-tray
|
||||
# ------
|
||||
```
|
||||
|
||||
##### Arch
|
||||
Available on the AUR as:
|
||||
* [jfa-go](https://aur.archlinux.org/packages/jfa-go/) (stable)
|
||||
* [jfa-go-bin](https://aur.archlinux.org/packages/jfa-go) (pre-compiled, stable)
|
||||
* [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/) (nightly)
|
||||
|
||||
##### Other platforms
|
||||
Download precompiled binaries from:
|
||||
* [The releases section](https://github.com/hrfee/jfa-go/releases) (stable)
|
||||
* [Buildrone](https://builds.hrfee.dev/view/hrfee/jfa-go) (nightly)
|
||||
|
||||
unzip the `jfa-go`/`jfa-go.exe` 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.
|
||||
@@ -71,17 +108,28 @@ Otherwise, full build instructions can be found [here](https://github.com/hrfee/
|
||||
Simply run `jfa-go` to start the application. A setup wizard will start on `localhost:8056` (or your own specified address). Upon completion, refresh the page.
|
||||
|
||||
```
|
||||
Usage of ./jfa-go:
|
||||
-config string
|
||||
alternate path to config file. (default "~/.config/jfa-go/config.ini")
|
||||
-data string
|
||||
alternate path to data directory. (default "~/.config/jfa-go")
|
||||
Usage of jfa-go:
|
||||
start
|
||||
start jfa-go as a daemon and run in the background.
|
||||
stop
|
||||
stop a daemonized instance of jfa-go.
|
||||
systemd
|
||||
generate a systemd .service file.
|
||||
|
||||
-config, -c string
|
||||
alternate path to config file. (default "/home/hrfee/.config/jfa-go/config.ini")
|
||||
-data, -d string
|
||||
alternate path to data directory. (default "/home/hrfee/.config/jfa-go")
|
||||
-debug
|
||||
Enables debug logging and exposes pprof.
|
||||
Enables debug logging.
|
||||
-help, -h
|
||||
prints this message.
|
||||
-host string
|
||||
alternate address to host web ui on.
|
||||
-port int
|
||||
-port, -p int
|
||||
alternate port to host web ui on.
|
||||
-pprof
|
||||
Exposes pprof profiler on /debug/pprof.
|
||||
-swagger
|
||||
Enable swagger at /swagger/index.html
|
||||
```
|
||||
|
||||
578
api.go
@@ -217,7 +217,7 @@ func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, er
|
||||
username := jfUser.Name
|
||||
email := ""
|
||||
if e, ok := app.storage.emails[jfID]; ok {
|
||||
email = e.(string)
|
||||
email = e.Addr
|
||||
}
|
||||
for _, ombiUser := range ombiUsers {
|
||||
ombiAddr := ""
|
||||
@@ -283,7 +283,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
|
||||
}
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
if emailEnabled {
|
||||
app.storage.emails[id] = req.Email
|
||||
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
|
||||
app.storage.storeEmails()
|
||||
}
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
@@ -330,6 +330,58 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
success = false
|
||||
return
|
||||
}
|
||||
var discordUser DiscordUser
|
||||
discordVerified := false
|
||||
if discordEnabled {
|
||||
if req.DiscordPIN == "" {
|
||||
if app.config.Section("discord").Key("required").MustBool(false) {
|
||||
f = func(gc *gin.Context) {
|
||||
app.debug.Printf("%s: New user failed: Discord verification not completed", req.Code)
|
||||
respond(401, "errorDiscordVerification", gc)
|
||||
}
|
||||
success = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
discordUser, discordVerified = app.discord.verifiedTokens[req.DiscordPIN]
|
||||
if !discordVerified {
|
||||
f = func(gc *gin.Context) {
|
||||
app.debug.Printf("%s: New user failed: Discord PIN was invalid", req.Code)
|
||||
respond(401, "errorInvalidPIN", gc)
|
||||
}
|
||||
success = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
var matrixUser MatrixUser
|
||||
matrixVerified := false
|
||||
if matrixEnabled {
|
||||
if req.MatrixPIN == "" {
|
||||
if app.config.Section("matrix").Key("required").MustBool(false) {
|
||||
f = func(gc *gin.Context) {
|
||||
app.debug.Printf("%s: New user failed: Matrix verification not completed", req.Code)
|
||||
respond(401, "errorMatrixVerification", gc)
|
||||
}
|
||||
success = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
user, ok := app.matrix.tokens[req.MatrixPIN]
|
||||
if !ok || !user.Verified {
|
||||
matrixVerified = false
|
||||
f = func(gc *gin.Context) {
|
||||
app.debug.Printf("%s: New user failed: Matrix PIN was invalid", req.Code)
|
||||
respond(401, "errorInvalidPIN", gc)
|
||||
}
|
||||
success = false
|
||||
return
|
||||
}
|
||||
matrixVerified = user.Verified
|
||||
matrixUser = *user.User
|
||||
|
||||
}
|
||||
}
|
||||
telegramTokenIndex := -1
|
||||
if telegramEnabled {
|
||||
if req.TelegramPIN == "" {
|
||||
@@ -454,7 +506,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
}
|
||||
// if app.config.Section("password_resets").Key("enabled").MustBool(false) {
|
||||
if req.Email != "" {
|
||||
app.storage.emails[id] = req.Email
|
||||
app.storage.emails[id] = EmailAddress{Addr: req.Email, Contact: true}
|
||||
app.storage.storeEmails()
|
||||
}
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
@@ -479,7 +531,18 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
app.err.Printf("Failed to store user duration: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if discordEnabled && discordVerified {
|
||||
discordUser.Contact = req.DiscordContact
|
||||
if app.storage.discord == nil {
|
||||
app.storage.discord = map[string]DiscordUser{}
|
||||
}
|
||||
app.storage.discord[user.ID] = discordUser
|
||||
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||
app.err.Printf("Failed to store Discord users: %v", err)
|
||||
} else {
|
||||
delete(app.discord.verifiedTokens, req.DiscordPIN)
|
||||
}
|
||||
}
|
||||
if telegramEnabled && telegramTokenIndex != -1 {
|
||||
tgToken := app.telegram.verifiedTokens[telegramTokenIndex]
|
||||
tgUser := TelegramUser{
|
||||
@@ -494,16 +557,25 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
app.storage.telegram = map[string]TelegramUser{}
|
||||
}
|
||||
app.storage.telegram[user.ID] = tgUser
|
||||
err := app.storage.storeTelegramUsers()
|
||||
if err != nil {
|
||||
if err := app.storage.storeTelegramUsers(); err != nil {
|
||||
app.err.Printf("Failed to store Telegram users: %v", err)
|
||||
} else {
|
||||
app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1], app.telegram.verifiedTokens[telegramTokenIndex] = app.telegram.verifiedTokens[telegramTokenIndex], app.telegram.verifiedTokens[len(app.telegram.verifiedTokens)-1]
|
||||
app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
|
||||
}
|
||||
}
|
||||
|
||||
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 {
|
||||
if matrixVerified {
|
||||
matrixUser.Contact = req.MatrixContact
|
||||
delete(app.matrix.tokens, req.MatrixPIN)
|
||||
if app.storage.matrix == nil {
|
||||
app.storage.matrix = map[string]MatrixUser{}
|
||||
}
|
||||
app.storage.matrix[user.ID] = matrixUser
|
||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||
app.err.Printf("Failed to store Matrix users: %v", err)
|
||||
}
|
||||
}
|
||||
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 || discordVerified {
|
||||
name := app.getAddressOrName(user.ID)
|
||||
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
|
||||
msg, err := app.email.constructWelcome(req.Username, expiry, app, false)
|
||||
@@ -792,18 +864,44 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
invite.UserMinutes = req.UserMinutes
|
||||
}
|
||||
invite.ValidTill = validTill
|
||||
if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||
app.debug.Printf("%s: Sending invite email", inviteCode)
|
||||
invite.Email = req.Email
|
||||
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
|
||||
if err != nil {
|
||||
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
||||
app.err.Printf("%s: Failed to construct invite email: %v", inviteCode, err)
|
||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
||||
app.err.Printf("%s: %s: %v", inviteCode, invite.Email, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.Email)
|
||||
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||
addressValid := false
|
||||
discord := ""
|
||||
app.debug.Printf("%s: Sending invite message", inviteCode)
|
||||
if discordEnabled && !strings.Contains(req.SendTo, "@") {
|
||||
users := app.discord.GetUsers(req.SendTo)
|
||||
if len(users) == 0 {
|
||||
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
|
||||
} else if len(users) > 1 {
|
||||
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
|
||||
} else {
|
||||
invite.SendTo = req.SendTo
|
||||
addressValid = true
|
||||
discord = users[0].User.ID
|
||||
}
|
||||
} else if emailEnabled {
|
||||
addressValid = true
|
||||
invite.SendTo = req.SendTo
|
||||
}
|
||||
if addressValid {
|
||||
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
|
||||
if err != nil {
|
||||
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||
app.err.Printf("%s: Failed to construct invite message: %v", inviteCode, err)
|
||||
} else {
|
||||
var err error
|
||||
if discord != "" {
|
||||
err = app.discord.SendDM(msg, discord)
|
||||
} else {
|
||||
err = app.email.send(msg, req.SendTo)
|
||||
}
|
||||
if err != nil {
|
||||
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||
app.err.Printf("%s: %s: %v", inviteCode, invite.SendTo, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.SendTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.Profile != "" {
|
||||
@@ -867,15 +965,15 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
if inv.RemainingUses != 0 {
|
||||
invite.RemainingUses = inv.RemainingUses
|
||||
}
|
||||
if inv.Email != "" {
|
||||
invite.Email = inv.Email
|
||||
if inv.SendTo != "" {
|
||||
invite.SendTo = inv.SendTo
|
||||
}
|
||||
if len(inv.Notify) != 0 {
|
||||
var address string
|
||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
app.storage.loadEmails()
|
||||
if addr := app.storage.emails[gc.GetString("jfId")]; addr != nil {
|
||||
address = addr.(string)
|
||||
if addr, ok := app.storage.emails[gc.GetString("jfId")]; ok && addr.Addr != "" {
|
||||
address = addr.Addr
|
||||
}
|
||||
} else {
|
||||
address = app.config.Section("ui").Key("email").String()
|
||||
@@ -1074,14 +1172,14 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
}
|
||||
var address string
|
||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
var ok bool
|
||||
address, ok = app.storage.emails[gc.GetString("jfId")].(string)
|
||||
addr, ok := app.storage.emails[gc.GetString("jfId")]
|
||||
if !ok {
|
||||
app.err.Printf("%s: Couldn't find email address. Make sure it's set", code)
|
||||
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
||||
respond(500, "Missing user email", gc)
|
||||
return
|
||||
}
|
||||
address = addr.Addr
|
||||
} else {
|
||||
address = app.config.Section("ui").Key("email").String()
|
||||
}
|
||||
@@ -1168,7 +1266,8 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
user.LastActive = jfUser.LastActivityDate.Unix()
|
||||
}
|
||||
if email, ok := app.storage.emails[jfUser.ID]; ok {
|
||||
user.Email = email.(string)
|
||||
user.Email = email.Addr
|
||||
user.NotifyThroughEmail = email.Contact
|
||||
}
|
||||
expiry, ok := app.storage.users[jfUser.ID]
|
||||
if ok {
|
||||
@@ -1178,6 +1277,15 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
user.Telegram = tgUser.Username
|
||||
user.NotifyThroughTelegram = tgUser.Contact
|
||||
}
|
||||
if mxUser, ok := app.storage.matrix[jfUser.ID]; ok {
|
||||
user.Matrix = mxUser.UserID
|
||||
user.NotifyThroughMatrix = mxUser.Contact
|
||||
}
|
||||
if dcUser, ok := app.storage.discord[jfUser.ID]; ok {
|
||||
user.Discord = dcUser.Username + "#" + dcUser.Discriminator
|
||||
user.DiscordID = dcUser.ID
|
||||
user.NotifyThroughDiscord = dcUser.Contact
|
||||
}
|
||||
resp.UserList[i] = user
|
||||
i++
|
||||
}
|
||||
@@ -1253,7 +1361,11 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
for _, jfUser := range users {
|
||||
id := jfUser.ID
|
||||
if address, ok := req[id]; ok {
|
||||
app.storage.emails[id] = address
|
||||
contact := true
|
||||
if oldAddr, ok := app.storage.emails[id]; ok {
|
||||
contact = oldAddr.Contact
|
||||
}
|
||||
app.storage.emails[id] = EmailAddress{Addr: address, Contact: contact}
|
||||
if ombiEnabled {
|
||||
ombiUser, code, err := app.getOmbiUser(id)
|
||||
if code == 200 && err == nil {
|
||||
@@ -1271,6 +1383,77 @@ func (app *appContext) ModifyEmails(gc *gin.Context) {
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Resets a user's password with a PIN, and optionally set a new password if given.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Success 400 {object} PasswordValidation
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param ResetPasswordDTO body ResetPasswordDTO true "Pin and optional Password."
|
||||
// @Router /reset [post]
|
||||
// @tags Other
|
||||
func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
var req ResetPasswordDTO
|
||||
gc.BindJSON(&req)
|
||||
validation := app.validator.validate(req.Password)
|
||||
valid := true
|
||||
for _, val := range validation {
|
||||
if !val {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
if !valid || req.PIN == "" {
|
||||
// 200 bcs idk what i did in js
|
||||
app.info.Printf("%s: Password reset failed: Invalid password", req.PIN)
|
||||
gc.JSON(400, validation)
|
||||
return
|
||||
}
|
||||
resp, status, err := app.jf.ResetPassword(req.PIN)
|
||||
if status != 200 || err != nil || !resp.Success {
|
||||
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
||||
respondBool(status, false, gc)
|
||||
return
|
||||
}
|
||||
if req.Password == "" || len(resp.UsersReset) == 0 {
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
user, status, err := app.jf.UserByName(resp.UsersReset[0], false)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get user \"%s\" (%d): %v", resp.UsersReset[0], status, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
status, err = app.jf.SetPassword(user.ID, req.PIN, req.Password)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to change password for \"%s\" (%d): %v", resp.UsersReset[0], status, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
// Silently fail for changing ombi passwords
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", resp.UsersReset[0], status, err)
|
||||
respondBool(200, true, gc)
|
||||
return
|
||||
}
|
||||
ombiUser, status, err := app.getOmbiUser(user.ID)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", resp.UsersReset[0], status, err)
|
||||
respondBool(200, true, gc)
|
||||
return
|
||||
}
|
||||
ombiUser["password"] = req.Password
|
||||
status, err = app.ombi.ModifyUser(ombiUser)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
|
||||
respondBool(200, true, gc)
|
||||
return
|
||||
}
|
||||
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Apply settings to a list of users, either from a profile or from another user.
|
||||
// @Produce json
|
||||
// @Param userSettingsDTO body userSettingsDTO true "Parameters for applying settings"
|
||||
@@ -1398,6 +1581,16 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if PLATFORM == "windows" {
|
||||
delete(resp.Sections["smtp"].Settings, "ssl_cert")
|
||||
for i, v := range resp.Sections["smtp"].Order {
|
||||
if v == "ssl_cert" {
|
||||
sect := resp.Sections["smtp"]
|
||||
sect.Order = append(sect.Order[:i], sect.Order[i+1:]...)
|
||||
resp.Sections["smtp"] = sect
|
||||
}
|
||||
}
|
||||
}
|
||||
for sectName, section := range resp.Sections {
|
||||
for settingName, setting := range section.Settings {
|
||||
val := app.config.Section(sectName).Key(settingName)
|
||||
@@ -1418,6 +1611,8 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
resp.Sections["email"].Settings["language"] = el
|
||||
resp.Sections["password_resets"].Settings["language"] = pl
|
||||
resp.Sections["telegram"].Settings["language"] = tl
|
||||
resp.Sections["discord"].Settings["language"] = tl
|
||||
resp.Sections["matrix"].Settings["language"] = tl
|
||||
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
@@ -1426,6 +1621,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Router /config [post]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
@@ -1451,14 +1647,20 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
tempConfig.SaveTo(app.configPath)
|
||||
tempConfig.Section("").Key("first_run").SetValue("false")
|
||||
if err := tempConfig.SaveTo(app.configPath); err != nil {
|
||||
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.debug.Println("Config saved")
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
if req["restart-program"] != nil && req["restart-program"].(bool) {
|
||||
app.info.Println("Restarting...")
|
||||
err := app.Restart()
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't restart, try restarting manually: %s", err)
|
||||
if TRAY {
|
||||
TRAYRESTART <- true
|
||||
} else {
|
||||
RESTART <- true
|
||||
}
|
||||
}
|
||||
app.loadConfig()
|
||||
@@ -1992,40 +2194,91 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Sets whether to notify a user through telegram or not.
|
||||
// @Summary Sets whether to notify a user through telegram/discord/matrix/email or not.
|
||||
// @Produce json
|
||||
// @Param telegramNotifyDTO body telegramNotifyDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
|
||||
// @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Success 400 {object} boolResponse
|
||||
// @Success 500 {object} boolResponse
|
||||
// @Router /users/telegram/notify [post]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramSetNotify(gc *gin.Context) {
|
||||
var req telegramNotifyDTO
|
||||
func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
var req SetContactMethodsDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.ID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
if tgUser, ok := app.storage.telegram[req.ID]; ok {
|
||||
tgUser.Contact = req.Enabled
|
||||
change := tgUser.Contact != req.Telegram
|
||||
tgUser.Contact = req.Telegram
|
||||
app.storage.telegram[req.ID] = tgUser
|
||||
if err := app.storage.storeTelegramUsers(); err != nil {
|
||||
respondBool(500, false, gc)
|
||||
app.err.Printf("Telegram: Failed to store users: %v", err)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
msg := ""
|
||||
if !req.Enabled {
|
||||
msg = "not"
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Telegram {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
|
||||
}
|
||||
app.debug.Printf("Telegram: User \"%s\" will %s be notified through Telegram.", tgUser.Username, msg)
|
||||
return
|
||||
}
|
||||
app.err.Printf("Telegram: User \"%s\" does not have a telegram account registered.", req.ID)
|
||||
respondBool(400, false, gc)
|
||||
if dcUser, ok := app.storage.discord[req.ID]; ok {
|
||||
change := dcUser.Contact != req.Discord
|
||||
dcUser.Contact = req.Discord
|
||||
app.storage.discord[req.ID] = dcUser
|
||||
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||
respondBool(500, false, gc)
|
||||
app.err.Printf("Discord: Failed to store users: %v", err)
|
||||
return
|
||||
}
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Discord {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||
}
|
||||
}
|
||||
if mxUser, ok := app.storage.matrix[req.ID]; ok {
|
||||
change := mxUser.Contact != req.Matrix
|
||||
mxUser.Contact = req.Matrix
|
||||
app.storage.matrix[req.ID] = mxUser
|
||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||
respondBool(500, false, gc)
|
||||
app.err.Printf("Matrix: Failed to store users: %v", err)
|
||||
return
|
||||
}
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Matrix {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
|
||||
}
|
||||
}
|
||||
if email, ok := app.storage.emails[req.ID]; ok {
|
||||
change := email.Contact != req.Email
|
||||
email.Contact = req.Email
|
||||
app.storage.emails[req.ID] = email
|
||||
if err := app.storage.storeEmails(); err != nil {
|
||||
respondBool(500, false, gc)
|
||||
app.err.Printf("Failed to store emails: %v", err)
|
||||
return
|
||||
}
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Email {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
|
||||
}
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
|
||||
@@ -2082,6 +2335,241 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
||||
respondBool(200, tokenIndex != -1, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Router /invite/{invCode}/discord/verified/{pin} [get]
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
pin := gc.Param("pin")
|
||||
_, ok := app.discord.verifiedTokens[pin]
|
||||
respondBool(200, ok, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns a 10-minute, one-use Discord server invite
|
||||
// @Produce json
|
||||
// @Success 200 {object} DiscordInviteDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Router /invite/{invCode}/discord/invite [get]
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||
if app.discord.inviteChannelName == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
invURL, iconURL := app.discord.NewTempInvite(10*60, 1)
|
||||
if invURL == "" {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
|
||||
}
|
||||
|
||||
// @Summary Generate and send a new PIN to a specified Matrix user.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID."
|
||||
// @Router /invite/{invCode}/matrix/user [post]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
var req MatrixSendPINDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.UserID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
ok := app.matrix.SendStart(req.UserID)
|
||||
if !ok {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Check whether a matrix PIN is valid, and mark the token as verified if so. Requires invite code.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Param userID path string true "Matrix User ID"
|
||||
// @Router /invite/{invCode}/matrix/verified/{userID}/{pin} [get]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
app.debug.Println("Matrix: Invite code was invalid")
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
userID := gc.Param("userID")
|
||||
pin := gc.Param("pin")
|
||||
user, ok := app.matrix.tokens[pin]
|
||||
if !ok {
|
||||
app.debug.Println("Matrix: PIN not found")
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
if user.User.UserID != userID {
|
||||
app.debug.Println("Matrix: User ID of PIN didn't match")
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
user.Verified = true
|
||||
app.matrix.tokens[pin] = user
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Generates a Matrix access token from a username and password.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} stringResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param MatrixLoginDTO body MatrixLoginDTO true "Username & password."
|
||||
// @Router /matrix/login [post]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixLogin(gc *gin.Context) {
|
||||
var req MatrixLoginDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.Username == "" || req.Password == "" {
|
||||
respond(400, "errorLoginBlank", gc)
|
||||
return
|
||||
}
|
||||
token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password)
|
||||
if err != nil {
|
||||
app.err.Printf("Matrix: Failed to generate token: %v", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
matrix := tempConfig.Section("matrix")
|
||||
matrix.Key("enabled").SetValue("true")
|
||||
matrix.Key("homeserver").SetValue(req.Homeserver)
|
||||
matrix.Key("token").SetValue(token)
|
||||
matrix.Key("user_id").SetValue(req.Username)
|
||||
if err := tempConfig.SaveTo(app.configPath); err != nil {
|
||||
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Links a Matrix user to a Jellyfin account via user IDs. Notifications are turned on by default.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param MatrixConnectUserDTO body MatrixConnectUserDTO true "User's Jellyfin ID & Matrix user ID."
|
||||
// @Router /users/matrix [post]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixConnect(gc *gin.Context) {
|
||||
var req MatrixConnectUserDTO
|
||||
gc.BindJSON(&req)
|
||||
if app.storage.matrix == nil {
|
||||
app.storage.matrix = map[string]MatrixUser{}
|
||||
}
|
||||
roomID, err := app.matrix.CreateRoom(req.UserID)
|
||||
if err != nil {
|
||||
app.err.Printf("Matrix: Failed to create room: %v", err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.storage.matrix[req.JellyfinID] = MatrixUser{
|
||||
UserID: req.UserID,
|
||||
RoomID: roomID,
|
||||
Lang: "en-us",
|
||||
Contact: true,
|
||||
}
|
||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||
app.err.Printf("Failed to store Matrix users: %v", err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
|
||||
// @Produce json
|
||||
// @Success 200 {object} DiscordUsersDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param username path string true "username to search."
|
||||
// @Router /users/discord/{username} [get]
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordGetUsers(gc *gin.Context) {
|
||||
name := gc.Param("username")
|
||||
if name == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
users := app.discord.GetUsers(name)
|
||||
resp := DiscordUsersDTO{Users: make([]DiscordUserDTO, len(users))}
|
||||
for i, u := range users {
|
||||
resp.Users[i] = DiscordUserDTO{
|
||||
Name: u.User.Username + "#" + u.User.Discriminator,
|
||||
ID: u.User.ID,
|
||||
AvatarURL: u.User.AvatarURL("32"),
|
||||
}
|
||||
}
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
// @Summary Links a Discord account to a Jellyfin account via user IDs. Notifications are turned on by default.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
|
||||
// @Router /users/discord [post]
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordConnect(gc *gin.Context) {
|
||||
var req DiscordConnectUserDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.JellyfinID == "" || req.DiscordID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
user, ok := app.discord.NewUser(req.DiscordID)
|
||||
if !ok {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.storage.discord[req.JellyfinID] = user
|
||||
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||
app.err.Printf("Failed to store Discord users: %v", err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Restarts the program. No response means success.
|
||||
// @Router /restart [post]
|
||||
// @Security Bearer
|
||||
|
||||
69
autostart.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// +build tray
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/emersion/go-autostart"
|
||||
"github.com/getlantern/systray"
|
||||
)
|
||||
|
||||
type Autostart struct {
|
||||
as *autostart.App
|
||||
enabled bool
|
||||
menuitem *systray.MenuItem
|
||||
clicked chan bool
|
||||
}
|
||||
|
||||
func NewAutostart(name, displayname, trayName, trayTooltip string) *Autostart {
|
||||
a := &Autostart{
|
||||
as: &autostart.App{
|
||||
Name: name,
|
||||
DisplayName: displayname,
|
||||
},
|
||||
enabled: true,
|
||||
clicked: make(chan bool),
|
||||
}
|
||||
a.menuitem = systray.AddMenuItemCheckbox(trayName, trayTooltip, a.as.IsEnabled())
|
||||
command := os.Args
|
||||
command[0], _ = filepath.Abs(command[0])
|
||||
// Make sure to replace any relative paths with absolute ones
|
||||
pathArgs := []string{"-d", "-data", "-c", "-config"}
|
||||
for i := 1; i < len(command); i++ {
|
||||
isPath := false
|
||||
for _, p := range pathArgs {
|
||||
if command[i-1] == p {
|
||||
isPath = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isPath {
|
||||
command[i], _ = filepath.Abs(command[i])
|
||||
}
|
||||
}
|
||||
a.as.Exec = command
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Autostart) HandleCheck() {
|
||||
for range a.menuitem.ClickedCh {
|
||||
if !a.menuitem.Checked() {
|
||||
if err := a.as.Enable(); err != nil {
|
||||
log.Printf("Failed to enable autostart on login: %v", err)
|
||||
} else {
|
||||
a.menuitem.Check()
|
||||
log.Printf("Enabled autostart")
|
||||
}
|
||||
} else {
|
||||
if err := a.as.Disable(); err != nil {
|
||||
log.Printf("Failed to disable autostart on login: %v", err)
|
||||
} else {
|
||||
a.menuitem.Uncheck()
|
||||
log.Printf("Disabled autostart")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ type TimeoutHandler func()
|
||||
func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler {
|
||||
return func() {
|
||||
if r := recover(); r != nil {
|
||||
out := fmt.Sprintf("Failed to authenticate with %s @ %s: Timed out", name, addr)
|
||||
out := fmt.Sprintf("Failed to authenticate with %s @ \"%s\": Timed out", name, addr)
|
||||
if noFail {
|
||||
log.Print(out)
|
||||
} else {
|
||||
|
||||
37
config.go
@@ -14,6 +14,8 @@ import (
|
||||
var emailEnabled = false
|
||||
var messagesEnabled = false
|
||||
var telegramEnabled = false
|
||||
var discordEnabled = false
|
||||
var matrixEnabled = false
|
||||
|
||||
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
||||
val := app.config.Section(sect).Key(key).MustString("")
|
||||
@@ -42,7 +44,7 @@ func (app *appContext) loadConfig() error {
|
||||
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users"} {
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users"} {
|
||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
||||
}
|
||||
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
|
||||
@@ -82,20 +84,26 @@ func (app *appContext) loadConfig() error {
|
||||
app.MustSetValue("user_expiry", "email_html", "jfa-go:"+"user-expired.html")
|
||||
app.MustSetValue("user_expiry", "email_text", "jfa-go:"+"user-expired.txt")
|
||||
|
||||
app.MustSetValue("matrix", "topic", "Jellyfin notifications")
|
||||
|
||||
app.config.Section("jellyfin").Key("version").SetValue(version)
|
||||
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
|
||||
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
|
||||
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
|
||||
discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false)
|
||||
matrixEnabled = app.config.Section("matrix").Key("enabled").MustBool(false)
|
||||
if !messagesEnabled {
|
||||
emailEnabled = false
|
||||
telegramEnabled = false
|
||||
discordEnabled = false
|
||||
matrixEnabled = false
|
||||
} else if app.config.Section("email").Key("method").MustString("") == "" {
|
||||
emailEnabled = false
|
||||
} else {
|
||||
emailEnabled = true
|
||||
}
|
||||
if !emailEnabled && !telegramEnabled {
|
||||
if !emailEnabled && !telegramEnabled && !discordEnabled && !matrixEnabled {
|
||||
messagesEnabled = false
|
||||
}
|
||||
|
||||
@@ -143,28 +151,3 @@ func (app *appContext) loadConfig() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *appContext) migrateEmailConfig() {
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
fmt.Println(warning("Part of your email configuration will be migrated to the new \"messages\" section.\nA backup will be made."))
|
||||
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to backup config: %v", err)
|
||||
return
|
||||
}
|
||||
for _, setting := range []string{"use_24h", "date_format", "message"} {
|
||||
if val := app.config.Section("email").Key(setting).Value(); val != "" {
|
||||
tempConfig.Section("email").Key(setting).SetValue("")
|
||||
tempConfig.Section("messages").Key(setting).SetValue(val)
|
||||
}
|
||||
}
|
||||
if app.config.Section("messages").Key("enabled").MustBool(false) || app.config.Section("telegram").Key("enabled").MustBool(false) {
|
||||
tempConfig.Section("messages").Key("enabled").SetValue("true")
|
||||
}
|
||||
err = tempConfig.SaveTo(app.configPath)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to save config: %v", err)
|
||||
return
|
||||
}
|
||||
app.loadConfig()
|
||||
}
|
||||
|
||||
@@ -546,11 +546,94 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Discord",
|
||||
"description": "Settings for Discord invites/signup/notifications"
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enable signup verification through Discord and the sending of notifications through it.\nSee the jfa-go wiki for setting up a bot."
|
||||
},
|
||||
"required": {
|
||||
"name": "Require on sign-up",
|
||||
"required": false,
|
||||
"required_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Require Discord connection on sign-up. See the jfa-go wiki for info on setting this up."
|
||||
},
|
||||
"token": {
|
||||
"name": "API Token",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Discord Bot API Token."
|
||||
},
|
||||
"start_command": {
|
||||
"name": "Start command",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "!start",
|
||||
"description": "Command to start the user verification process."
|
||||
},
|
||||
"channel": {
|
||||
"name": "Channel to monitor",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Only listen to commands in specified channel. Leave blank to monitor all."
|
||||
},
|
||||
"provide_invite": {
|
||||
"name": "Provide server invite",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Generate a one-time discord server invite for the account creation form. Required Bot permission \"Create instant invite\", you may need to re-add the bot to your server after."
|
||||
},
|
||||
"invite_channel": {
|
||||
"name": "Invite channel",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "provide_invite",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Channel to invite new users to."
|
||||
},
|
||||
"language": {
|
||||
"name": "Language",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "select",
|
||||
"options": [
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default Discord message language. Visit weblate if you'd like to translate."
|
||||
}
|
||||
}
|
||||
},
|
||||
"telegram": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Telegram",
|
||||
"description": "Settings for Telegram signup/notifications"
|
||||
"description": "Settings for Telegram signup/notifications. See the jfa-go wiki for info on setting this up."
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
@@ -565,6 +648,7 @@
|
||||
"name": "Require on sign-up",
|
||||
"required": false,
|
||||
"required_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Require telegram connection on sign-up."
|
||||
@@ -592,6 +676,80 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"matrix": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Matrix",
|
||||
"description": "Settings for Matrix invites/signup/notifications. See the jfa-go wiki for info on setting this up."
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enable signup verification through Matrix and the sending of notifications through it.\nSee the jfa-go wiki for setting up a bot."
|
||||
},
|
||||
"required": {
|
||||
"name": "Require on sign-up",
|
||||
"required": false,
|
||||
"required_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Require Matrix connection on sign-up."
|
||||
},
|
||||
"homeserver": {
|
||||
"name": "Home Server URL",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Matrix Home server URL."
|
||||
},
|
||||
"token": {
|
||||
"name": "Access Token",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Matrix Bot API Token."
|
||||
},
|
||||
"user_id": {
|
||||
"name": "Bot User ID",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "User ID of bot account (Example: @jfa-bot:riot.im)"
|
||||
},
|
||||
"topic": {
|
||||
"name": "Chat topic",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "Jellyfin notifications",
|
||||
"description": "Topic of Matrix private chats."
|
||||
},
|
||||
"language": {
|
||||
"name": "Language",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "select",
|
||||
"options": [
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default Matrix message language. Visit weblate if you'd like to translate."
|
||||
}
|
||||
}
|
||||
},
|
||||
"password_resets": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
@@ -626,6 +784,15 @@
|
||||
"value": false,
|
||||
"description": "Send users a link to reset their password instead of a PIN. Must be enabled to reset Ombi password at the same time as the Jellyfin password."
|
||||
},
|
||||
"set_password": {
|
||||
"name": "Set password through link",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "link_reset",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Instead of automatically setting the user's password to the PIN, allow them to set a new password through the reset link."
|
||||
},
|
||||
"language": {
|
||||
"name": "Default reset link language",
|
||||
"required": false,
|
||||
@@ -1077,6 +1244,14 @@
|
||||
"value": "",
|
||||
"description": "Location of stored invites (json)."
|
||||
},
|
||||
"password_resets": {
|
||||
"name": "Password Resets",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Location of stored non-Jellyfin password resets (json)."
|
||||
},
|
||||
"emails": {
|
||||
"name": "Email Addresses",
|
||||
"required": false,
|
||||
@@ -1140,6 +1315,22 @@
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Stores telegram user IDs and language preferences."
|
||||
},
|
||||
"matrix_users": {
|
||||
"name": "Matrix users",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Stores matrix user IDs and language preferences."
|
||||
},
|
||||
"discord_users": {
|
||||
"name": "Discord users",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Stores discord user IDs and language preferences."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
56
css/base.css
@@ -30,12 +30,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
@media screen and (max-width: 1000px) {
|
||||
:root {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.table-responsive table {
|
||||
min-width: 660px;
|
||||
min-width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,10 @@ div.card:contains(section.banner.footer) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -172,7 +176,7 @@ div.card:contains(section.banner.footer) {
|
||||
}
|
||||
|
||||
p.sm,
|
||||
span.sm {
|
||||
span.sm:not(.heading) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
@@ -424,6 +428,7 @@ p.top {
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#notification-box {
|
||||
@@ -438,6 +443,10 @@ p.top {
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-display.lg {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap; /* css-3 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
@@ -483,3 +492,44 @@ a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
|
||||
max-width: 15rem;
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
td.img-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
span.img-circle.lg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
span.shield.img-circle {
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
img.img-circle {
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table td.sm {
|
||||
padding-top: 0.1rem;
|
||||
padding-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.table-inline {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.card:contains(section.banner.footer) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.card.sectioned {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.card.sectioned .section {
|
||||
padding: var(--spacing-4, 1rem);
|
||||
}
|
||||
|
||||
410
discord.go
Normal file
@@ -0,0 +1,410 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
dg "github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
type DiscordDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
bot *dg.Session
|
||||
username string
|
||||
tokens []string
|
||||
verifiedTokens map[string]DiscordUser // Map of tokens to discord users.
|
||||
channelID, channelName, inviteChannelID, inviteChannelName string
|
||||
guildID string
|
||||
serverChannelName, serverName string
|
||||
users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start.
|
||||
app *appContext
|
||||
}
|
||||
|
||||
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
||||
token := app.config.Section("discord").Key("token").String()
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("token was blank")
|
||||
}
|
||||
bot, err := dg.New("Bot " + token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dd := &DiscordDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
bot: bot,
|
||||
tokens: []string{},
|
||||
verifiedTokens: map[string]DiscordUser{},
|
||||
users: map[string]DiscordUser{},
|
||||
app: app,
|
||||
}
|
||||
for _, user := range app.storage.discord {
|
||||
dd.users[user.ID] = user
|
||||
}
|
||||
|
||||
return dd, nil
|
||||
}
|
||||
|
||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||
func (d *DiscordDaemon) NewAuthToken() string {
|
||||
pin := genAuthToken()
|
||||
d.tokens = append(d.tokens, pin)
|
||||
return pin
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) NewUnknownUser(channelID, userID, discrim, username string) DiscordUser {
|
||||
user := DiscordUser{
|
||||
ChannelID: channelID,
|
||||
ID: userID,
|
||||
Username: username,
|
||||
Discriminator: discrim,
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string) DiscordUser {
|
||||
if user, ok := d.users[userID]; ok {
|
||||
return user
|
||||
}
|
||||
return d.NewUnknownUser(channelID, userID, discrim, username)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) run() {
|
||||
d.bot.AddHandler(d.messageHandler)
|
||||
d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers | dg.IntentsGuildInvites
|
||||
if err := d.bot.Open(); err != nil {
|
||||
d.app.err.Printf("Discord: Failed to start daemon: %v", err)
|
||||
return
|
||||
}
|
||||
// Wait for everything to populate, it's slow sometimes.
|
||||
for d.bot.State == nil {
|
||||
continue
|
||||
}
|
||||
for d.bot.State.User == nil {
|
||||
continue
|
||||
}
|
||||
d.username = d.bot.State.User.Username
|
||||
for d.bot.State.Guilds == nil {
|
||||
continue
|
||||
}
|
||||
// Choose the last guild (server), for now we don't really support multiple anyway
|
||||
d.guildID = d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID
|
||||
guild, err := d.bot.Guild(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
||||
}
|
||||
d.serverChannelName = guild.Name
|
||||
d.serverName = guild.Name
|
||||
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
|
||||
d.channelName = channel
|
||||
d.serverChannelName += "/" + channel
|
||||
}
|
||||
if d.app.config.Section("discord").Key("provide_invite").MustBool(false) {
|
||||
if invChannel := d.app.config.Section("discord").Key("invite_channel").String(); invChannel != "" {
|
||||
d.inviteChannelName = invChannel
|
||||
}
|
||||
}
|
||||
defer d.bot.Close()
|
||||
<-d.ShutdownChannel
|
||||
d.ShutdownChannel <- "Down"
|
||||
return
|
||||
}
|
||||
|
||||
// NewTempInvite creates an invite link, and returns the invite URL, as well as the URL for the server icon.
|
||||
func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconURL string) {
|
||||
var inv *dg.Invite
|
||||
var err error
|
||||
if d.inviteChannelName == "" {
|
||||
d.app.err.Println("Discord: Cannot create invite without channel specified in settings.")
|
||||
return
|
||||
}
|
||||
if d.inviteChannelID == "" {
|
||||
channels, err := d.bot.GuildChannels(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Couldn't get channel list: %v", err)
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for _, channel := range channels {
|
||||
// channel, err := d.bot.Channel(ch.ID)
|
||||
// if err != nil {
|
||||
// d.app.err.Printf("Discord: Couldn't get channel: %v", err)
|
||||
// return
|
||||
// }
|
||||
if channel.Name == d.inviteChannelName {
|
||||
d.inviteChannelID = channel.ID
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
d.app.err.Printf("Discord: Couldn't find invite channel \"%s\"", d.inviteChannelName)
|
||||
return
|
||||
}
|
||||
}
|
||||
// channel, err := d.bot.Channel(d.inviteChannelID)
|
||||
// if err != nil {
|
||||
// d.app.err.Printf("Discord: Couldn't get invite channel: %v", err)
|
||||
// return
|
||||
// }
|
||||
inv, err = d.bot.ChannelInviteCreate(d.inviteChannelID, dg.Invite{
|
||||
// Guild: d.bot.State.Guilds[len(d.bot.State.Guilds)-1],
|
||||
// Channel: channel,
|
||||
// Inviter: d.bot.State.User,
|
||||
MaxAge: ageSeconds,
|
||||
MaxUses: maxUses,
|
||||
Temporary: false,
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create invite: %v", err)
|
||||
return
|
||||
}
|
||||
inviteURL = "https://discord.gg/" + inv.Code
|
||||
guild, err := d.bot.Guild(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
||||
return
|
||||
}
|
||||
iconURL = guild.IconURL()
|
||||
return
|
||||
}
|
||||
|
||||
// Returns the user(s) roughly corresponding to the username (if they are in the guild).
|
||||
// if no discriminator (#xxxx) is given in the username and there are multiple corresponding users, a list of all matching users is returned.
|
||||
func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
|
||||
members, err := d.bot.GuildMembers(
|
||||
d.guildID,
|
||||
"",
|
||||
1000,
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get members: %v", err)
|
||||
return nil
|
||||
}
|
||||
hasDiscriminator := strings.Contains(username, "#")
|
||||
var users []*dg.Member
|
||||
for _, member := range members {
|
||||
if hasDiscriminator {
|
||||
if member.User.Username+"#"+member.User.Discriminator == username {
|
||||
return []*dg.Member{member}
|
||||
}
|
||||
}
|
||||
if strings.Contains(member.User.Username, username) {
|
||||
users = append(users, member)
|
||||
}
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
|
||||
u, err := d.bot.User(ID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get user: %v", err)
|
||||
return
|
||||
}
|
||||
user.ID = ID
|
||||
user.Username = u.Username
|
||||
user.Contact = true
|
||||
user.Discriminator = u.Discriminator
|
||||
channel, err := d.bot.UserChannelCreate(ID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create DM channel: %v", err)
|
||||
return
|
||||
}
|
||||
user.ChannelID = channel.ID
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) Shutdown() {
|
||||
d.Stopped = true
|
||||
d.ShutdownChannel <- "Down"
|
||||
<-d.ShutdownChannel
|
||||
close(d.ShutdownChannel)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
|
||||
if m.GuildID != "" && d.channelName != "" {
|
||||
if d.channelID == "" {
|
||||
channel, err := s.Channel(m.ChannelID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
|
||||
d.channelName = ""
|
||||
}
|
||||
if channel.Name == d.channelName {
|
||||
d.channelID = channel.ID
|
||||
}
|
||||
}
|
||||
if d.channelID != m.ChannelID {
|
||||
d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
|
||||
return
|
||||
}
|
||||
}
|
||||
if m.Author.ID == s.State.User.ID {
|
||||
return
|
||||
}
|
||||
sects := strings.Split(m.Content, " ")
|
||||
if len(sects) == 0 {
|
||||
return
|
||||
}
|
||||
lang := d.app.storage.lang.chosenTelegramLang
|
||||
if user, ok := d.users[m.Author.ID]; ok {
|
||||
if _, ok := d.app.storage.lang.Telegram[user.Lang]; ok {
|
||||
lang = user.Lang
|
||||
}
|
||||
}
|
||||
switch msg := sects[0]; msg {
|
||||
case d.app.config.Section("discord").Key("start_command").MustString("!start"):
|
||||
d.commandStart(s, m, lang)
|
||||
case "!lang":
|
||||
d.commandLang(s, m, sects, lang)
|
||||
default:
|
||||
d.commandPIN(s, m, sects, lang)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) commandStart(s *dg.Session, m *dg.MessageCreate, lang string) {
|
||||
channel, err := s.UserChannelCreate(m.Author.ID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", m.Author.Username, err)
|
||||
return
|
||||
}
|
||||
user := d.MustGetUser(channel.ID, m.Author.ID, m.Author.Discriminator, m.Author.Username)
|
||||
d.users[m.Author.ID] = user
|
||||
content := d.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
||||
content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"})
|
||||
_, err = s.ChannelMessageSend(channel.ID, content)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) commandLang(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
|
||||
if len(sects) == 1 {
|
||||
list := "!lang <lang>\n"
|
||||
for code := range d.app.storage.lang.Telegram {
|
||||
list += fmt.Sprintf("%s: %s\n", code, d.app.storage.lang.Telegram[code].Meta.Name)
|
||||
}
|
||||
_, err := s.ChannelMessageSendReply(
|
||||
m.ChannelID,
|
||||
list,
|
||||
m.Reference(),
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
|
||||
var user DiscordUser
|
||||
for jfID, user := range d.app.storage.discord {
|
||||
if user.ID == m.Author.ID {
|
||||
user.Lang = sects[1]
|
||||
d.app.storage.discord[jfID] = user
|
||||
if err := d.app.storage.storeDiscordUsers(); err != nil {
|
||||
d.app.err.Printf("Failed to store Discord users: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
d.users[m.Author.ID] = user
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
|
||||
if _, ok := d.users[m.Author.ID]; ok {
|
||||
channel, err := s.Channel(m.ChannelID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get channel: %v", err)
|
||||
return
|
||||
}
|
||||
if channel.Type != dg.ChannelTypeDM {
|
||||
d.app.debug.Println("Discord: Ignoring message as not a DM")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
d.app.debug.Println("Discord: Ignoring message as user was not found")
|
||||
return
|
||||
}
|
||||
tokenIndex := -1
|
||||
for i, token := range d.tokens {
|
||||
if sects[0] == token {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if tokenIndex == -1 {
|
||||
_, err := s.ChannelMessageSend(
|
||||
m.ChannelID,
|
||||
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
_, err := s.ChannelMessageSend(
|
||||
m.ChannelID,
|
||||
d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
}
|
||||
d.verifiedTokens[sects[0]] = d.users[m.Author.ID]
|
||||
d.tokens[len(d.tokens)-1], d.tokens[tokenIndex] = d.tokens[tokenIndex], d.tokens[len(d.tokens)-1]
|
||||
d.tokens = d.tokens[:len(d.tokens)-1]
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error {
|
||||
channels := make([]string, len(userID))
|
||||
for i, id := range userID {
|
||||
channel, err := d.bot.UserChannelCreate(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
channels[i] = channel.ID
|
||||
}
|
||||
return d.Send(message, channels...)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
|
||||
msg := ""
|
||||
var embeds []*dg.MessageEmbed
|
||||
if message.Markdown != "" {
|
||||
msg, embeds = StripAltText(message.Markdown, true)
|
||||
} else {
|
||||
msg = message.Text
|
||||
}
|
||||
for _, id := range channelID {
|
||||
var err error
|
||||
if len(embeds) != 0 {
|
||||
_, err = d.bot.ChannelMessageSendComplex(
|
||||
id,
|
||||
&dg.MessageSend{
|
||||
Content: msg,
|
||||
Embed: embeds[0],
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 1; i < len(embeds); i++ {
|
||||
_, err := d.bot.ChannelMessageSendEmbed(id, embeds[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, err := d.bot.ChannelMessageSend(
|
||||
id,
|
||||
msg,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
52
email.go
@@ -25,6 +25,8 @@ import (
|
||||
"github.com/mailgun/mailgun-go/v4"
|
||||
)
|
||||
|
||||
var renderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
||||
|
||||
// implements email sending, right now via smtp or mailgun.
|
||||
type EmailClient interface {
|
||||
Send(fromName, fromAddr string, message *Message, address ...string) error
|
||||
@@ -177,6 +179,20 @@ func (emailer *Emailer) NewMailgun(url, key string) {
|
||||
|
||||
// NewSMTP returns an SMTP emailClient.
|
||||
func (emailer *Emailer) NewSMTP(server string, port int, username, password string, sslTLS bool, certPath string) (err error) {
|
||||
// x509.SystemCertPool is unavailable on windows
|
||||
if PLATFORM == "windows" {
|
||||
emailer.sender = &SMTP{
|
||||
auth: smtp.PlainAuth("", username, password, server),
|
||||
server: server,
|
||||
port: port,
|
||||
sslTLS: sslTLS,
|
||||
tlsConfig: &tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
ServerName: server,
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
rootCAs, err := x509.SystemCertPool()
|
||||
if rootCAs == nil || err != nil {
|
||||
rootCAs = x509.NewCertPool()
|
||||
@@ -215,9 +231,8 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
|
||||
}
|
||||
var keys []string
|
||||
plaintext := app.config.Section("email").Key("plaintext").MustBool(false)
|
||||
telegram := app.config.Section("telegram").Key("enabled").MustBool(false)
|
||||
if plaintext {
|
||||
if telegram {
|
||||
if telegramEnabled || discordEnabled {
|
||||
keys = []string{"text"}
|
||||
text, markdown = "", ""
|
||||
} else {
|
||||
@@ -225,7 +240,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
|
||||
text = ""
|
||||
}
|
||||
} else {
|
||||
if telegram {
|
||||
if telegramEnabled || discordEnabled {
|
||||
keys = []string{"html", "text", "markdown"}
|
||||
} else {
|
||||
keys = []string{"html", "text"}
|
||||
@@ -324,7 +339,6 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
|
||||
|
||||
func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Message, error) {
|
||||
email := &Message{Subject: subject}
|
||||
renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
||||
html := markdown.ToHTML([]byte(md), nil, renderer)
|
||||
text := stripMarkdown(md)
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
@@ -522,7 +536,7 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo
|
||||
if inviteLink != "" {
|
||||
// Strip /invite form end of this URL, ik its ugly.
|
||||
template["link_reset"] = true
|
||||
pinLink := fmt.Sprintf("%s/reset?pin=%s", strings.Replace(inviteLink, "/invite", "", 1), pwr.Pin)
|
||||
pinLink := fmt.Sprintf("%s/reset?pin=%s", strings.TrimPrefix(inviteLink, "/invite"), pwr.Pin)
|
||||
template["pin"] = pinLink
|
||||
// Only used in html email.
|
||||
template["pin_code"] = pwr.Pin
|
||||
@@ -794,8 +808,27 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
|
||||
var err error
|
||||
if tgChat, ok := app.storage.telegram[id]; ok && tgChat.Contact && telegramEnabled {
|
||||
err = app.telegram.Send(email, tgChat.ChatID)
|
||||
} else if address, ok := app.storage.emails[id]; ok {
|
||||
err = app.email.send(email, address.(string))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if dcChat, ok := app.storage.discord[id]; ok && dcChat.Contact && discordEnabled {
|
||||
err = app.discord.Send(email, dcChat.ChannelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mxChat, ok := app.storage.matrix[id]; ok && mxChat.Contact && matrixEnabled {
|
||||
err = app.matrix.Send(email, mxChat.RoomID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if address, ok := app.storage.emails[id]; ok && address.Contact && emailEnabled {
|
||||
err = app.email.send(email, address.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -805,11 +838,14 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
|
||||
}
|
||||
|
||||
func (app *appContext) getAddressOrName(jfID string) string {
|
||||
if dcChat, ok := app.storage.discord[jfID]; ok && dcChat.Contact && discordEnabled {
|
||||
return dcChat.Username + "#" + dcChat.Discriminator
|
||||
}
|
||||
if tgChat, ok := app.storage.telegram[jfID]; ok && tgChat.Contact && telegramEnabled {
|
||||
return "@" + tgChat.Username
|
||||
}
|
||||
if addr, ok := app.storage.emails[jfID]; ok {
|
||||
return addr.(string)
|
||||
return addr.Addr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
44
exit.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/browser"
|
||||
)
|
||||
|
||||
// Exit dumps the last 100 lines of output to a crash file in /tmp (or equivalent), and generates a prettier HTML file containing it that is opened in the browser if possible.
|
||||
func Exit(err interface{}) {
|
||||
tmpl, err2 := template.ParseFS(localFS, "html/crash.html", "html/header.html")
|
||||
if err2 != nil {
|
||||
log.Fatalf("Failed to load template: %v", err)
|
||||
}
|
||||
logCache := lineCache.String()
|
||||
sanitized := sanitizeLog(logCache)
|
||||
data := map[string]interface{}{
|
||||
"Log": logCache,
|
||||
"SanitizedLog": sanitized,
|
||||
}
|
||||
if err != nil {
|
||||
data["Err"] = err
|
||||
}
|
||||
fpath := filepath.Join(temp, "jfa-go-crash-"+time.Now().Local().Format("2006-01-02T15:04:05"))
|
||||
err2 = os.WriteFile(fpath+".txt", []byte(logCache), 0666)
|
||||
if err2 != nil {
|
||||
log.Fatalf("Failed to write crash dump file: %v", err2)
|
||||
}
|
||||
log.Printf("\n------\nA crash report has been saved to \"%s\".\n------", fpath+".txt")
|
||||
f, err2 := os.OpenFile(fpath+".html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err2 != nil {
|
||||
log.Fatalf("Failed to open crash dump file: %v", err2)
|
||||
}
|
||||
defer f.Close()
|
||||
err2 = tmpl.Execute(f, data)
|
||||
if err2 != nil {
|
||||
log.Fatalf("Failed to execute template: %v", err2)
|
||||
}
|
||||
browser.OpenFile(fpath + ".html")
|
||||
}
|
||||
22
go.mod
@@ -10,30 +10,42 @@ replace github.com/hrfee/jfa-go/ombi => ./ombi
|
||||
|
||||
replace github.com/hrfee/jfa-go/logger => ./logger
|
||||
|
||||
replace github.com/hrfee/jfa-go/linecache => ./linecache
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/discordgo v0.23.2
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/fatih/color v1.10.0
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/getlantern/systray v1.1.0
|
||||
github.com/gin-contrib/pprof v1.3.0
|
||||
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.3 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||
github.com/golang/protobuf v1.4.3 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd
|
||||
github.com/google/uuid v1.1.2 // indirect
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20210105184019-fdc97b4e86cc
|
||||
github.com/hrfee/jfa-go/docs v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/jfa-go/linecache v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/hrfee/jfa-go/logger v0.0.0-00010101000000-000000000000
|
||||
github.com/hrfee/jfa-go/ombi v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/mediabrowser v0.3.3
|
||||
github.com/hrfee/mediabrowser v0.3.4
|
||||
github.com/itchyny/timefmt-go v0.1.2
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4
|
||||
github.com/mailgun/mailgun-go/v4 v4.5.1
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16
|
||||
github.com/pkg/browser v0.0.0-20210606212950-a7b7a6107d32 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
|
||||
github.com/swaggo/gin-swagger v1.3.0
|
||||
@@ -42,9 +54,9 @@ require (
|
||||
github.com/ugorji/go v1.2.0 // indirect
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c // indirect
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 // indirect
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b // indirect
|
||||
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 // indirect
|
||||
golang.org/x/tools v0.1.3 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
)
|
||||
|
||||
79
go.sum
@@ -11,12 +11,16 @@ 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/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
|
||||
github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
|
||||
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/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -26,6 +30,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
|
||||
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/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:M88ob4TyDnEqNuL3PgsE/p3bDujfspnulR+0dQWNYZs=
|
||||
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:buzQsO8HHkZX2Q45fdfGH1xejPjuDQaXH8btcYMFzPM=
|
||||
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=
|
||||
@@ -40,6 +46,20 @@ github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/systray v1.1.0 h1:U0wCEqseLi2ok1fE6b88gJklzriavPJixZysZPkZd/Y=
|
||||
github.com/getlantern/systray v1.1.0/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/gzip v0.0.1 h1:ezvKOL6jH+jlzdHNE4h9h8q8uMpDQjyl0NN0Jd7jozc=
|
||||
@@ -70,6 +90,8 @@ github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL9
|
||||
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/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
|
||||
github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
|
||||
@@ -93,6 +115,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
@@ -128,8 +152,10 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/hrfee/mediabrowser v0.3.3 h1:7E05uiol8hh2ytKn3WVLrUIvHAyifYEIy3Y5qtuNh8I=
|
||||
github.com/hrfee/mediabrowser v0.3.3/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hrfee/mediabrowser v0.3.4 h1:D2FTnuRDXUUAHW80L1kamhVUKNifm8peZVVPNe0yWmA=
|
||||
github.com/hrfee/mediabrowser v0.3.4/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||
github.com/itchyny/timefmt-go v0.1.2 h1:q0Xa4P5it6K6D7ISsbLAMwx1PnWlixDcJL6/sFs93Hs=
|
||||
github.com/itchyny/timefmt-go v0.1.2/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
@@ -165,6 +191,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
|
||||
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/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 h1:ZtO5uywdd5dLDCud4r0r55eP4j9FuUNpl60Gmntcop4=
|
||||
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
@@ -181,6 +209,10 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pkg/browser v0.0.0-20210606212950-a7b7a6107d32 h1:K3WnH8Ka32vWygzmjKEhz1zAVqckNoWDqX3azMxuiSA=
|
||||
github.com/pkg/browser v0.0.0-20210606212950-a7b7a6107d32/go.mod h1:yvwcBfzEX4m+eTgxPBbNYytaWFv4PSQzBaeYjxp8Iik=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -189,10 +221,14 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCb
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
@@ -235,10 +271,12 @@ 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/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
|
||||
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead h1:jeP6FgaSLNTMP+Yri3qjlACywQLye+huGLmNGhBzm6k=
|
||||
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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=
|
||||
@@ -251,8 +289,9 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
|
||||
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=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -268,16 +307,21 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c h1:KHUzaHIpjWVlVVNh65G3hhuj3KB1HnjY6Cq5cTvRQT8=
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -289,20 +333,27 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210531080801-fdfd190a6549 h1:OL5GcZ2XPkte3dpfuFQ9o884vrE3BZQhajdntNMruv4=
|
||||
golang.org/x/sys v0.0.0-20210531080801-fdfd190a6549/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 h1:faDu4veV+8pcThn4fewv6TVlNCezafGoC1gM/mxQLbQ=
|
||||
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -314,8 +365,10 @@ golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgw
|
||||
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/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=
|
||||
golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3 h1:L69ShwSZEyCsLKoAxDKeMvLDZkumEe8gXUZAjab0tX8=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
<script>
|
||||
window.URLBase = "{{ .urlBase }}";
|
||||
window.notificationsEnabled = {{ .notifications }};
|
||||
window.emailEnabled = {{ .email_enabled }};
|
||||
window.telegramEnabled = {{ .telegram_enabled }};
|
||||
window.emailEnabled = {{ .emailEnabled }};
|
||||
window.telegramEnabled = {{ .telegramEnabled }};
|
||||
window.discordEnabled = {{ .discordEnabled }};
|
||||
window.matrixEnabled = {{ .matrixEnabled }};
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.usernameEnabled = {{ .username }};
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
@@ -309,7 +311,7 @@
|
||||
<span class="button ~urge !normal full-width center" id="update-update">{{ .strings.update }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .telegram_enabled }}
|
||||
{{ if .telegramEnabled }}
|
||||
<div id="modal-telegram" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading mb-1">{{ .strings.linkTelegram }}</span>
|
||||
@@ -327,6 +329,31 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .discordEnabled }}
|
||||
<div id="modal-discord" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading mb-1"><span id="discord-header"></span><span class="modal-close">×</span></span>
|
||||
<p class="content mb-1" id="discord-description"></p>
|
||||
<div class="row">
|
||||
<input type="search" class="col sm field ~neutral !normal input" id="discord-search" placeholder="user#1234">
|
||||
</div>
|
||||
<table class="table"><tbody id="discord-list"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div id="modal-matrix" class="modal">
|
||||
<form class="modal-content card" id="form-matrix" href="">
|
||||
<span class="heading">{{ .strings.linkMatrix }}</span>
|
||||
<p class="content">{{ .strings.linkMatrixDescription }}</p>
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.matrixHomeServer }}" id="matrix-homeserver">
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.username }}" id="matrix-user">
|
||||
<input type="password" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="matrix-password">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="notification-box"></div>
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
@@ -488,7 +515,14 @@
|
||||
<div id="create-send-to-container">
|
||||
<label class="label supra">{{ .strings.inviteSendToEmail }}</label>
|
||||
<div class="flex-expand mb-1 mt-half">
|
||||
{{ if .discordEnabled }}
|
||||
<input type="text" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com | user#1234">
|
||||
<span id="create-send-to-search" class="button ~neutral !normal mr-1">
|
||||
<i class="icon ri-search-2-line" title="{{ .strings.search }}"></i>
|
||||
</span>
|
||||
{{ else }}
|
||||
<input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com">
|
||||
{{ end }}
|
||||
<label for="create-send-to-enabled" class="button ~neutral !normal">
|
||||
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
|
||||
</label>
|
||||
@@ -522,9 +556,15 @@
|
||||
<th><input type="checkbox" value="" id="accounts-select-all"></th>
|
||||
<th>{{ .strings.username }}</th>
|
||||
<th>{{ .strings.emailAddress }}</th>
|
||||
{{ if .telegram_enabled }}
|
||||
{{ if .telegramEnabled }}
|
||||
<th>Telegram</th>
|
||||
{{ end }}
|
||||
{{ if .matrixEnabled }}
|
||||
<th>Matrix</th>
|
||||
{{ end }}
|
||||
{{ if .discordEnabled }}
|
||||
<th>Discord</th>
|
||||
{{ end }}
|
||||
<th>{{ .strings.expiry }}</th>
|
||||
<th>{{ .strings.lastActiveTime }}</th>
|
||||
</tr>
|
||||
|
||||
49
html/crash.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link inline rel="stylesheet" type="text/css" href="bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>Crash report</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card ~critical sectioned">
|
||||
<section class="section ~critical">
|
||||
<span class="heading">Crash report for jfa-go</span>
|
||||
{{ if .Err }}
|
||||
<div class="monospace pre-line mt-1 mb-1">
|
||||
Error: {{ .Err }}
|
||||
</div>
|
||||
{{ end }}
|
||||
<a class="button ~critical mb-1" target="_blank" href="https://github.com/hrfee/jfa-go/issues/new/choose">Create an Issue</a>
|
||||
</section>
|
||||
<section class="section ~neutral !low">
|
||||
<div class="flex-expand">
|
||||
<span class="subheading">Full Log</span>
|
||||
<span class="button ~urge ml-half" id="copy-log">Copy</span>
|
||||
</div>
|
||||
<div class="row mb-1">
|
||||
<label class="col mr-1">
|
||||
<span class="button ~neutral !high supra full-width center" id="button-log-normal">Normal</span>
|
||||
</label>
|
||||
<label class="col mr-1">
|
||||
<span class="button ~neutral !normal supra full-width center" id="button-log-sanitized">Sanitized</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="log-normal">
|
||||
<pre class="monospace pre-line">{{ .Log }}</pre>
|
||||
</div>
|
||||
<div id="log-sanitized" class="unfocused">
|
||||
<p class="subheading">An attempt has been made to remove sensitive info, but make sure to check yourself.</p>
|
||||
<pre class="monospace pre-line">{{ .SanitizedLog }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script inline src="crash.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,6 +17,18 @@
|
||||
window.telegramEnabled = {{ .telegramEnabled }};
|
||||
window.telegramRequired = {{ .telegramRequired }};
|
||||
window.telegramPIN = "{{ .telegramPIN }}";
|
||||
window.discordEnabled = {{ .discordEnabled }};
|
||||
window.discordRequired = {{ .discordRequired }};
|
||||
window.discordPIN = "{{ .discordPIN }}";
|
||||
window.discordInviteLink = {{ .discordInviteLink }};
|
||||
window.discordServerName = "{{ .discordServerName }}";
|
||||
window.matrixEnabled = {{ .matrixEnabled }};
|
||||
window.matrixRequired = {{ .matrixRequired }};
|
||||
window.matrixUserID = "{{ .matrixUser }}";
|
||||
</script>
|
||||
{{ if .passwordReset }}
|
||||
<script src="js/pwr.js" type="module"></script>
|
||||
{{ else }}
|
||||
<script src="js/form.js" type="module"></script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>{{ .strings.pageTitle }}</title>
|
||||
<title>
|
||||
{{ if .passwordReset }}
|
||||
{{ .strings.passwordReset }}
|
||||
{{ else }}
|
||||
{{ .strings.pageTitle }}
|
||||
{{ end }}
|
||||
</title>
|
||||
</head>
|
||||
<body class="max-w-full overflow-x-hidden section">
|
||||
<div id="modal-success" class="modal">
|
||||
@@ -37,6 +43,35 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .discordEnabled }}
|
||||
<div id="modal-discord" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading mb-1">{{ .strings.linkDiscord }}</span>
|
||||
<p class="content mb-1"> {{ .discordSendPINMessage }}</p>
|
||||
<h1 class="ac">{{ .discordPIN }}</h1>
|
||||
<a id="discord-invite"></a>
|
||||
<span class="button ~info !normal full-width center mt-1" id="discord-waiting">{{ .strings.success }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .matrixEnabled }}
|
||||
<div id="modal-matrix" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading mb-1">{{ .strings.linkMatrix }}</span>
|
||||
<p class="content mb-1"> {{ .strings.matrixEnterUser }}</p>
|
||||
<input type="text" class="input ~neutral !high" placeholder="@user:riot.im" id="matrix-userid">
|
||||
<div class="subheading link-center mt-1">
|
||||
<span class="shield ~info mr-1">
|
||||
<span class="icon">
|
||||
<i class="ri-chat-3-line"></i>
|
||||
</span>
|
||||
</span>
|
||||
{{ .matrixUser }}
|
||||
</div>
|
||||
<span class="button ~info !normal full-width center mt-1" id="matrix-send">{{ .strings.submit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
@@ -51,8 +86,20 @@
|
||||
<div class="page-container">
|
||||
<div class="card ~neutral !low">
|
||||
<div class="row baseline">
|
||||
<span class="col heading">{{ .strings.createAccountHeader }}</span>
|
||||
<span class="col subheading"> {{ .helpMessage }}</span>
|
||||
<span class="col heading">
|
||||
{{ if .passwordReset }}
|
||||
{{ .strings.passwordReset }}
|
||||
{{ else }}
|
||||
{{ .strings.createAccountHeader }}
|
||||
{{ end }}
|
||||
</span>
|
||||
<span class="col subheading">
|
||||
{{ if .passwordReset }}
|
||||
{{ .strings.enterYourPassword }}
|
||||
{{ else }}
|
||||
{{ .helpMessage }}
|
||||
{{ end }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
@@ -60,6 +107,7 @@
|
||||
<aside class="col aside sm ~warning" id="user-expiry-message"></aside>
|
||||
{{ end }}
|
||||
<form class="card ~neutral !normal" id="form-create" href="">
|
||||
{{ if not .passwordReset }}
|
||||
<label class="label supra">
|
||||
{{ .strings.username }}
|
||||
<input type="text" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.username }}" id="create-username" aria-label="{{ .strings.username }}">
|
||||
@@ -69,15 +117,36 @@
|
||||
<input type="email" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.emailAddress }}" id="create-email" aria-label="{{ .strings.emailAddress }}" value="{{ .email }}">
|
||||
{{ if .telegramEnabled }}
|
||||
<span class="button ~info !normal full-width center mb-1" id="link-telegram">{{ .strings.linkTelegram }}</span>
|
||||
{{ end }}
|
||||
{{ if .discordEnabled }}
|
||||
<span class="button ~info !normal full-width center mb-1" id="link-discord">{{ .strings.linkDiscord }}</span>
|
||||
{{ end }}
|
||||
{{ if .matrixEnabled }}
|
||||
<span class="button ~info !normal full-width center mb-1" id="link-matrix">{{ .strings.linkMatrix }}</span>
|
||||
{{ end }}
|
||||
{{ if or (.telegramEnabled) (or .discordEnabled .matrixEnabled) }}
|
||||
<div id="contact-via" class="unfocused">
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="contact-via" value="email"><span>Contact through Email</span>
|
||||
</label>
|
||||
{{ if .telegramEnabled }}
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="contact-via" value="telegram" id="contact-via-telegram"><span>Contact through Telegram</span>
|
||||
</label>
|
||||
{{ end }}
|
||||
{{ if .discordEnabled }}
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="contact-via" value="discord" id="contact-via-discord"><span>Contact through Discord</span>
|
||||
</label>
|
||||
{{ end }}
|
||||
{{ if .matrixEnabled }}
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="contact-via" value="matrix" id="contact-via-matrix"><span>Contact through Matrix</span>
|
||||
</label>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<label class="label supra" for="create-password">{{ .strings.password }}</label>
|
||||
<input type="password" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-password" aria-label="{{ .strings.password }}">
|
||||
|
||||
@@ -85,7 +154,13 @@
|
||||
<input type="password" class="input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="create-reenter-password" aria-label="{{ .strings.reEnterPassword }}">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.createAccountButton }}</span>
|
||||
<span class="button ~urge !normal full-width center supra submit">
|
||||
{{ if .passwordReset }}
|
||||
{{ .strings.reset }}
|
||||
{{ else }}
|
||||
{{ .strings.createAccountButton }}
|
||||
{{ end }}
|
||||
</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
@@ -110,4 +185,3 @@
|
||||
{{ template "form-base" . }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -40,6 +40,6 @@
|
||||
</div>
|
||||
<i class="content">{{ .contactMessage }}</i>
|
||||
</div>
|
||||
<script src="{{ .urlBase }}/js/pwr.js" type="module"></script>
|
||||
<script src="{{ .urlBase }}/js/pwr-pin.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
images/discord/1.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
images/discord/2.jpg
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
images/discord/3.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
images/discord/4.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
images/discord/5.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
images/discord/6.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
images/discord/7.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
images/discord/8.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
images/matrix/1.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
images/matrix/2.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
images/matrix/3.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
images/matrix/4.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/tg-settings.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
images/tg.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
@@ -42,7 +42,7 @@ func (rt *inviteDaemon) run() {
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *inviteDaemon) shutdown() {
|
||||
func (rt *inviteDaemon) Shutdown() {
|
||||
rt.Stopped = true
|
||||
rt.ShutdownChannel <- "Down"
|
||||
<-rt.ShutdownChannel
|
||||
@@ -32,7 +32,7 @@
|
||||
"modifySettings": "Einstellungen ändern",
|
||||
"modifySettingsDescription": "Wende Einstellungen von einem bestehenden Profil an, oder beziehe sie direkt von einem Benutzer.",
|
||||
"applyHomescreenLayout": "Startbildschirmlayout anwenden",
|
||||
"sendDeleteNotificationEmail": "Benachrichtigungs-E-Mail senden",
|
||||
"sendDeleteNotificationEmail": "Benachrichtigung senden",
|
||||
"sendDeleteNotifiationExample": "Dein Konto wurde gelöscht.",
|
||||
"settingsRestartRequired": "Neustart erforderlich",
|
||||
"settingsRestartRequiredDescription": "Ein Neustart ist notwendig, um einige Einstellungen anzuwenden, die du geändert hast. Jetzt oder später neu starten?",
|
||||
@@ -72,7 +72,7 @@
|
||||
"customizeMessages": "E-Mails anpassen",
|
||||
"customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
|
||||
"announce": "Ankündigen",
|
||||
"subject": "E-Mail-Betreff",
|
||||
"subject": "Betreff",
|
||||
"message": "Nachricht",
|
||||
"markdownSupported": "Markdown wird unterstützt.",
|
||||
"advancedSettings": "Erweiterte Einstellungen",
|
||||
@@ -87,7 +87,20 @@
|
||||
"update": "Aktualisieren",
|
||||
"updates": "Aktualisierungen",
|
||||
"expiry": "Ablaufdatum",
|
||||
"extendExpiry": "Ablaufdatum verlängern"
|
||||
"extendExpiry": "Ablaufdatum verlängern",
|
||||
"reEnable": "Wieder aktivieren",
|
||||
"disable": "Deaktivieren",
|
||||
"donate": "Spenden",
|
||||
"conditionals": "Bedingungen",
|
||||
"contactThrough": "Kontakt über:",
|
||||
"sendPIN": "Bitte den Benutzer, die unten stehende PIN an den Bot zu senden.",
|
||||
"inviteMonths": "Monate",
|
||||
"add": "Hinzufügen",
|
||||
"select": "Auswählen",
|
||||
"searchDiscordUser": "Gib den Discord-Benutzername ein, um den Benutzer zu finden.",
|
||||
"findDiscordUser": "Suche Discord-Benutzer",
|
||||
"linkMatrixDescription": "Gib den Benutzernamen und das Passwort des Benutzers ein, der als Bot verwendet werden soll. Nach dem Absenden wird die App neu gestartet.",
|
||||
"matrixHomeServer": "Adresse des Homeservers"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "E-Mail-Adresse von {n} geändert.",
|
||||
@@ -116,7 +129,7 @@
|
||||
"errorFailureCheckLogs": "Fehlgeschlagen (überprüfe die Konsole/Logs)",
|
||||
"errorPartialFailureCheckLogs": "Teilweiser Fehlschlag (überprüfe die Konsole/Logs)",
|
||||
"errorUserCreated": "Fehler beim Erstellen des Benutzers {n}.",
|
||||
"errorSendWelcomeEmail": "Fehler beim Senden der Willkommens-E-Mail (überprüfe die Konsole/Logs)",
|
||||
"errorSendWelcomeEmail": "Fehler beim Senden der Willkommensnachricht (überprüfe die Konsole/Logs)",
|
||||
"saveEmail": "E-Mail gespeichert.",
|
||||
"errorSaveEmail": "Fehler beim Speichern der E-Mail.",
|
||||
"sentAnnouncement": "Ankündigung gesendet.",
|
||||
@@ -124,7 +137,10 @@
|
||||
"errorApplyUpdate": "Fehler beim Anwenden der Aktualisierung, versuche es manuell.",
|
||||
"errorCheckUpdate": "Fehler beim Suchen nach Aktualisierungen.",
|
||||
"updateAvailable": "Eine neue Aktualisierung ist verfügbar, überprüfe die Einstellungen.",
|
||||
"noUpdatesAvailable": "Keinen neuen Aktualisierungen verfügbar."
|
||||
"noUpdatesAvailable": "Keinen neuen Aktualisierungen verfügbar.",
|
||||
"updateAppliedRefresh": "Update angewendet, bitte aktualisieren.",
|
||||
"telegramVerified": "Telegram-Konto verifiziert.",
|
||||
"accountConnected": "Konto verbunden."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
@@ -162,6 +178,22 @@
|
||||
"extendedExpiry": {
|
||||
"singular": "Ablaufdatum für {n} Benutzer verlängern.",
|
||||
"plural": "Ablaufdatum für {n} Benutzer verlängern."
|
||||
},
|
||||
"disabledUser": {
|
||||
"plural": "Benutzer {n} Deaktiviert.",
|
||||
"singular": "Benutzer {n} Deaktiviert."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "Benutzer {n} Aktiviert.",
|
||||
"plural": "Benutzer {n} Aktiviert."
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Benutzer {n} deaktivieren",
|
||||
"plural": "Deaktiviere {n} Benutzer"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Benutzer {n} wieder aktivieren",
|
||||
"plural": "Benutzer {n} wieder aktivieren"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
"create": "Create",
|
||||
"apply": "Apply",
|
||||
"delete": "Delete",
|
||||
"add": "Add",
|
||||
"select": "Select",
|
||||
"name": "Name",
|
||||
"date": "Date",
|
||||
"enabled": "Enabled",
|
||||
@@ -94,7 +96,11 @@
|
||||
"notifyEvent": "Notify on:",
|
||||
"notifyInviteExpiry": "On expiry",
|
||||
"notifyUserCreation": "On user creation",
|
||||
"sendPIN": "Ask the user to send the PIN below to the bot."
|
||||
"sendPIN": "Ask the user to send the PIN below to the bot.",
|
||||
"searchDiscordUser": "Start typing the Discord username to find the user.",
|
||||
"findDiscordUser": "Find Discord user",
|
||||
"linkMatrixDescription": "Enter the username and password of the user to use as a bot. Once submitted, the app will restart.",
|
||||
"matrixHomeServer": "Home server address"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Changed email address of {n}.",
|
||||
@@ -107,6 +113,7 @@
|
||||
"updateApplied": "Update applied, please restart.",
|
||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||
"telegramVerified": "Telegram account verified.",
|
||||
"accountConnected": "Account connected.",
|
||||
"errorConnection": "Couldn't connect to jfa-go.",
|
||||
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||
|
||||
@@ -91,7 +91,8 @@
|
||||
"notifyEvent": "Notificar en:",
|
||||
"notifyInviteExpiry": "Al vencimiento",
|
||||
"notifyUserCreation": "Sobre la creación de usuarios",
|
||||
"conditionals": "Condicionales"
|
||||
"conditionals": "Condicionales",
|
||||
"donate": "Donar"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Se cambió la dirección de correo electrónico de {n}.",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"modifySettings": "Modifier les paramètres",
|
||||
"modifySettingsDescription": "Appliquez les paramètres à partir d'un profil existant ou obtenez-les directement auprès d'un utilisateur.",
|
||||
"applyHomescreenLayout": "Appliquer la disposition de l'écran d'accueil",
|
||||
"sendDeleteNotificationEmail": "Envoyer un e-mail de notification",
|
||||
"sendDeleteNotificationEmail": "Envoyer un message de notification",
|
||||
"sendDeleteNotifiationExample": "Votre compte a été supprimé.",
|
||||
"settingsRestartRequired": "Redémarrage nécessaire",
|
||||
"settingsRestartRequiredDescription": "Un redémarrage est nécessaire pour appliquer certains paramètres que vous avez modifiés. Redémarrer maintenant ou plus tard ?",
|
||||
@@ -90,7 +90,12 @@
|
||||
"update": "Mise à jour",
|
||||
"download": "Téléchargement",
|
||||
"search": "Recherche",
|
||||
"conditionals": "Conditions"
|
||||
"conditionals": "Conditions",
|
||||
"userExpiryDescription": "Un laps de temps spécifié après chaque inscription, jfa-go supprimera / désactivera le compte. Vous pouvez modifier ce comportement dans les paramètres.",
|
||||
"donate": "Faire un don",
|
||||
"extendExpiry": "Prolonger l'expiration",
|
||||
"contactThrough": "Contactez par :",
|
||||
"sendPIN": "Demandez à l'utilisateur d'envoyer le code PIN ci-dessous au bot."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Adresse e-mail modifiée de {n}.",
|
||||
@@ -119,11 +124,17 @@
|
||||
"errorFailureCheckLogs": "Échec (vérifier la console / les journaux)",
|
||||
"errorPartialFailureCheckLogs": "Panne partielle (vérifier la console / les journaux)",
|
||||
"errorUserCreated": "Echec lors de la création de l'utilisateur {n}.",
|
||||
"errorSendWelcomeEmail": "Echec lors de l'envoi du mail de bienvenue (vérifier la console/les journaux)",
|
||||
"errorSendWelcomeEmail": "Echec lors de l'envoi du message de bienvenue (vérifier la console/les journaux)",
|
||||
"sentAnnouncement": "Annonce envoyée.",
|
||||
"saveEmail": "Email enregistré.",
|
||||
"errorSaveEmail": "Échec de l'enregistrement de l'e-mail.",
|
||||
"updateApplied": "Mise à jour appliquée, veuillez redémarrer."
|
||||
"updateApplied": "Mise à jour appliquée, veuillez redémarrer.",
|
||||
"errorApplyUpdate": "Échec de l'application de la mise à jour, essayez manuellement.",
|
||||
"errorCheckUpdate": "Échec de la vérification de la mise à jour.",
|
||||
"updateAvailable": "Une nouvelle mise à jour est disponible, vérifiez les paramètres.",
|
||||
"noUpdatesAvailable": "Aucune nouvelle mise à jour disponible.",
|
||||
"telegramVerified": "Compte Telegram vérifié.",
|
||||
"updateAppliedRefresh": "Mise à jour appliquée, veuillez actualiser."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
@@ -153,6 +164,30 @@
|
||||
"announceTo": {
|
||||
"singular": "Annonce à {n} utilisateur",
|
||||
"plural": "Annonce à {n} utilisateurs"
|
||||
},
|
||||
"enabledUser": {
|
||||
"plural": "{n} utilisateurs activés.",
|
||||
"singular": "{n} utilisateur activé."
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Prolonger l'expiration pour {n} utilisateur",
|
||||
"plural": "Prolonger l'expiration pour {n} utilisateurs"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Expiration prolongée pour {n} utilisateur.",
|
||||
"plural": "Expiration prolongée pour {n} utilisateurs."
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Désactiver {n} utilisateur",
|
||||
"plural": "Désactiver {n} utilisateurs"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Ré-activer {n} utilisateur",
|
||||
"plural": "Ré-activer {n} utilisateurs"
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "{n} utilisateur désactivé.",
|
||||
"plural": "{n} utilisateurs désactivés."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"modifySettings": "Instellingen aanpassen",
|
||||
"modifySettingsDescription": "Pas instellingen van een bestaand profiel toe, of neem ze direct over van een gebruiker.",
|
||||
"applyHomescreenLayout": "Sla startpagina indeling op",
|
||||
"sendDeleteNotificationEmail": "Stuur meldingse-mail",
|
||||
"sendDeleteNotificationEmail": "Stuur melding",
|
||||
"sendDeleteNotifiationExample": "Je account is verwijderd.",
|
||||
"settingsRestartRequired": "Herstart nodig",
|
||||
"settingsRestartRequiredDescription": "Er is een herstart nodig om de wijzigingen door te voeren. Herstart nu of later?",
|
||||
@@ -67,7 +67,7 @@
|
||||
"settingsRestarting": "Aan het herstarten…",
|
||||
"announce": "Aankondiging",
|
||||
"markdownSupported": "Markdown wordt ondersteund.",
|
||||
"subject": "E-mailonderwerp",
|
||||
"subject": "Onderwerp",
|
||||
"message": "Bericht",
|
||||
"variables": "Variabelen",
|
||||
"customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
|
||||
@@ -91,7 +91,16 @@
|
||||
"inviteMonths": "Maanden",
|
||||
"reEnable": "Opnieuw inschakelen",
|
||||
"disable": "Uitschakelen",
|
||||
"conditionals": "Voorwaarden"
|
||||
"conditionals": "Voorwaarden",
|
||||
"donate": "Doneer",
|
||||
"contactThrough": "Stuur bericht via:",
|
||||
"sendPIN": "Vraag de gebruiker om onderstaande pincode naar de bot te sturen.",
|
||||
"add": "Voeg toe",
|
||||
"searchDiscordUser": "Begin de Discord gebruikersnaam te typen om de gebruiker te vinden.",
|
||||
"linkMatrixDescription": "Vul de gebruikersnaam en wachtwoord in van de gebruiker om als bot te gebruiken. De app start zodra ze zijn verstuurd.",
|
||||
"select": "Selecteer",
|
||||
"findDiscordUser": "Zoek Discord gebruiker",
|
||||
"matrixHomeServer": "Adres home server"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "E-mailadres van {n} gewijzigd.",
|
||||
@@ -119,7 +128,7 @@
|
||||
"errorChangedEmailAddress": "Wijzigen van e-mailadres van {n} mislukt.",
|
||||
"errorFailureCheckLogs": "Mislukt (controleer console/logbestanden)",
|
||||
"errorPartialFailureCheckLogs": "Gedeeltelijke fout (controleer console/logbestanden)",
|
||||
"errorSendWelcomeEmail": "Versturen van welkomste-mail is mislukt (zie console/logs)",
|
||||
"errorSendWelcomeEmail": "Versturen van welkomstbericht is mislukt (zie console/logs)",
|
||||
"errorUserCreated": "Aanmaken van gebruiker {n} is mislukt.",
|
||||
"sentAnnouncement": "Aankondiging verzonden.",
|
||||
"saveEmail": "E-mail opgeslagen.",
|
||||
@@ -128,7 +137,10 @@
|
||||
"errorApplyUpdate": "Installatie van update mislukt, probeer handmatig.",
|
||||
"errorCheckUpdate": "Controleren op update mislukt.",
|
||||
"updateAvailable": "Er is een nieuwe update beschikbaar, kijk bij instellingen.",
|
||||
"noUpdatesAvailable": "Geen nieuwe updates beschikbaar."
|
||||
"noUpdatesAvailable": "Geen nieuwe updates beschikbaar.",
|
||||
"telegramVerified": "Telegram-account goedgekeurd.",
|
||||
"updateAppliedRefresh": "Update toegepast, ververs alsjeblieft.",
|
||||
"accountConnected": "Account gekoppeld."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"modifySettings": "Modificar configurações",
|
||||
"modifySettingsDescription": "Aplique as configurações de um perfil existente ou obtenha-as diretamente de um usuário.",
|
||||
"applyHomescreenLayout": "Aplicar layout na tela inicial",
|
||||
"sendDeleteNotificationEmail": "Enviar email de notificação",
|
||||
"sendDeleteNotificationEmail": "Enviar mensagem de notificação",
|
||||
"sendDeleteNotifiationExample": "Sua conta foi deletada.",
|
||||
"settingsRestartRequired": "Necessário reiniciar",
|
||||
"settingsRestartRequiredDescription": "É necessário reiniciar para aplicar algumas configurações alteradas. Deseja reiniciar agora ou mais tarde?",
|
||||
@@ -66,7 +66,7 @@
|
||||
"settingsRestart": "Reiniciar",
|
||||
"settingsRestarting": "Reiniciando…",
|
||||
"announce": "Anunciar",
|
||||
"subject": "Assunto do email",
|
||||
"subject": "Assunto",
|
||||
"message": "Mensagem",
|
||||
"markdownSupported": "Suporte a Markdown.",
|
||||
"customizeMessagesDescription": "Se não quiser usar os modelos de email do jfa-go, você pode criar o seu próprio usando o Markdown.",
|
||||
@@ -91,7 +91,15 @@
|
||||
"inviteMonths": "Meses",
|
||||
"reEnable": "Reativar",
|
||||
"disable": "Desativar",
|
||||
"conditionals": "Condicionais"
|
||||
"conditionals": "Condicionais",
|
||||
"donate": "Doar",
|
||||
"contactThrough": "Contato através:",
|
||||
"sendPIN": "Peça ao usuário para enviar o PIN abaixo para o bot.",
|
||||
"searchDiscordUser": "Digite o nome de usuário do Discord.",
|
||||
"findDiscordUser": "Encontrar usuário Discord",
|
||||
"add": "Adicionar",
|
||||
"linkMatrixDescription": "Digite o nome de usuário e a senha para usar como bot. Depois de enviado, o aplicativo será reiniciado.",
|
||||
"select": "Selecionar"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Endereço de e-mail alterado de {n}.",
|
||||
@@ -120,7 +128,7 @@
|
||||
"errorFailureCheckLogs": "Falha (verificar console/logs)",
|
||||
"errorPartialFailureCheckLogs": "Falha parcial (verificar console/logs)",
|
||||
"errorUserCreated": "Falha ao criar o usuário {n}.",
|
||||
"errorSendWelcomeEmail": "Falha ao enviar e-mail de boas-vindas (verifique console/logs)",
|
||||
"errorSendWelcomeEmail": "Falha ao enviar mensagem de boas-vindas (verifique console/logs)",
|
||||
"sentAnnouncement": "Comunicado enviado.",
|
||||
"saveEmail": "Email salvo.",
|
||||
"errorSaveEmail": "Falha ao salvar o email.",
|
||||
@@ -128,7 +136,10 @@
|
||||
"errorApplyUpdate": "Falha ao aplicar a atualização, tente manualmente.",
|
||||
"updateAvailable": "Uma nova atualização está disponível, verifique as configurações.",
|
||||
"errorCheckUpdate": "Falha ao verificar atualizações.",
|
||||
"noUpdatesAvailable": "Nenhuma atualização disponível."
|
||||
"noUpdatesAvailable": "Nenhuma atualização disponível.",
|
||||
"telegramVerified": "Conta do Telegram verificada.",
|
||||
"updateAppliedRefresh": "Atualização instalada, atualize.",
|
||||
"accountConnected": "Conta conectada."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
"theme": "Thema",
|
||||
"time24h": "24h-Format",
|
||||
"time12h": "12h-Format",
|
||||
"copied": "Kopiert"
|
||||
"copied": "Kopiert",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Kontakt über E-Mail",
|
||||
"contactTelegram": "Kontakt über Telegram",
|
||||
"linkDiscord": "Link Discord",
|
||||
"linkMatrix": "Link Matrix",
|
||||
"send": "Senden",
|
||||
"contactDiscord": "Kontakt über Discord"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"emailAddress": "Email Address",
|
||||
"name": "Name",
|
||||
"submit": "Submit",
|
||||
"send": "Send",
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"copy": "Copy",
|
||||
@@ -17,6 +18,9 @@
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Contact through Email",
|
||||
"contactTelegram": "Contact through Telegram",
|
||||
"linkDiscord": "Link Discord",
|
||||
"linkMatrix": "Link Matrix",
|
||||
"contactDiscord": "Contact through Discord",
|
||||
"theme": "Theme"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"copy": "Copier",
|
||||
"time24h": "Temps 24h",
|
||||
"time12h": "Temps 12h",
|
||||
"theme": "Thème"
|
||||
"theme": "Thème",
|
||||
"copied": "Copié",
|
||||
"linkTelegram": "Lien Telegram",
|
||||
"contactEmail": "Contact par e-mail",
|
||||
"contactTelegram": "Contact par Telegram"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,19 @@
|
||||
"password": "Wachtwoord",
|
||||
"emailAddress": "E-mailadres",
|
||||
"submit": "Verstuur",
|
||||
"success": "Success",
|
||||
"success": "Succes",
|
||||
"error": "Fout",
|
||||
"copy": "Kopiëer",
|
||||
"theme": "Thema",
|
||||
"time24h": "24u-formaat",
|
||||
"time12h": "12u-formaat",
|
||||
"copied": "Gekopieerd"
|
||||
"copied": "Gekopieerd",
|
||||
"linkTelegram": "Koppel Telegram",
|
||||
"contactEmail": "Stuur e-mailbericht",
|
||||
"contactTelegram": "Stuur Telegram-bericht",
|
||||
"send": "Verstuur",
|
||||
"linkDiscord": "Koppel Discord",
|
||||
"linkMatrix": "Koppel Matrix",
|
||||
"contactDiscord": "Stuur Discord bericht"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
"theme": "Tema",
|
||||
"time24h": "Horário 24h",
|
||||
"time12h": "Horário 12h",
|
||||
"copied": "Copiado"
|
||||
"copied": "Copiado",
|
||||
"linkTelegram": "Link do Telegram",
|
||||
"contactEmail": "Contato por Email",
|
||||
"contactTelegram": "Contato pelo Telegram",
|
||||
"send": "Enviar",
|
||||
"linkDiscord": "Link do Discord",
|
||||
"linkMatrix": "Link do Matrix",
|
||||
"contactDiscord": "Contato através do Discord"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Deutsch (DE)"
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Wenn du das nicht warst, ignoriere bitte diese E-Mail.",
|
||||
"ifItWasNotYou": "Wenn du das nicht warst, ignoriere bitte dies.",
|
||||
"reason": "Grund",
|
||||
"helloUser": "Hallo {username},"
|
||||
},
|
||||
@@ -27,7 +27,8 @@
|
||||
"ifItWasYou": "Wenn du das warst, gib die PIN unten in die Eingabeaufforderung ein.",
|
||||
"codeExpiry": "Der Code wird am {date}, um {time} UTC ablaufen, was in {expiresInMinutes} ist.",
|
||||
"pin": "PIN",
|
||||
"name": "Passwortzurücksetzung"
|
||||
"name": "Passwortzurücksetzung",
|
||||
"ifItWasYouLink": "Wenn du das warst, klick auf den Link unten."
|
||||
},
|
||||
"userDeleted": {
|
||||
"title": "Dein Konto wurde gelöscht - Jellyfin",
|
||||
@@ -48,7 +49,8 @@
|
||||
"welcome": "Willkommen bei Jellyfin!",
|
||||
"youCanLoginWith": "Du kannst dich mit den mit den untenstehenden Zugangsdaten anmelden",
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Willkommens-E-Mail"
|
||||
"name": "Willkommen",
|
||||
"yourAccountWillExpire": "Dein Konto läuft am {date} ab."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
"title": "Bestätige deine E-Mail - Jellyfin",
|
||||
@@ -61,5 +63,15 @@
|
||||
"title": "Dein Konto ist abgelaufen - Jellyfin",
|
||||
"yourAccountHasExpired": "Dein Konto ist abgelaufen.",
|
||||
"contactTheAdmin": "Kontaktiere den Administrator für weitere Informationen."
|
||||
},
|
||||
"userDisabled": {
|
||||
"name": "Benutzer deaktiviert",
|
||||
"title": "Dein Konto wurde deaktiviert - Jellyfin",
|
||||
"yourAccountWasDisabled": "Dein Konto wurde deaktiviert."
|
||||
},
|
||||
"userEnabled": {
|
||||
"name": "Benutzer aktiviert",
|
||||
"title": "Dein Konto wurde wieder freigeschaltet - Jellyfin",
|
||||
"yourAccountWasEnabled": "Dein Konto wurde wieder aktiviert."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"author": "https://github.com/Cornichon420"
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Si ce n'était pas toi, tu peux ignorer ce mail.",
|
||||
"ifItWasNotYou": "Si ce n'était pas toi, tu peux ignorer ceci.",
|
||||
"reason": "Motif",
|
||||
"helloUser": "Salut {username},"
|
||||
},
|
||||
@@ -50,7 +50,7 @@
|
||||
"title": "Bienvenue sur Jellyfin",
|
||||
"welcome": "Bienvenue sur Jellyfin !",
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Courriel de bienvenue",
|
||||
"name": "Bienvenue",
|
||||
"yourAccountWillExpire": "Ton compte expirera le {date}."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Nederlands (NL)"
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Als jij dit niet was, negeer dan alsjeblieft deze email.",
|
||||
"ifItWasNotYou": "Als jij dit niet was, negeer dit dan alsjeblieft.",
|
||||
"reason": "Reden",
|
||||
"helloUser": "Hoi {username},"
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
"welcome": "Welkom bij Jellyfin!",
|
||||
"youCanLoginWith": "Je kunt inloggen met onderstaande gegevens",
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Welkomste-mail",
|
||||
"name": "Welkom",
|
||||
"yourAccountWillExpire": "Je account verloopt op {date}."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Português (BR)"
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Se não foi você, ignore este e-mail.",
|
||||
"ifItWasNotYou": "Se não foi você, ignore.",
|
||||
"reason": "Razão",
|
||||
"helloUser": "Ola {username},"
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
"welcome": "Bem vindo ao Jellyfin!",
|
||||
"youCanLoginWith": "Abaixo está os detalhes para fazer o login",
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Email de Boas vindas",
|
||||
"name": "Bem-vindo",
|
||||
"yourAccountWillExpire": "Sua conta irá expirar em {date}."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
"successContinueButton": "Weiter",
|
||||
"confirmationRequired": "E-Mail-Bestätigung erforderlich",
|
||||
"confirmationRequiredMessage": "Bitte überprüfe dein Posteingang und bestätige deine E-Mail-Adresse.",
|
||||
"yourAccountIsValidUntil": "Dein Konto wird bis zum {date} gültig sein."
|
||||
"yourAccountIsValidUntil": "Dein Konto wird bis zum {date} gültig sein.",
|
||||
"sendPIN": "Sende die untenstehende PIN an den Bot und komm dann hierher zurück, um dein Konto zu verbinden.",
|
||||
"sendPINDiscord": "Gib auf Discord {command} in {server_channel} ein und sende die untenstehende PIN als DM an den Bot.",
|
||||
"matrixEnterUser": "Gib deine Benutzer-ID ein und drücke auf Absenden. Anschließend erhälst du ein PIN, die hier eingegeben wird um fortzufahren."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
@@ -43,6 +46,13 @@
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "Benutzer existiert bereits.",
|
||||
"errorInvalidCode": "Ungültiger Invite-Code."
|
||||
"errorInvalidCode": "Ungültiger Invite-Code.",
|
||||
"telegramVerified": "Telegram-Konto verifiziert.",
|
||||
"errorTelegramVerification": "Verifizierung von Telegram erforderlich.",
|
||||
"errorInvalidPIN": "PIN ist ungültig.",
|
||||
"errorDiscordVerification": "Discord-Verifizierung erforderlich.",
|
||||
"errorMatrixVerification": "Matrix-Verifizierung erforderlich.",
|
||||
"errorUnknown": "Unbekannter Fehler.",
|
||||
"verified": "Konto verifiziert."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,19 @@
|
||||
"confirmationRequired": "Email confirmation required",
|
||||
"confirmationRequiredMessage": "Please check your email inbox to verify your address.",
|
||||
"yourAccountIsValidUntil": "Your account will be valid until {date}.",
|
||||
"sendPIN": "Send the PIN below to the bot, then come back here to link your account."
|
||||
"sendPIN": "Send the PIN below to the bot, then come back here to link your account.",
|
||||
"sendPINDiscord": "Type {command} in {server_channel} on Discord, then send the PIN below via DM to the bot.",
|
||||
"matrixEnterUser": "Enter your User ID, press submit, and a PIN will be sent to you. Enter it here to continue."
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "User already exists.",
|
||||
"errorInvalidCode": "Invalid invite code.",
|
||||
"errorTelegramVerification": "Telegram verification required.",
|
||||
"errorInvalidPIN": "Telegram PIN is invalid.",
|
||||
"telegramVerified": "Telegram account verified."
|
||||
"errorDiscordVerification": "Discord verification required.",
|
||||
"errorMatrixVerification": "Matrix verification required.",
|
||||
"errorInvalidPIN": "PIN is invalid.",
|
||||
"errorUnknown": "Unknown error.",
|
||||
"verified": "Account verified."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
"successHeader": "Succes!",
|
||||
"successContinueButton": "Continuer",
|
||||
"confirmationRequired": "Confirmation de l'adresse e-mail requise",
|
||||
"confirmationRequiredMessage": "Veuillez vérifier votre boite de réception pour confirmer votre adresse e-mail."
|
||||
"confirmationRequiredMessage": "Veuillez vérifier votre boite de réception pour confirmer votre adresse e-mail.",
|
||||
"yourAccountIsValidUntil": "Votre compte sera valide jusqu'au {date}.",
|
||||
"sendPIN": "Envoyez le code PIN ci-dessous au bot, puis revenez ici pour lier votre compte."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
@@ -43,6 +45,9 @@
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "Utilisateur déjà existant.",
|
||||
"errorInvalidCode": "Code d’invitation non valide."
|
||||
"errorInvalidCode": "Code d’invitation non valide.",
|
||||
"errorTelegramVerification": "Vérification Telegram requise.",
|
||||
"errorInvalidPIN": "PIN Telegram invalide.",
|
||||
"telegramVerified": "Compte Telegram vérifié."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
"successContinueButton": "Doorgaan",
|
||||
"confirmationRequired": "Bevestiging van e-mailadres verplicht",
|
||||
"confirmationRequiredMessage": "Controleer je e-mail inbox om je adres te bevestigen.",
|
||||
"yourAccountIsValidUntil": "Je account zal geldig zijn tot {date}."
|
||||
"yourAccountIsValidUntil": "Je account zal geldig zijn tot {date}.",
|
||||
"sendPIN": "Stuur onderstaande pincode naar de bot, en kom daarna hier terug om je account te koppelen.",
|
||||
"matrixEnterUser": "Voer je gebruikers ID in, druk op versturen, en er wordt je een pincode toegestuurd. Vul die hier in om door te gaan.",
|
||||
"sendPINDiscord": "Typ {command} in {server_channel} op Discord, stuur daarna onderstaande pincode via DM naar de bot."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
@@ -43,6 +46,13 @@
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "Gebruiker bestaat al.",
|
||||
"errorInvalidCode": "Ongeldige uitnodigingscode."
|
||||
"errorInvalidCode": "Ongeldige uitnodigingscode.",
|
||||
"telegramVerified": "Telegram-account goedgekeurd.",
|
||||
"errorTelegramVerification": "Telegram-verificatie nodig.",
|
||||
"errorInvalidPIN": "Pincode is ongeldig.",
|
||||
"errorDiscordVerification": "Discord-verificatie vereist.",
|
||||
"errorUnknown": "Onbekende fout.",
|
||||
"errorMatrixVerification": "Matrix-verificatie vereist.",
|
||||
"verified": "Account geverifieerd."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,23 @@
|
||||
"passwordRequirementsHeader": "Requisitos da Senha",
|
||||
"successHeader": "Sucesso!",
|
||||
"successContinueButton": "Continuar",
|
||||
"confirmationRequired": "Necessária confirmação de e-mail",
|
||||
"confirmationRequired": "Confirmação por e-mail",
|
||||
"confirmationRequiredMessage": "Verifique sua caixa de email para finalizar o cadastro.",
|
||||
"yourAccountIsValidUntil": "Sua conta é válida até {date}."
|
||||
"yourAccountIsValidUntil": "Sua conta é válida até {date}.",
|
||||
"sendPIN": "Envie o PIN abaixo para o bot e volte aqui para vincular sua conta.",
|
||||
"sendPINDiscord": "Digite {command} em {server_channel} no Discord e envie o PIN abaixo via DM para o bot.",
|
||||
"matrixEnterUser": "Digite sua ID de usuário, pressione enviar e um PIN será enviado. E digite aqui para continuar."
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "Esse usuário já existe.",
|
||||
"errorInvalidCode": "Código do convite invalido."
|
||||
"errorInvalidCode": "Código do convite invalido.",
|
||||
"telegramVerified": "Conta do Telegram verificada.",
|
||||
"errorInvalidPIN": "PIN inválido.",
|
||||
"errorTelegramVerification": "Requer a verificação do telegram.",
|
||||
"errorDiscordVerification": "Necessária verificação pelo Discord.",
|
||||
"errorMatrixVerification": "Necessária verificação Matrix.",
|
||||
"errorUnknown": "Erro desconhecido.",
|
||||
"verified": "Conta verificada."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
|
||||
@@ -17,11 +17,15 @@
|
||||
"successContinueButton": "Fortsätt",
|
||||
"confirmationRequired": "E-postbekräftelse krävs",
|
||||
"confirmationRequiredMessage": "Kontrollera din e-postkorg för att verifiera din adress.",
|
||||
"yourAccountIsValidUntil": "Ditt konto är giltigt fram tills {date}."
|
||||
"yourAccountIsValidUntil": "Ditt konto är giltigt fram tills {date}.",
|
||||
"sendPIN": "Skicka PIN-koden nedan till botten, återvänd sedan till den här sidan för att länka ditt konto."
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "Användare finns redan.",
|
||||
"errorInvalidCode": "Ogiltig inbjudningskod."
|
||||
"errorInvalidCode": "Ogiltig inbjudningskod.",
|
||||
"errorInvalidPIN": "Telegram-PIN är ogiltig.",
|
||||
"errorTelegramVerification": "Telegram-verifiering krävs.",
|
||||
"telegramVerified": "Telegram-konto verifierades."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
|
||||
13
lang/pwreset/de-DE.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Deutsch (DE)"
|
||||
},
|
||||
"strings": {
|
||||
"passwordReset": "Passwort zurücksetzen",
|
||||
"resetFailed": "Zurücksetzen des Passworts fehlgeschlagen",
|
||||
"tryAgain": "Bitte versuche es erneut.",
|
||||
"youCanLogin": "Du kannst dich nun mit dem unten stehenden Code als Passwort anmelden.",
|
||||
"youCanLoginOmbi": "Du kannst dich jetzt bei Jellyfin und Ombi mit dem unten stehenden Code als Passwort anmelden.",
|
||||
"changeYourPassword": "Achte darauf, dass du dein Passwort nach der Anmeldung änderst."
|
||||
}
|
||||
}
|
||||
13
lang/pwreset/en-GB.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": ""
|
||||
},
|
||||
"strings": {
|
||||
"passwordReset": "",
|
||||
"resetFailed": "",
|
||||
"tryAgain": "",
|
||||
"youCanLogin": "",
|
||||
"youCanLoginOmbi": "",
|
||||
"changeYourPassword": ""
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@
|
||||
},
|
||||
"strings": {
|
||||
"passwordReset": "Password reset",
|
||||
"reset": "Reset",
|
||||
"resetFailed": "Password reset failed",
|
||||
"tryAgain": "Please try again.",
|
||||
"youCanLogin": "You can now log in with the below code as your password.",
|
||||
"youCanLoginOmbi": "You can now log in to Jellyfin & Ombi with the below code as your password.",
|
||||
"changeYourPassword": "Make sure to change your password after you log in."
|
||||
"changeYourPassword": "Make sure to change your password after you log in.",
|
||||
"enterYourPassword": "Enter your new password below."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"resetFailed": "Error al cambiar contraseña",
|
||||
"tryAgain": "Por favor intente nuevamente.",
|
||||
"youCanLogin": "Ahora puedes logearte con el codigo como contraseña.",
|
||||
"changeYourPassword": "Recuerda cambiar tu contraseña luego de iniciar sesión."
|
||||
"changeYourPassword": "Recuerda cambiar tu contraseña luego de iniciar sesión.",
|
||||
"youCanLoginOmbi": "Ahora puede iniciar sesión en Jellyfin & Ombi con el siguiente código como contraseña."
|
||||
}
|
||||
}
|
||||
|
||||
13
lang/pwreset/pt-BR.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Português (BR)"
|
||||
},
|
||||
"strings": {
|
||||
"passwordReset": "Redefinir senha",
|
||||
"resetFailed": "Falha ao redefinir a senha",
|
||||
"tryAgain": "Tente novamente.",
|
||||
"youCanLogin": "Agora você pode fazer login com o código abaixo como senha.",
|
||||
"youCanLoginOmbi": "Agora você pode fazer login no Jellyfin & Ombi com o código abaixo como senha.",
|
||||
"changeYourPassword": "Certifique-se de alterar sua senha depois de fazer o login."
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,10 @@
|
||||
"title": "Passwortzurücksetzungen",
|
||||
"description": "Wenn ein Benutzer versucht sein Passwort zurückzusetzen, erstellt Jellyfin eine Datei namens 'passwordreset-*.json', welche eine PIN enthält. jfa-go liest die Datei und sendet die PIN an den Benutzer.",
|
||||
"pathToJellyfin": "Pfad zum Jellyfin-Konfigurationsverzeichnis",
|
||||
"pathToJellyfinNotice": "Wenn du nicht weißt, wo das ist, versuche dein Passwort in Jellyfin zurückzusetzen. Ein Popup mit '<Pfad zu Jellyfin>/passwordreset-*.json' wird angezeigt werden."
|
||||
"pathToJellyfinNotice": "Wenn du nicht weißt, wo das ist, versuche dein Passwort in Jellyfin zurückzusetzen. Ein Popup mit '<Pfad zu Jellyfin>/passwordreset-*.json' wird angezeigt werden.",
|
||||
"resetLinks": "Einen Link statt einer PIN senden",
|
||||
"resetLinksNotice": "Wenn die Ombi-Integration aktiviert ist, verwende dies, um zurückgesetzte Passwörter von Jellyfin mit Ombi zu synchronisieren.",
|
||||
"resetLinksLanguage": "Standardsprache für den Link zum Zurücksetzen des Passworts"
|
||||
},
|
||||
"passwordValidation": {
|
||||
"title": "Passwortüberprüfung",
|
||||
|
||||
@@ -108,7 +108,10 @@
|
||||
"title": "Restablecimiento de contraseña",
|
||||
"description": "Cuando un usuario intenta restablecer su contraseña, Jellyfin crea un archivo llamado 'passwordreset - *. Json' que contiene un PIN. jfa-go lee el archivo y envía el PIN al usuario.",
|
||||
"pathToJellyfin": "Ruta al directorio de configuración de Jellyfin",
|
||||
"pathToJellyfinNotice": "Si no sabe dónde está, intente restablecer su contraseña en Jellyfin. Aparecerá una ventana emergente con '<ruta a jellyfin>/passwordreset-. Json'."
|
||||
"pathToJellyfinNotice": "Si no sabe dónde está, intente restablecer su contraseña en Jellyfin. Aparecerá una ventana emergente con '<ruta a jellyfin>/passwordreset-. Json'.",
|
||||
"resetLinks": "Envía un enlace en lugar de un PIN",
|
||||
"resetLinksNotice": "Si la integración de Ombi está habilitada, utilícela para sincronizar los restablecimientos de contraseña de Jellyfin con Ombi.",
|
||||
"resetLinksLanguage": "Enlace de restablecimiento predeterminado"
|
||||
},
|
||||
"passwordValidation": {
|
||||
"title": "Validación de contraseña",
|
||||
|
||||
@@ -101,7 +101,10 @@
|
||||
"title": "Réinitialisation de mot de passe",
|
||||
"description": "Lorsqu'un utilisateur essaie de réinitialiser son mot de passe, Jellyfin créé un fichier nommé 'passwordreset-*.json' qui contient le code PIN. jfa-go lit le fichier et envoie le code PIN à l'utilisateur.",
|
||||
"pathToJellyfin": "Chemin du dossier de configuration de Jellyfin",
|
||||
"pathToJellyfinNotice": "Si vous ne savez pas où c'est, essayez de réinitialiser votre mot de passe dans Jellyfin. Une popup avec '<path to jellyfin>/passwordreset-*.json' apparaitra."
|
||||
"pathToJellyfinNotice": "Si vous ne savez pas où c'est, essayez de réinitialiser votre mot de passe dans Jellyfin. Une popup avec '<path to jellyfin>/passwordreset-*.json' apparaitra.",
|
||||
"resetLinks": "Envoyer un lien plutôt qu'un PIN",
|
||||
"resetLinksNotice": "Si l'intégration est activée, utilisez ceci pour synchroniser les réinitialisations de mots de passe Jellyfin avec Ombi.",
|
||||
"resetLinksLanguage": "Langue du lien de réinitialisation par défaut"
|
||||
},
|
||||
"passwordValidation": {
|
||||
"title": "Validation du mot de passe",
|
||||
@@ -123,5 +126,12 @@
|
||||
"successMessageNotice": "S'affiche lorsqu'un utilisateur crée son compte.",
|
||||
"emailMessage": "Message de l'e-mail",
|
||||
"emailMessageNotice": "S'affiche au bas des e-mails."
|
||||
},
|
||||
"updates": {
|
||||
"title": "Mises à jour",
|
||||
"description": "Activez pour être averti lorsque de nouvelles mises à jour sont disponibles. jfa-go vérifiera {n} toutes les 30 minutes. Aucune adresse IP ou information personnellement identifiable n'est collectée.",
|
||||
"updateChannel": "Mettre à jour la chaîne",
|
||||
"stable": "Stable",
|
||||
"unstable": "Instable"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,12 +69,12 @@
|
||||
},
|
||||
"ombi": {
|
||||
"title": "Ombi",
|
||||
"description": "Bij verbinding met Ombi, worden voor nieuwe gebruikers via jfa-go zowel een Jellyfin als een Ombi account aangemaakt. Ga nadat de setup voltooid is naar instellingen om een standaardprofiel voor nieuwe Ombi-gebruikers te kiezen.",
|
||||
"description": "Bij verbinding met Ombi, wordt voor nieuwe gebruikers via jfa-go zowel een Jellyfin als een Ombi account aangemaakt. Ga nadat de setup voltooid is naar instellingen om een standaardprofiel voor nieuwe Ombi-gebruikers te kiezen.",
|
||||
"apiKeyNotice": "Te vinden op het eerste tabblad van de Ombi-instellingen."
|
||||
},
|
||||
"email": {
|
||||
"title": "E-mail",
|
||||
"description": "jfa-go kan wachtwoord reset PINs en meldingen via e-mail versturen. Je kunt verbinding maken met een SMTP server, of de {n} API gebruiken.",
|
||||
"description": "jfa-go kan wachtwoord reset pincodes en meldingen via e-mail versturen. Je kunt verbinding maken met een SMTP server, of de {n} API gebruiken.",
|
||||
"method": "Verstuurmethode",
|
||||
"useEmailAsUsername": "Gebruik e-mailadres als gebruikersnaam",
|
||||
"useEmailAsUsernameNotice": "Indien ingeschakeld wordt voor nieuwe Jellyfin/Emby-gebruikers hun e-mailadres in plaats van gebruikersnaam gebruikt.",
|
||||
@@ -99,9 +99,12 @@
|
||||
},
|
||||
"passwordResets": {
|
||||
"title": "Wachtwoordresets",
|
||||
"description": "Wanneer een gebruiker een wachtwoordreset aanvraagt, maakt Jellyfin een bestand aan dat 'passwordreset-*.json' heet en een PIN bevat. jfa-go leest dit bestand uit en stuurt de PIN naar de gebruiker.",
|
||||
"description": "Wanneer een gebruiker een wachtwoordreset aanvraagt, maakt Jellyfin een bestand aan dat 'passwordreset-*.json' heet en een pincode bevat. jfa-go leest dit bestand uit en stuurt de pincode naar de gebruiker.",
|
||||
"pathToJellyfin": "Pad naar Jellyfin configuratiemap",
|
||||
"pathToJellyfinNotice": "Als je niet weet waar dit is, probeer de je wachtwoord te resetten in Jellyfin. Er verschijnt dan een popup met '<path to jellyfin>/passwordreset-*.json'."
|
||||
"pathToJellyfinNotice": "Als je niet weet waar dit is, probeer de je wachtwoord te resetten in Jellyfin. Er verschijnt dan een popup met '<path to jellyfin>/passwordreset-*.json'.",
|
||||
"resetLinks": "Stuur een link in plaats van een pincode",
|
||||
"resetLinksNotice": "Als Ombi-integratie is ingeschakeld, gebruik dan dit om Jellyfin wachtwoordresets te synchroniseren met Ombi.",
|
||||
"resetLinksLanguage": "Standaard reset-link taal"
|
||||
},
|
||||
"passwordValidation": {
|
||||
"title": "Wachtwoordvalidatie",
|
||||
|
||||
@@ -101,7 +101,10 @@
|
||||
"title": "Redefinir Senha",
|
||||
"description": "Quando um usuário tenta redefinir sua senha, o Jellyfin cria um arquivo chamado 'passwordreset-*.json' que contém um PIN. jfa-go lê o arquivo e envia o PIN ao usuário.",
|
||||
"pathToJellyfin": "Local do diretório de configuração do Jellyfin",
|
||||
"pathToJellyfinNotice": "Se você não sabe o local onde fica, tente redefinir sua senha no Jellyfin. Um pop-up com '<path to jellyfin>/passwordreset-*.json' aparecerá."
|
||||
"pathToJellyfinNotice": "Se você não sabe o local onde fica, tente redefinir sua senha no Jellyfin. Um pop-up com '<path to jellyfin>/passwordreset-*.json' aparecerá.",
|
||||
"resetLinks": "Envie um link em vez de um PIN",
|
||||
"resetLinksNotice": "Se a integração do Ombi estiver habilitada, use para sincronizar as redefinições de senha do Jellyfin com o Ombi.",
|
||||
"resetLinksLanguage": "Idioma do link de redefinição padrão"
|
||||
},
|
||||
"passwordValidation": {
|
||||
"title": "Validar Senha",
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
},
|
||||
"ombi": {
|
||||
"title": "Ombi",
|
||||
"description": "Genom att ansluta till Ombi skapas både ett Jellyfin- och Ombi-konto när en användare går med genom jfa-go. Efter installationen, om du är klar, gå till Inställningar för att ställa in en standardprofil för nya ombi-användare.",
|
||||
"description": "Genom att ansluta till Ombi skapas både ett Jellyfin- och Ombi-konto när en användare går med genom jfa-go. Efter installationen är klar, gå till Inställningar för att ställa in en standardprofil för nya ombi-användare.",
|
||||
"apiKeyNotice": "Hitta detta i den första fliken i Ombi-inställningarna."
|
||||
},
|
||||
"email": {
|
||||
@@ -101,7 +101,10 @@
|
||||
"title": "Lösenordsåterställningar",
|
||||
"description": "När en användare försöker återställa sitt lösenord skapar Jellyfin en fil med namnet 'passwordreset-*.json' som innehåller en PIN-kod. jfa-go läser filen och skickar PIN-koden till användaren.",
|
||||
"pathToJellyfin": "Sökväg till Jellyfin-konfigurationskatalogen",
|
||||
"pathToJellyfinNotice": "Om du inte vet var det är, försök återställa lösenordet i Jellyfin. En popup med '<sökväg till jellyfin>/passwordreset-*.json' kommer då visas."
|
||||
"pathToJellyfinNotice": "Om du inte vet var det är, försök återställa lösenordet i Jellyfin. En popup med '<sökväg till jellyfin>/passwordreset-*.json' kommer då visas.",
|
||||
"resetLinks": "Skicka en länk istället för en PIN",
|
||||
"resetLinksNotice": "Om Ombi-integrationen är aktiverad, använd den för att synkronisera lösenordsåterställningar med Ombi.",
|
||||
"resetLinksLanguage": "Förvalt språk för återställningslänk"
|
||||
},
|
||||
"passwordValidation": {
|
||||
"title": "Validering av lösenord",
|
||||
@@ -123,5 +126,12 @@
|
||||
"successMessageNotice": "Visas när en användare skapar sitt konto.",
|
||||
"emailMessage": "E-postmeddelande",
|
||||
"emailMessageNotice": "Visas längst ner i e-postmeddelanden."
|
||||
},
|
||||
"updates": {
|
||||
"title": "Uppdateringar",
|
||||
"updateChannel": "Uppdateringskanal",
|
||||
"description": "Aktivera för att få notifikation när nya uppdateringar finns tillgängliga. jfa-go kollar {n} var 30:e minut. Inga IP-adresser eller personligt identifierande handlingar samlas in.",
|
||||
"stable": "Stabil",
|
||||
"unstable": "Ostabil"
|
||||
}
|
||||
}
|
||||
|
||||
12
lang/telegram/de-DE.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Deutsch (DE)"
|
||||
},
|
||||
"strings": {
|
||||
"startMessage": "Hallo,\ngib deinen Jellyfin-PIN-Code ein, um dein Konto zu verifizieren.",
|
||||
"matrixStartMessage": "Hallo,\ngib den untenstehenden PIN auf der Anmeldeseite von Jellyfin ein, um dein Konto zu verifizieren.",
|
||||
"invalidPIN": "Diese PIN war ungültig, versuche es erneut.",
|
||||
"pinSuccess": "Erfolg! Du kannst nun zur Anmeldeseite zurückkehren.",
|
||||
"languageMessage": "Hinweis: Zeige verfügbare Sprachen mit {command} an und stelle mit {command} <Sprachcode> die gewünschte ein."
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@
|
||||
},
|
||||
"strings": {
|
||||
"startMessage": "Hi!\nEnter your Jellyfin PIN code here to verify your account.",
|
||||
"matrixStartMessage": "Hi\nEnter the below PIN in the Jellyfin sign-up page to verify your account.",
|
||||
"invalidPIN": "That PIN was invalid, try again.",
|
||||
"pinSuccess": "Success! You can now return to the sign-up page.",
|
||||
"languageMessage": "Note: See available languages with /lang, and set language with /lang <language code>."
|
||||
"languageMessage": "Note: See available languages with {command}, and set language with {command} <language code>."
|
||||
}
|
||||
}
|
||||
|
||||
12
lang/telegram/nl-NL.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Nederlands (NL)"
|
||||
},
|
||||
"strings": {
|
||||
"startMessage": "Hallo!\nVoer je Jellyfin pincode in om je account te verifiëren.",
|
||||
"matrixStartMessage": "Hallo\nVoer onderstaande pincode in op de Jellyfin aanmeldpagina om je account te verifiëren.",
|
||||
"invalidPIN": "Die pincode was ongeldig, probeer het nogmaals.",
|
||||
"pinSuccess": "Succes! Je kunt nu teruggaan naar de aanmeldpagina.",
|
||||
"languageMessage": "Opmerking: Bekijk beschikbare talen met {command}, en stel de taal in met {command} <taalcode>."
|
||||
}
|
||||
}
|
||||
12
lang/telegram/pt-BR.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Português (BR)"
|
||||
},
|
||||
"strings": {
|
||||
"startMessage": "Oi!\nDigite seu código PIN Jellyfin aqui para verificar sua conta.",
|
||||
"matrixStartMessage": "Oi\nDigite o PIN abaixo na página do Jellyfin para verificar sua conta.",
|
||||
"invalidPIN": "PIN invalido, tente novamente.",
|
||||
"pinSuccess": "Concluído. Agora você pode retornar à página de inscrição.",
|
||||
"languageMessage": "Nota: Veja os idiomas disponíveis com {command} e defina o idioma com {command} <language code>."
|
||||
}
|
||||
}
|
||||
3
linecache/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/hrfee/jfa-go/linecache
|
||||
|
||||
go 1.16
|
||||
66
linecache/linecache.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Package linecache provides a writer that stores n lines of text at once, overwriting old content as it reaches its capacity. Its contents can be read from with a String() method.
|
||||
package linecache
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// LineCache provides an io.Writer that stores a fixed number of lines of text.
|
||||
type LineCache struct {
|
||||
count int
|
||||
lines [][]byte
|
||||
current int
|
||||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
// NewLineCache returns a new line cache of capacity (n) lines.
|
||||
func NewLineCache(n int) *LineCache {
|
||||
return &LineCache{
|
||||
current: 0,
|
||||
count: n,
|
||||
lines: make([][]byte, n),
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Write writes a given byte array to the cache.
|
||||
func (l *LineCache) Write(p []byte) (n int, err error) {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
lines := strings.Split(string(p), "\n")
|
||||
for _, line := range lines {
|
||||
if string(line) == "" {
|
||||
continue
|
||||
}
|
||||
if l.current == l.count {
|
||||
l.current = 0
|
||||
}
|
||||
l.lines[l.current] = []byte(line)
|
||||
l.current++
|
||||
}
|
||||
n = len(p)
|
||||
return
|
||||
}
|
||||
|
||||
// String returns a string representation of the cache contents.
|
||||
func (l *LineCache) String() string {
|
||||
i := 0
|
||||
if l.lines[l.count-1] != nil && l.current != l.count {
|
||||
i = l.current
|
||||
}
|
||||
out := ""
|
||||
for {
|
||||
if l.lines[i] == nil {
|
||||
return out
|
||||
}
|
||||
out += string(l.lines[i]) + "\n"
|
||||
i++
|
||||
if i == l.current {
|
||||
return out
|
||||
}
|
||||
if i == l.count {
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
17
linecache/linecache_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package linecache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
wr := NewLineCache(10)
|
||||
for i := 10; i < 50; i++ {
|
||||
fmt.Fprintln(wr, i)
|
||||
fmt.Print(strings.ReplaceAll(wr.String(), "\n", " "), "\n")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
77
log.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/linecache"
|
||||
)
|
||||
|
||||
var logPath string = filepath.Join(temp, "jfa-go.log")
|
||||
var lineCache = linecache.NewLineCache(100)
|
||||
|
||||
func logOutput() (closeFunc func()) {
|
||||
old := os.Stdout
|
||||
log.Printf("Logging to \"%s\"", logPath)
|
||||
writers := []io.Writer{old, colorStripper{lineCache}}
|
||||
wExit := make(chan bool)
|
||||
r, w, _ := os.Pipe()
|
||||
if TRAY {
|
||||
f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
closeFunc = func() {}
|
||||
return
|
||||
}
|
||||
writers = append(writers, colorStripper{f})
|
||||
closeFunc = func() {
|
||||
w.Close()
|
||||
<-wExit
|
||||
f.Close()
|
||||
}
|
||||
} else {
|
||||
closeFunc = func() {
|
||||
w.Close()
|
||||
<-wExit
|
||||
}
|
||||
}
|
||||
writer := io.MultiWriter(writers...)
|
||||
os.Stdout, os.Stderr = w, w
|
||||
log.SetOutput(writer)
|
||||
gin.DefaultWriter, gin.DefaultErrorWriter = writer, writer
|
||||
go func() {
|
||||
io.Copy(writer, r)
|
||||
wExit <- true
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
// Regex that removes ANSI color escape sequences. Used for outputting to log file and log cache.
|
||||
var stripColors = func() *regexp.Regexp {
|
||||
r, err := regexp.Compile("\\x1b\\[[0-9;]*m")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to compile color escape regexp: %v", err)
|
||||
}
|
||||
return r
|
||||
}()
|
||||
|
||||
type colorStripper struct {
|
||||
file io.Writer
|
||||
}
|
||||
|
||||
func (c colorStripper) Write(p []byte) (n int, err error) {
|
||||
_, err = c.file.Write(stripColors.ReplaceAll(p, []byte("")))
|
||||
n = len(p)
|
||||
return
|
||||
}
|
||||
|
||||
func sanitizeLog(l string) string {
|
||||
quoteCensor, err := regexp.Compile("\"([^\"]*)\"")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to compile sanitizing regexp: %v", err)
|
||||
}
|
||||
return string(quoteCensor.ReplaceAll([]byte(l), []byte("\"CENSORED\"")))
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"runtime"
|
||||
@@ -16,12 +17,14 @@ type Logger interface {
|
||||
Println(v ...interface{})
|
||||
Fatal(v ...interface{})
|
||||
Fatalf(format string, v ...interface{})
|
||||
SetFatalFunc(f func(err interface{}))
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
logger *log.Logger
|
||||
shortfile bool
|
||||
printer *c.Color
|
||||
fatalFunc func(err interface{})
|
||||
}
|
||||
|
||||
func Lshortfile() string {
|
||||
@@ -97,7 +100,16 @@ func (l logger) Fatalf(format string, v ...interface{}) {
|
||||
out = Lshortfile()
|
||||
}
|
||||
out += " " + l.printer.Sprintf(format, v...)
|
||||
l.logger.Fatal(out)
|
||||
if l.fatalFunc != nil {
|
||||
l.logger.Print(out)
|
||||
l.fatalFunc(errors.New(out))
|
||||
} else {
|
||||
l.logger.Fatal(out)
|
||||
}
|
||||
}
|
||||
|
||||
func (l logger) SetFatalFunc(f func(err interface{})) {
|
||||
l.fatalFunc = f
|
||||
}
|
||||
|
||||
type EmptyLogger bool
|
||||
@@ -107,3 +119,4 @@ func (l EmptyLogger) Print(v ...interface{}) {}
|
||||
func (l EmptyLogger) Println(v ...interface{}) {}
|
||||
func (l EmptyLogger) Fatal(v ...interface{}) {}
|
||||
func (l EmptyLogger) Fatalf(format string, v ...interface{}) {}
|
||||
func (l EmptyLogger) SetFatalFunc(f func(err interface{})) {}
|
||||
|
||||
206
main.go
@@ -16,7 +16,6 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -35,12 +34,15 @@ var (
|
||||
SOCK string = "jfa-go.sock"
|
||||
SRV *http.Server
|
||||
RESTART chan bool
|
||||
TRAYRESTART chan bool
|
||||
DATA, CONFIG, HOST *string
|
||||
PORT *int
|
||||
DEBUG *bool
|
||||
PPROF *bool
|
||||
TEST bool
|
||||
SWAGGER *bool
|
||||
QUIT = false
|
||||
RUNNING = false
|
||||
warning = color.New(color.FgYellow).SprintfFunc()
|
||||
info = color.New(color.FgMagenta).SprintfFunc()
|
||||
hiwhite = color.New(color.FgHiWhite).SprintfFunc()
|
||||
@@ -94,6 +96,8 @@ type appContext struct {
|
||||
validator Validator
|
||||
email *Emailer
|
||||
telegram *TelegramDaemon
|
||||
discord *DiscordDaemon
|
||||
matrix *MatrixDaemon
|
||||
info, debug, err logger.Logger
|
||||
host string
|
||||
port int
|
||||
@@ -147,6 +151,8 @@ func test(app *appContext) {
|
||||
}
|
||||
|
||||
func start(asDaemon, firstCall bool) {
|
||||
RUNNING = true
|
||||
defer func() { RUNNING = false }()
|
||||
// app encompasses essentially all useful functions.
|
||||
app := new(appContext)
|
||||
|
||||
@@ -167,7 +173,9 @@ func start(asDaemon, firstCall bool) {
|
||||
}
|
||||
|
||||
app.info = logger.NewLogger(os.Stdout, "[INFO] ", log.Ltime, color.FgHiWhite)
|
||||
app.info.SetFatalFunc(Exit)
|
||||
app.err = logger.NewLogger(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile, color.FgRed)
|
||||
app.err.SetFatalFunc(Exit)
|
||||
|
||||
app.loadArgs(firstCall)
|
||||
|
||||
@@ -192,6 +200,9 @@ func start(asDaemon, firstCall bool) {
|
||||
app.err.Fatalf("Couldn't copy default config.")
|
||||
}
|
||||
app.info.Printf("Copied default configuration to \"%s\"", app.configPath)
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
tempConfig.Section("").Key("first_run").SetValue("true")
|
||||
tempConfig.SaveTo(app.configPath)
|
||||
}
|
||||
|
||||
var debugMode bool
|
||||
@@ -200,9 +211,8 @@ func start(asDaemon, firstCall bool) {
|
||||
app.err.Fatalf("Failed to load config file \"%s\": %v", app.configPath, err)
|
||||
}
|
||||
|
||||
// Some message settings have been moved from "email" to "messages", this will switch them.
|
||||
if app.config.Section("email").Key("use_24h").Value() != "" {
|
||||
app.migrateEmailConfig()
|
||||
if app.config.Section("").Key("first_run").MustBool(false) {
|
||||
firstRun = true
|
||||
}
|
||||
|
||||
app.version = app.config.Section("jellyfin").Key("version").String()
|
||||
@@ -316,6 +326,10 @@ func start(asDaemon, firstCall bool) {
|
||||
app.storage.emails_path = app.config.Section("files").Key("emails").String()
|
||||
if err := app.storage.loadEmails(); err != nil {
|
||||
app.err.Printf("Failed to load Emails: %v", err)
|
||||
err := migrateEmailStorage(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to migrate Email storage: %v", err)
|
||||
}
|
||||
}
|
||||
app.storage.policy_path = app.config.Section("files").Key("user_template").String()
|
||||
if err := app.storage.loadPolicy(); err != nil {
|
||||
@@ -337,26 +351,17 @@ func start(asDaemon, firstCall bool) {
|
||||
if err := app.storage.loadTelegramUsers(); err != nil {
|
||||
app.err.Printf("Failed to load Telegram users: %v", err)
|
||||
}
|
||||
app.storage.discord_path = app.config.Section("files").Key("discord_users").String()
|
||||
if err := app.storage.loadDiscordUsers(); err != nil {
|
||||
app.err.Printf("Failed to load Discord users: %v", err)
|
||||
}
|
||||
app.storage.matrix_path = app.config.Section("files").Key("matrix_users").String()
|
||||
if err := app.storage.loadMatrixUsers(); err != nil {
|
||||
app.err.Printf("Failed to load Matrix users: %v", err)
|
||||
}
|
||||
|
||||
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
|
||||
app.storage.loadProfiles()
|
||||
// Migrate from pre-0.2.0 user templates to profiles
|
||||
if !(app.storage.policy.BlockedTags == nil && app.storage.configuration.GroupedFolders == nil && len(app.storage.displayprefs) == 0) {
|
||||
app.info.Println("Migrating user template files to new profile format")
|
||||
app.storage.migrateToProfile()
|
||||
for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
dir, fname := filepath.Split(path)
|
||||
newFname := strings.Replace(fname, ".json", ".old.json", 1)
|
||||
err := os.Rename(path, filepath.Join(dir, newFname))
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to rename %s: %s", fname, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
app.info.Println("In case of a problem, your original files have been renamed to <file>.old.json")
|
||||
app.storage.storeProfiles()
|
||||
}
|
||||
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
app.storage.ombi_path = app.config.Section("files").Key("ombi_template").String()
|
||||
@@ -375,17 +380,6 @@ func start(asDaemon, firstCall bool) {
|
||||
configBase, _ := fs.ReadFile(localFS, app.configBasePath)
|
||||
json.Unmarshal(configBase, &app.configBase)
|
||||
|
||||
themes := map[string]string{
|
||||
"Jellyfin (Dark)": "dark-theme",
|
||||
"Default (Light)": "light-theme",
|
||||
}
|
||||
// For move from Bootstrap to a17t (0.2.5)
|
||||
if app.config.Section("ui").Key("theme").String() == "Bootstrap (Light)" {
|
||||
app.config.Section("ui").Key("theme").SetValue("Default (Light)")
|
||||
}
|
||||
if val, ok := themes[app.config.Section("ui").Key("theme").String()]; ok {
|
||||
app.cssClass = val
|
||||
}
|
||||
secret, err := generateSecret(16)
|
||||
if err != nil {
|
||||
app.err.Fatal(err)
|
||||
@@ -396,17 +390,17 @@ func start(asDaemon, firstCall bool) {
|
||||
server := app.config.Section("jellyfin").Key("server").String()
|
||||
cacheTimeout := int(app.config.Section("jellyfin").Key("cache_timeout").MustUint(30))
|
||||
stringServerType := app.config.Section("jellyfin").Key("type").String()
|
||||
timeoutHandler := mediabrowser.NewNamedTimeoutHandler("Jellyfin", server, true)
|
||||
timeoutHandler := mediabrowser.NewNamedTimeoutHandler("Jellyfin", "\""+server+"\"", true)
|
||||
if stringServerType == "emby" {
|
||||
serverType = mediabrowser.EmbyServer
|
||||
timeoutHandler = mediabrowser.NewNamedTimeoutHandler("Emby", server, true)
|
||||
timeoutHandler = mediabrowser.NewNamedTimeoutHandler("Emby", "\""+server+"\"", true)
|
||||
app.info.Println("Using Emby server type")
|
||||
fmt.Println(warning("WARNING: Emby compatibility is experimental, and support is limited.\nPassword resets are not available."))
|
||||
} else {
|
||||
app.info.Println("Using Jellyfin server type")
|
||||
}
|
||||
|
||||
app.jf, _ = mediabrowser.NewServer(
|
||||
app.jf, err = mediabrowser.NewServer(
|
||||
serverType,
|
||||
server,
|
||||
app.config.Section("jellyfin").Key("client").String(),
|
||||
@@ -416,85 +410,20 @@ func start(asDaemon, firstCall bool) {
|
||||
timeoutHandler,
|
||||
cacheTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ \"%s\": %v", server, err)
|
||||
}
|
||||
if debugMode {
|
||||
app.jf.Verbose = true
|
||||
}
|
||||
var status int
|
||||
_, status, err = app.jf.Authenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String())
|
||||
if status != 200 || err != nil {
|
||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s (%d): %v", server, status, err)
|
||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ \"%s\" (%d): %v", server, status, err)
|
||||
}
|
||||
app.info.Printf("Authenticated with %s", server)
|
||||
/* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs.
|
||||
This checks if the version is equal or higher. */
|
||||
checkVersion := func(version string) int {
|
||||
numberStrings := strings.Split(version, ".")
|
||||
n := 0
|
||||
for _, s := range numberStrings {
|
||||
num, err := strconv.Atoi(s)
|
||||
if err == nil {
|
||||
n += num
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") {
|
||||
// Get users to check if server uses hyphenated userIDs
|
||||
app.jf.GetUsers(false)
|
||||
|
||||
noHyphens := true
|
||||
for id := range app.storage.emails {
|
||||
if strings.Contains(id, "-") {
|
||||
noHyphens = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if noHyphens == app.jf.Hyphens {
|
||||
var newEmails map[string]interface{}
|
||||
var newUsers map[string]time.Time
|
||||
var status, status2 int
|
||||
var err, err2 error
|
||||
if app.jf.Hyphens {
|
||||
app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
|
||||
time.Sleep(time.Second * time.Duration(3))
|
||||
newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
|
||||
newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
|
||||
} else {
|
||||
app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
|
||||
time.Sleep(time.Second * time.Duration(3))
|
||||
newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
|
||||
newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
|
||||
}
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
|
||||
app.err.Fatalf("Couldn't upgrade emails.json")
|
||||
}
|
||||
if status2 != 200 || err2 != nil {
|
||||
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
|
||||
app.err.Fatalf("Couldn't upgrade users.json")
|
||||
}
|
||||
emailBakFile := app.storage.emails_path + ".bak"
|
||||
usersBakFile := app.storage.users_path + ".bak"
|
||||
err = storeJSON(emailBakFile, app.storage.emails)
|
||||
err2 = storeJSON(usersBakFile, app.storage.users)
|
||||
if err != nil {
|
||||
app.err.Fatalf("couldn't store emails.json backup: %v", err)
|
||||
}
|
||||
if err2 != nil {
|
||||
app.err.Fatalf("couldn't store users.json backup: %v", err)
|
||||
}
|
||||
app.storage.emails = newEmails
|
||||
app.storage.users = newUsers
|
||||
err = app.storage.storeEmails()
|
||||
err2 = app.storage.storeUsers()
|
||||
if err != nil {
|
||||
app.err.Fatalf("couldn't store emails.json: %v", err)
|
||||
}
|
||||
if err2 != nil {
|
||||
app.err.Fatalf("couldn't store users.json: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
runMigrations(app)
|
||||
|
||||
// Auth (manual user/pass or jellyfin)
|
||||
app.jellyfinLogin = true
|
||||
@@ -540,7 +469,7 @@ func start(asDaemon, firstCall bool) {
|
||||
|
||||
invDaemon := newInviteDaemon(time.Duration(60*time.Second), app)
|
||||
go invDaemon.run()
|
||||
defer invDaemon.shutdown()
|
||||
defer invDaemon.Shutdown()
|
||||
|
||||
userDaemon := newUserDaemon(time.Duration(60*time.Second), app)
|
||||
go userDaemon.run()
|
||||
@@ -558,11 +487,32 @@ func start(asDaemon, firstCall bool) {
|
||||
app.telegram, err = newTelegramDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to authenticate with Telegram: %v", err)
|
||||
telegramEnabled = false
|
||||
} else {
|
||||
go app.telegram.run()
|
||||
defer app.telegram.Shutdown()
|
||||
}
|
||||
}
|
||||
if discordEnabled {
|
||||
app.discord, err = newDiscordDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to authenticate with Discord: %v", err)
|
||||
discordEnabled = false
|
||||
} else {
|
||||
go app.discord.run()
|
||||
defer app.discord.Shutdown()
|
||||
}
|
||||
}
|
||||
if matrixEnabled {
|
||||
app.matrix, err = newMatrixDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to initialize Matrix daemon: %v", err)
|
||||
matrixEnabled = false
|
||||
} else {
|
||||
go app.matrix.run()
|
||||
defer app.matrix.Shutdown()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugMode = false
|
||||
address = "0.0.0.0:8056"
|
||||
@@ -599,6 +549,11 @@ func start(asDaemon, firstCall bool) {
|
||||
}
|
||||
}
|
||||
}()
|
||||
if firstRun {
|
||||
app.info.Printf("Loaded, visit %s to start.", address)
|
||||
} else {
|
||||
app.info.Printf("Loaded @ %s", address)
|
||||
}
|
||||
app.quit = make(chan os.Signal)
|
||||
signal.Notify(app.quit, os.Interrupt)
|
||||
go func() {
|
||||
@@ -618,13 +573,19 @@ func start(asDaemon, firstCall bool) {
|
||||
|
||||
func (app *appContext) shutdown() {
|
||||
app.info.Println("Shutting down...")
|
||||
|
||||
cntx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
if err := SRV.Shutdown(cntx); err != nil {
|
||||
app.err.Fatalf("Server shutdown error: %s", err)
|
||||
QUIT = true
|
||||
RESTART <- true
|
||||
for {
|
||||
if RUNNING {
|
||||
continue
|
||||
}
|
||||
cntx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
if err := SRV.Shutdown(cntx); err != nil {
|
||||
app.err.Fatalf("Server shutdown error: %s", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func flagPassed(name string) (found bool) {
|
||||
@@ -637,10 +598,10 @@ func flagPassed(name string) (found bool) {
|
||||
}
|
||||
|
||||
// @title jfa-go internal API
|
||||
// @version 0.2.0
|
||||
// @version 0.3.6
|
||||
// @description API for the jfa-go frontend
|
||||
// @contact.name Harvey Tindall
|
||||
// @contact.email hrfee@protonmail.ch
|
||||
// @contact.email hrfee@hrfee.dev
|
||||
// @license.name MIT
|
||||
// @license.url https://raw.githubusercontent.com/hrfee/jfa-go/main/LICENSE
|
||||
// @BasePath /
|
||||
@@ -674,10 +635,20 @@ func flagPassed(name string) (found bool) {
|
||||
// @tag.description Things that dont fit elsewhere.
|
||||
|
||||
func printVersion() {
|
||||
fmt.Println(info("jfa-go version: %s (%s)\n", hiwhite(version), white(commit)))
|
||||
tray := ""
|
||||
if TRAY {
|
||||
tray = " TrayIcon"
|
||||
}
|
||||
fmt.Println(info("jfa-go version: %s (%s)%s\n", hiwhite(version), white(commit), tray))
|
||||
}
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
Exit(r)
|
||||
}
|
||||
}()
|
||||
defer logOutput()()
|
||||
printVersion()
|
||||
SOCK = filepath.Join(temp, SOCK)
|
||||
fmt.Println("Socket:", SOCK)
|
||||
@@ -745,10 +716,15 @@ You can then run:
|
||||
fmt.Print(info("systemctl --user stop jfa-go\n\n"))
|
||||
color.New(color.FgYellow).PrintFunc()("To restart: ")
|
||||
fmt.Print(info("systemctl --user stop jfa-go\n"))
|
||||
} else if TRAY {
|
||||
RunTray()
|
||||
} else {
|
||||
RESTART = make(chan bool, 1)
|
||||
start(false, true)
|
||||
for {
|
||||
if QUIT {
|
||||
continue
|
||||
}
|
||||
printVersion()
|
||||
start(false, false)
|
||||
}
|
||||
|
||||
238
matrix.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/matrix-org/gomatrix"
|
||||
)
|
||||
|
||||
type MatrixDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
bot *gomatrix.Client
|
||||
userID string
|
||||
tokens map[string]UnverifiedUser // Map of tokens to users
|
||||
languages map[string]string // Map of roomIDs to language codes
|
||||
app *appContext
|
||||
}
|
||||
|
||||
type UnverifiedUser struct {
|
||||
Verified bool
|
||||
User *MatrixUser
|
||||
}
|
||||
|
||||
type MatrixUser struct {
|
||||
RoomID string
|
||||
UserID string
|
||||
Lang string
|
||||
Contact bool
|
||||
}
|
||||
|
||||
type MatrixIdentifier struct {
|
||||
User string `json:"user"`
|
||||
IdentType string `json:"type"`
|
||||
}
|
||||
|
||||
func (m MatrixIdentifier) Type() string { return m.IdentType }
|
||||
|
||||
var matrixFilter = gomatrix.Filter{
|
||||
Room: gomatrix.RoomFilter{
|
||||
Timeline: gomatrix.FilterPart{
|
||||
Types: []string{
|
||||
"m.room.message",
|
||||
"m.room.member",
|
||||
},
|
||||
},
|
||||
},
|
||||
EventFields: []string{
|
||||
"type",
|
||||
"event_id",
|
||||
"room_id",
|
||||
"state_key",
|
||||
"sender",
|
||||
"content.body",
|
||||
"content.membership",
|
||||
},
|
||||
}
|
||||
|
||||
func newMatrixDaemon(app *appContext) (d *MatrixDaemon, err error) {
|
||||
matrix := app.config.Section("matrix")
|
||||
homeserver := matrix.Key("homeserver").String()
|
||||
token := matrix.Key("token").String()
|
||||
d = &MatrixDaemon{
|
||||
ShutdownChannel: make(chan string),
|
||||
userID: matrix.Key("user_id").String(),
|
||||
tokens: map[string]UnverifiedUser{},
|
||||
languages: map[string]string{},
|
||||
app: app,
|
||||
}
|
||||
d.bot, err = gomatrix.NewClient(homeserver, d.userID, token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
filter, err := json.Marshal(matrixFilter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := d.bot.CreateFilter(filter)
|
||||
d.bot.Store.SaveFilterID(d.userID, resp.FilterID)
|
||||
for _, user := range app.storage.matrix {
|
||||
if user.Lang != "" {
|
||||
d.languages[user.RoomID] = user.Lang
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string) (string, error) {
|
||||
req := &gomatrix.ReqLogin{
|
||||
Type: "m.login.password",
|
||||
Identifier: MatrixIdentifier{
|
||||
User: username,
|
||||
IdentType: "m.id.user",
|
||||
},
|
||||
Password: password,
|
||||
DeviceID: "jfa-go-" + commit,
|
||||
}
|
||||
bot, err := gomatrix.NewClient(homeserver, username, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := bot.Login(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.AccessToken, nil
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) run() {
|
||||
d.app.info.Println("Starting Matrix bot daemon")
|
||||
syncer := d.bot.Syncer.(*gomatrix.DefaultSyncer)
|
||||
syncer.OnEventType("m.room.message", d.handleMessage)
|
||||
// syncer.OnEventType("m.room.member", d.handleMembership)
|
||||
if err := d.bot.Sync(); err != nil {
|
||||
d.app.err.Printf("Matrix sync failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) Shutdown() {
|
||||
d.bot.StopSync()
|
||||
d.Stopped = true
|
||||
close(d.ShutdownChannel)
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) handleMessage(event *gomatrix.Event) {
|
||||
if event.Sender == d.userID {
|
||||
return
|
||||
}
|
||||
lang := "en-us"
|
||||
if l, ok := d.languages[event.RoomID]; ok {
|
||||
if _, ok := d.app.storage.lang.Telegram[l]; ok {
|
||||
lang = l
|
||||
}
|
||||
}
|
||||
sects := strings.Split(event.Content["body"].(string), " ")
|
||||
switch sects[0] {
|
||||
case "!lang":
|
||||
if len(sects) == 2 {
|
||||
d.commandLang(event, sects[1], lang)
|
||||
} else {
|
||||
d.commandLang(event, "", lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) commandLang(event *gomatrix.Event, code, lang string) {
|
||||
if code == "" {
|
||||
list := "!lang <lang>\n"
|
||||
for c := range d.app.storage.lang.Telegram {
|
||||
list += fmt.Sprintf("%s: %s\n", c, d.app.storage.lang.Telegram[c].Meta.Name)
|
||||
}
|
||||
_, err := d.bot.SendText(
|
||||
event.RoomID,
|
||||
list,
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Matrix: Failed to send message to \"%s\": %v", event.Sender, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, ok := d.app.storage.lang.Telegram[code]; !ok {
|
||||
return
|
||||
}
|
||||
d.languages[event.RoomID] = code
|
||||
if u, ok := d.app.storage.matrix[event.RoomID]; ok {
|
||||
u.Lang = code
|
||||
d.app.storage.matrix[event.RoomID] = u
|
||||
if err := d.app.storage.storeMatrixUsers(); err != nil {
|
||||
d.app.err.Printf("Matrix: Failed to store Matrix users: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) CreateRoom(userID string) (string, error) {
|
||||
room, err := d.bot.CreateRoom(&gomatrix.ReqCreateRoom{
|
||||
Visibility: "private",
|
||||
Invite: []string{userID},
|
||||
Topic: d.app.config.Section("matrix").Key("topic").String(),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return room.RoomID, nil
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
||||
roomID, err := d.CreateRoom(userID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Failed to create room for user \"%s\": %v", userID, err)
|
||||
return
|
||||
}
|
||||
lang := "en-us"
|
||||
pin := genAuthToken()
|
||||
d.tokens[pin] = UnverifiedUser{
|
||||
false,
|
||||
&MatrixUser{
|
||||
RoomID: roomID,
|
||||
UserID: userID,
|
||||
Lang: lang,
|
||||
},
|
||||
}
|
||||
_, err = d.bot.SendText(
|
||||
roomID,
|
||||
d.app.storage.lang.Telegram[lang].Strings.get("matrixStartMessage")+"\n\n"+pin+"\n\n"+
|
||||
d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"}),
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Matrix: Failed to send welcome message to \"%s\": %v", userID, err)
|
||||
return
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) Send(message *Message, roomID ...string) (err error) {
|
||||
md := ""
|
||||
if message.Markdown != "" {
|
||||
// Convert images to links
|
||||
md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, renderer))
|
||||
}
|
||||
for _, id := range roomID {
|
||||
if md != "" {
|
||||
_, err = d.bot.SendFormattedText(id, message.Text, md)
|
||||
} else {
|
||||
_, err = d.bot.SendText(id, message.Text)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// User enters ID on sign-up, a PIN is sent to them. They enter it on sign-up.
|
||||
|
||||
// Message the user first, to avoid E2EE by default
|
||||
180
migrations.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func runMigrations(app *appContext) {
|
||||
migrateProfiles(app)
|
||||
migrateBootstrap(app)
|
||||
migrateEmailStorage(app)
|
||||
// migrateHyphens(app)
|
||||
}
|
||||
|
||||
// Migrate pre-0.2.0 user templates to profiles
|
||||
func migrateProfiles(app *appContext) {
|
||||
if !(app.storage.policy.BlockedTags == nil && app.storage.configuration.GroupedFolders == nil && len(app.storage.displayprefs) == 0) {
|
||||
app.info.Println("Migrating user template files to new profile format")
|
||||
app.storage.migrateToProfile()
|
||||
for _, path := range [3]string{app.storage.policy_path, app.storage.configuration_path, app.storage.displayprefs_path} {
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
dir, fname := filepath.Split(path)
|
||||
newFname := strings.Replace(fname, ".json", ".old.json", 1)
|
||||
err := os.Rename(path, filepath.Join(dir, newFname))
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to rename %s: %s", fname, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
app.info.Println("In case of a problem, your original files have been renamed to <file>.old.json")
|
||||
app.storage.storeProfiles()
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate pre-0.2.5 bootstrap theme choice to a17t version.
|
||||
func migrateBootstrap(app *appContext) {
|
||||
themes := map[string]string{
|
||||
"Jellyfin (Dark)": "dark-theme",
|
||||
"Default (Light)": "light-theme",
|
||||
}
|
||||
|
||||
if app.config.Section("ui").Key("theme").String() == "Bootstrap (Light)" {
|
||||
app.config.Section("ui").Key("theme").SetValue("Default (Light)")
|
||||
}
|
||||
if val, ok := themes[app.config.Section("ui").Key("theme").String()]; ok {
|
||||
app.cssClass = val
|
||||
}
|
||||
}
|
||||
|
||||
func migrateEmailConfig(app *appContext) {
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
fmt.Println(warning("Part of your email configuration will be migrated to the new \"messages\" section.\nA backup will be made."))
|
||||
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to backup config: %v", err)
|
||||
return
|
||||
}
|
||||
for _, setting := range []string{"use_24h", "date_format", "message"} {
|
||||
if val := app.config.Section("email").Key(setting).Value(); val != "" {
|
||||
tempConfig.Section("email").Key(setting).SetValue("")
|
||||
tempConfig.Section("messages").Key(setting).SetValue(val)
|
||||
}
|
||||
}
|
||||
if app.config.Section("messages").Key("enabled").MustBool(false) || app.config.Section("telegram").Key("enabled").MustBool(false) {
|
||||
tempConfig.Section("messages").Key("enabled").SetValue("true")
|
||||
}
|
||||
err = tempConfig.SaveTo(app.configPath)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to save config: %v", err)
|
||||
return
|
||||
}
|
||||
app.loadConfig()
|
||||
}
|
||||
|
||||
// Migrate pre-0.3.6 email settings to the new messages section.
|
||||
// Called just after loading email storage in main.go.
|
||||
func migrateEmailStorage(app *appContext) error {
|
||||
// use_24h was moved to messages, so this checks if migration has already occurred or not.
|
||||
if app.config.Section("email").Key("use_24h").Value() == "" {
|
||||
return nil
|
||||
}
|
||||
var emails map[string]interface{}
|
||||
err := loadJSON(app.storage.emails_path, &emails)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newEmails := map[string]EmailAddress{}
|
||||
for jfID, addr := range emails {
|
||||
newEmails[jfID] = EmailAddress{
|
||||
Addr: addr.(string),
|
||||
Contact: true,
|
||||
}
|
||||
}
|
||||
err = storeJSON(app.storage.emails_path+".bak", emails)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = storeJSON(app.storage.emails_path, newEmails)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app.info.Println("Migrated to new email format. A backup has also been made.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Migrate between hyphenated & non-hyphenated user IDs. Doesn't seem to happen anymore, so disabled.
|
||||
// func migrateHyphens(app *appContext) {
|
||||
// checkVersion := func(version string) int {
|
||||
// numberStrings := strings.Split(version, ".")
|
||||
// n := 0
|
||||
// for _, s := range numberStrings {
|
||||
// num, err := strconv.Atoi(s)
|
||||
// if err == nil {
|
||||
// n += num
|
||||
// }
|
||||
// }
|
||||
// return n
|
||||
// }
|
||||
// if serverType == mediabrowser.JellyfinServer && checkVersion(app.jf.ServerInfo.Version) >= checkVersion("10.7.0") {
|
||||
// // Get users to check if server uses hyphenated userIDs
|
||||
// app.jf.GetUsers(false)
|
||||
//
|
||||
// noHyphens := true
|
||||
// for id := range app.storage.emails {
|
||||
// if strings.Contains(id, "-") {
|
||||
// noHyphens = false
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if noHyphens == app.jf.Hyphens {
|
||||
// var newEmails map[string]interface{}
|
||||
// var newUsers map[string]time.Time
|
||||
// var status, status2 int
|
||||
// var err, err2 error
|
||||
// if app.jf.Hyphens {
|
||||
// app.info.Println(info("Your build of Jellyfin appears to hypenate user IDs. Your emails.json/users.json file will be modified to match."))
|
||||
// time.Sleep(time.Second * time.Duration(3))
|
||||
// newEmails, status, err = app.hyphenateEmailStorage(app.storage.emails)
|
||||
// newUsers, status2, err2 = app.hyphenateUserStorage(app.storage.users)
|
||||
// } else {
|
||||
// app.info.Println(info("Your emails.json/users.json file uses hyphens, but the Jellyfin server no longer does. It will be modified."))
|
||||
// time.Sleep(time.Second * time.Duration(3))
|
||||
// newEmails, status, err = app.deHyphenateEmailStorage(app.storage.emails)
|
||||
// newUsers, status2, err2 = app.deHyphenateUserStorage(app.storage.users)
|
||||
// }
|
||||
// if status != 200 || err != nil {
|
||||
// app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
|
||||
// app.err.Fatalf("Couldn't upgrade emails.json")
|
||||
// }
|
||||
// if status2 != 200 || err2 != nil {
|
||||
// app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
|
||||
// app.err.Fatalf("Couldn't upgrade users.json")
|
||||
// }
|
||||
// emailBakFile := app.storage.emails_path + ".bak"
|
||||
// usersBakFile := app.storage.users_path + ".bak"
|
||||
// err = storeJSON(emailBakFile, app.storage.emails)
|
||||
// err2 = storeJSON(usersBakFile, app.storage.users)
|
||||
// if err != nil {
|
||||
// app.err.Fatalf("couldn't store emails.json backup: %v", err)
|
||||
// }
|
||||
// if err2 != nil {
|
||||
// app.err.Fatalf("couldn't store users.json backup: %v", err)
|
||||
// }
|
||||
// app.storage.emails = newEmails
|
||||
// app.storage.users = newUsers
|
||||
// err = app.storage.storeEmails()
|
||||
// err2 = app.storage.storeUsers()
|
||||
// if err != nil {
|
||||
// app.err.Fatalf("couldn't store emails.json: %v", err)
|
||||
// }
|
||||
// if err2 != nil {
|
||||
// app.err.Fatalf("couldn't store users.json: %v", err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
77
models.go
@@ -17,6 +17,10 @@ type newUserDTO struct {
|
||||
Code string `json:"code" example:"abc0933jncjkcjj"` // Invite code (required on /newUser)
|
||||
TelegramPIN string `json:"telegram_pin" example:"A1-B2-3C"` // Telegram verification PIN (if used)
|
||||
TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs
|
||||
DiscordPIN string `json:"discord_pin" example:"A1-B2-3C"` // Discord verification PIN (if used)
|
||||
DiscordContact bool `json:"discord_contact"` // Whether or not to use discord for notifications/pwrs
|
||||
MatrixPIN string `json:"matrix_pin" example:"A1-B2-3C"` // Matrix verification PIN (if used)
|
||||
MatrixContact bool `json:"matrix_contact"` // Whether or not to use matrix for notifications/pwrs
|
||||
}
|
||||
|
||||
type newUserResponse struct {
|
||||
@@ -48,7 +52,7 @@ type generateInviteDTO struct {
|
||||
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
|
||||
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
|
||||
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
|
||||
Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address
|
||||
SendTo string `json:"send-to" example:"jeff@jellyf.in"` // Send invite to this address or discord name
|
||||
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
|
||||
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
|
||||
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
|
||||
@@ -98,7 +102,7 @@ type inviteDTO struct {
|
||||
UsedBy map[string]int64 `json:"used-by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time
|
||||
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
|
||||
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
|
||||
Email string `json:"email,omitempty"` // Email the invite was sent to (if applicable)
|
||||
SendTo string `json:"send_to,omitempty"` // Email/Discord username the invite was sent to (if applicable)
|
||||
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
||||
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
||||
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
|
||||
@@ -125,12 +129,18 @@ type respUser struct {
|
||||
ID string `json:"id" example:"fdgsdfg45534fa"` // userID of user
|
||||
Name string `json:"name" example:"jeff"` // Username of user
|
||||
Email string `json:"email,omitempty" example:"jeff@jellyf.in"` // Email address of user (if available)
|
||||
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
|
||||
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
|
||||
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
|
||||
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
|
||||
Telegram string `json:"telegram"` // Telegram username (if known)
|
||||
NotifyThroughEmail bool `json:"notify_email"`
|
||||
LastActive int64 `json:"last_active" example:"1617737207510"` // Time of last activity on Jellyfin
|
||||
Admin bool `json:"admin" example:"false"` // Whether or not the user is Administrator
|
||||
Expiry int64 `json:"expiry" example:"1617737207510"` // Expiry time of user as Epoch/Unix time.
|
||||
Disabled bool `json:"disabled"` // Whether or not the user is disabled.
|
||||
Telegram string `json:"telegram"` // Telegram username (if known)
|
||||
NotifyThroughTelegram bool `json:"notify_telegram"`
|
||||
Discord string `json:"discord"` // Discord username (if known)
|
||||
DiscordID string `json:"discord_id"` // Discord user ID for creating links.
|
||||
NotifyThroughDiscord bool `json:"notify_discord"`
|
||||
Matrix string `json:"matrix"` // Matrix ID (if known)
|
||||
NotifyThroughMatrix bool `json:"notify_matrix"`
|
||||
}
|
||||
|
||||
type getUsersDTO struct {
|
||||
@@ -249,7 +259,54 @@ type telegramSetDTO struct {
|
||||
ID string `json:"id"` // Jellyfin ID of user.
|
||||
}
|
||||
|
||||
type telegramNotifyDTO struct {
|
||||
ID string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
type SetContactMethodsDTO struct {
|
||||
ID string `json:"id"`
|
||||
Email bool `json:"email"`
|
||||
Discord bool `json:"discord"`
|
||||
Telegram bool `json:"telegram"`
|
||||
Matrix bool `json:"matrix"`
|
||||
}
|
||||
|
||||
type DiscordUserDTO struct {
|
||||
Name string `json:"name"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type DiscordUsersDTO struct {
|
||||
Users []DiscordUserDTO `json:"users"`
|
||||
}
|
||||
|
||||
type DiscordConnectUserDTO struct {
|
||||
JellyfinID string `json:"jf_id"`
|
||||
DiscordID string `json:"discord_id"`
|
||||
}
|
||||
|
||||
type DiscordInviteDTO struct {
|
||||
InviteURL string `json:"invite"`
|
||||
IconURL string `json:"icon"`
|
||||
}
|
||||
|
||||
type MatrixSendPINDTO struct {
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type MatrixCheckPINDTO struct {
|
||||
PIN string `json:"pin"`
|
||||
}
|
||||
|
||||
type MatrixConnectUserDTO struct {
|
||||
JellyfinID string `json:"jf_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type MatrixLoginDTO struct {
|
||||
Homeserver string `json:"homeserver"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type ResetPasswordDTO struct {
|
||||
PIN string `json:"pin"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
7
notray.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// +build !tray
|
||||
|
||||
package main
|
||||
|
||||
var TRAY = false
|
||||
|
||||
func RunTray() {}
|
||||
6606
package-lock.json
generated
@@ -20,11 +20,14 @@
|
||||
"@ts-stack/markdown": "^1.3.0",
|
||||
"@types/node": "^15.0.1",
|
||||
"a17t": "^0.4.0",
|
||||
"browserslist": "^4.16.6",
|
||||
"esbuild": "^0.8.57",
|
||||
"lodash": "^4.17.19",
|
||||
"inline-source-cli": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mjml": "^4.8.0",
|
||||
"remixicon": "^2.5.0",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"typescript": "^4.0.3"
|
||||
"typescript": "^4.0.3",
|
||||
"uncss": "^0.17.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +54,12 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
||||
var pwr PasswordReset
|
||||
data, err := os.ReadFile(event.Name)
|
||||
if err != nil {
|
||||
app.debug.Printf("PWR: Failed to read file: %v", err)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(data, &pwr)
|
||||
if len(pwr.Pin) == 0 || err != nil {
|
||||
app.debug.Printf("PWR: Failed to read PIN: %v", err)
|
||||
return
|
||||
}
|
||||
app.info.Printf("New password reset for user \"%s\"", pwr.Username)
|
||||
|
||||
24
router.go
@@ -108,6 +108,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
|
||||
if app.config.Section("password_resets").Key("link_reset").MustBool(false) {
|
||||
router.GET(p+"/reset", app.ResetPassword)
|
||||
if app.config.Section("password_resets").Key("set_password").MustBool(false) {
|
||||
router.POST(p+"/reset", app.ResetSetPassword)
|
||||
}
|
||||
}
|
||||
|
||||
router.GET(p+"/accounts", app.AdminPage)
|
||||
@@ -121,6 +124,17 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
if telegramEnabled {
|
||||
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
|
||||
}
|
||||
if discordEnabled {
|
||||
router.GET(p+"/invite/:invCode/discord/verified/:pin", app.DiscordVerifiedInvite)
|
||||
if app.config.Section("discord").Key("provide_invite").MustBool(false) {
|
||||
router.GET(p+"/invite/:invCode/discord/invite", app.DiscordServerInvite)
|
||||
}
|
||||
}
|
||||
if matrixEnabled {
|
||||
router.GET(p+"/invite/:invCode/matrix/verified/:userID/:pin", app.MatrixCheckPIN)
|
||||
router.POST(p+"/invite/:invCode/matrix/user", app.MatrixSendPIN)
|
||||
router.POST(p+"/users/matrix", app.MatrixConnect)
|
||||
}
|
||||
}
|
||||
if *SWAGGER {
|
||||
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
||||
@@ -158,16 +172,22 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
api.GET(p+"/config", app.GetConfig)
|
||||
api.POST(p+"/config", app.ModifyConfig)
|
||||
api.POST(p+"/restart", app.restart)
|
||||
if telegramEnabled {
|
||||
if telegramEnabled || discordEnabled || matrixEnabled {
|
||||
api.GET(p+"/telegram/pin", app.TelegramGetPin)
|
||||
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
|
||||
api.POST(p+"/users/telegram", app.TelegramAddUser)
|
||||
api.POST(p+"/users/telegram/notify", app.TelegramSetNotify)
|
||||
api.POST(p+"/users/contact", app.SetContactMethods)
|
||||
}
|
||||
if discordEnabled {
|
||||
api.GET(p+"/users/discord/:username", app.DiscordGetUsers)
|
||||
api.POST(p+"/users/discord", app.DiscordConnect)
|
||||
}
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
api.GET(p+"/ombi/users", app.OmbiUsers)
|
||||
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)
|
||||
}
|
||||
api.POST(p+"/matrix/login", app.MatrixLogin)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
def fix_description(desc):
|
||||
return "; " + desc.replace("\n", "\n; ")
|
||||
|
||||
def generate_ini(base_file, ini_file):
|
||||
"""
|
||||
@@ -17,10 +19,10 @@ def generate_ini(base_file, ini_file):
|
||||
for section in config_base["sections"]:
|
||||
ini.add_section(section)
|
||||
if "meta" in config_base["sections"][section]:
|
||||
ini.set(section, "; " + config_base["sections"][section]["meta"]["description"])
|
||||
ini.set(section, fix_description(config_base["sections"][section]["meta"]["description"]))
|
||||
for entry in config_base["sections"][section]["settings"]:
|
||||
if "description" in config_base["sections"][section]["settings"][entry]:
|
||||
ini.set(section, "; " + config_base["sections"][section]["settings"][entry]["description"])
|
||||
ini.set(section, fix_description(config_base["sections"][section]["settings"][entry]["description"]))
|
||||
value = config_base["sections"][section]["settings"][entry]["value"]
|
||||
if isinstance(value, bool):
|
||||
value = str(value).lower()
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
# sets version environment variable for goreleaser to use
|
||||
# scripts/version.sh goreleaser ...
|
||||
JFA_GO_VERSION=$(git describe --exact-match HEAD 2> /dev/null || echo 'vgit')
|
||||
JFA_GO_VERSION="$(echo $JFA_GO_VERSION | sed 's/v//g')" $@
|
||||
JFA_GO_NFPM_EPOCH=$(git rev-list --all --count) JFA_GO_VERSION="$(echo $JFA_GO_VERSION | sed 's/v//g')" $@
|
||||
|
||||
179
storage.go
@@ -15,19 +15,22 @@ import (
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path string
|
||||
users map[string]time.Time
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
emails, displayprefs, ombi_template map[string]interface{}
|
||||
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
|
||||
customEmails customEmails
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
lang Lang
|
||||
invitesLock, usersLock sync.Mutex
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path string
|
||||
users map[string]time.Time
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
displayprefs, ombi_template map[string]interface{}
|
||||
emails map[string]EmailAddress
|
||||
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
|
||||
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
|
||||
matrix map[string]MatrixUser // Map of Jellyfin user IDs to Matrix users.
|
||||
customEmails customEmails
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
lang Lang
|
||||
invitesLock, usersLock sync.Mutex
|
||||
}
|
||||
|
||||
type TelegramUser struct {
|
||||
@@ -37,6 +40,20 @@ type TelegramUser struct {
|
||||
Contact bool // Whether to contact through telegram or not
|
||||
}
|
||||
|
||||
type DiscordUser struct {
|
||||
ChannelID string
|
||||
ID string
|
||||
Username string
|
||||
Discriminator string
|
||||
Lang string
|
||||
Contact bool
|
||||
}
|
||||
|
||||
type EmailAddress struct {
|
||||
Addr string
|
||||
Contact bool
|
||||
}
|
||||
|
||||
type customEmails struct {
|
||||
UserCreated customEmail `json:"userCreated"`
|
||||
InviteExpiry customEmail `json:"inviteExpiry"`
|
||||
@@ -79,7 +96,7 @@ type Invite struct {
|
||||
UserDays int `json:"user-days,omitempty"`
|
||||
UserHours int `json:"user-hours,omitempty"`
|
||||
UserMinutes int `json:"user-minutes,omitempty"`
|
||||
Email string `json:"email"`
|
||||
SendTo string `json:"email"`
|
||||
// Used to be stored as formatted time, now as Unix.
|
||||
UsedBy [][]string `json:"used-by"`
|
||||
Notify map[string]map[string]bool `json:"notify"`
|
||||
@@ -89,23 +106,24 @@ type Invite struct {
|
||||
}
|
||||
|
||||
type Lang struct {
|
||||
AdminPath string
|
||||
chosenAdminLang string
|
||||
Admin adminLangs
|
||||
AdminJSON map[string]string
|
||||
FormPath string
|
||||
chosenFormLang string
|
||||
Form formLangs
|
||||
PasswordResetPath string
|
||||
chosenPWRLang string
|
||||
PasswordReset pwrLangs
|
||||
EmailPath string
|
||||
chosenEmailLang string
|
||||
Email emailLangs
|
||||
CommonPath string
|
||||
Common commonLangs
|
||||
SetupPath string
|
||||
Setup setupLangs
|
||||
AdminPath string
|
||||
chosenAdminLang string
|
||||
Admin adminLangs
|
||||
AdminJSON map[string]string
|
||||
FormPath string
|
||||
chosenFormLang string
|
||||
Form formLangs
|
||||
PasswordResetPath string
|
||||
chosenPWRLang string
|
||||
PasswordReset pwrLangs
|
||||
EmailPath string
|
||||
chosenEmailLang string
|
||||
Email emailLangs
|
||||
CommonPath string
|
||||
Common commonLangs
|
||||
SetupPath string
|
||||
Setup setupLangs
|
||||
// Telegram translations are also used for Discord bots (and likely future ones).
|
||||
chosenTelegramLang string
|
||||
TelegramPath string
|
||||
Telegram telegramLangs
|
||||
@@ -765,6 +783,22 @@ func (st *Storage) storeTelegramUsers() error {
|
||||
return storeJSON(st.telegram_path, st.telegram)
|
||||
}
|
||||
|
||||
func (st *Storage) loadDiscordUsers() error {
|
||||
return loadJSON(st.discord_path, &st.discord)
|
||||
}
|
||||
|
||||
func (st *Storage) storeDiscordUsers() error {
|
||||
return storeJSON(st.discord_path, st.discord)
|
||||
}
|
||||
|
||||
func (st *Storage) loadMatrixUsers() error {
|
||||
return loadJSON(st.matrix_path, &st.matrix)
|
||||
}
|
||||
|
||||
func (st *Storage) storeMatrixUsers() error {
|
||||
return storeJSON(st.matrix_path, st.matrix)
|
||||
}
|
||||
|
||||
func (st *Storage) loadCustomEmails() error {
|
||||
return loadJSON(st.customEmails_path, &st.customEmails)
|
||||
}
|
||||
@@ -883,86 +917,3 @@ func storeJSON(path string, obj interface{}) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage.
|
||||
|
||||
func hyphenate(userID string) string {
|
||||
if userID[8] == '-' {
|
||||
return userID
|
||||
}
|
||||
return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
|
||||
}
|
||||
|
||||
func (app *appContext) deHyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||
jfUsers, status, err := app.jf.GetUsers(false)
|
||||
if status != 200 || err != nil {
|
||||
return nil, status, err
|
||||
}
|
||||
newEmails := map[string]interface{}{}
|
||||
for _, user := range jfUsers {
|
||||
unHyphenated := user.ID
|
||||
hyphenated := hyphenate(unHyphenated)
|
||||
val, ok := old[hyphenated]
|
||||
if ok {
|
||||
newEmails[unHyphenated] = val
|
||||
}
|
||||
}
|
||||
return newEmails, status, err
|
||||
}
|
||||
|
||||
func (app *appContext) hyphenateStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||
jfUsers, status, err := app.jf.GetUsers(false)
|
||||
if status != 200 || err != nil {
|
||||
return nil, status, err
|
||||
}
|
||||
newEmails := map[string]interface{}{}
|
||||
for _, user := range jfUsers {
|
||||
unstripped := user.ID
|
||||
stripped := strings.ReplaceAll(unstripped, "-", "")
|
||||
val, ok := old[stripped]
|
||||
if ok {
|
||||
newEmails[unstripped] = val
|
||||
}
|
||||
}
|
||||
return newEmails, status, err
|
||||
}
|
||||
|
||||
func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||
return app.hyphenateStorage(old)
|
||||
}
|
||||
|
||||
func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
|
||||
return app.deHyphenateStorage(old)
|
||||
}
|
||||
|
||||
func (app *appContext) hyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
|
||||
asInterface := map[string]interface{}{}
|
||||
for k, v := range old {
|
||||
asInterface[k] = v
|
||||
}
|
||||
fixed, status, err := app.hyphenateStorage(asInterface)
|
||||
if err != nil {
|
||||
return nil, status, err
|
||||
}
|
||||
out := map[string]time.Time{}
|
||||
for k, v := range fixed {
|
||||
out[k] = v.(time.Time)
|
||||
}
|
||||
return out, status, err
|
||||
}
|
||||
|
||||
func (app *appContext) deHyphenateUserStorage(old map[string]time.Time) (map[string]time.Time, int, error) {
|
||||
asInterface := map[string]interface{}{}
|
||||
for k, v := range old {
|
||||
asInterface[k] = v
|
||||
}
|
||||
fixed, status, err := app.deHyphenateStorage(asInterface)
|
||||
if err != nil {
|
||||
return nil, status, err
|
||||
}
|
||||
out := map[string]time.Time{}
|
||||
for k, v := range fixed {
|
||||
out[k] = v.(time.Time)
|
||||
}
|
||||
return out, status, err
|
||||
}
|
||||
|
||||
46
stripmd.go
@@ -3,22 +3,57 @@ package main
|
||||
import (
|
||||
"strings"
|
||||
|
||||
dg "github.com/bwmarrin/discordgo"
|
||||
stripmd "github.com/writeas/go-strip-markdown"
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
Alt, URL string
|
||||
}
|
||||
|
||||
// StripAltText removes Markdown alt text from links and images and replaces them with just the URL.
|
||||
// Currently uses the deepest alt text when links/images are nested.
|
||||
func StripAltText(md string) string {
|
||||
// If links = true, links are completely removed, and a list of URLs and their alt text is also returned.
|
||||
func StripAltText(md string, links bool) (string, []*dg.MessageEmbed) {
|
||||
altTextStart := -1 // Start of alt text (between '[' & ']')
|
||||
URLStart := -1 // Start of url (between '(' & ')')
|
||||
URLEnd := -1
|
||||
previousURLEnd := -2
|
||||
out := ""
|
||||
embeds := []*dg.MessageEmbed{}
|
||||
for i := range md {
|
||||
if altTextStart != -1 && URLStart != -1 && md[i] == ')' {
|
||||
URLEnd = i - 1
|
||||
out += md[previousURLEnd+2:altTextStart-1] + md[URLStart:URLEnd+1]
|
||||
out += md[previousURLEnd+2 : altTextStart-1]
|
||||
if links {
|
||||
embed := &dg.MessageEmbed{
|
||||
Type: dg.EmbedTypeLink,
|
||||
Title: md[altTextStart : URLStart-2],
|
||||
}
|
||||
if md[altTextStart-1] == '!' {
|
||||
embed.Title = md[altTextStart+1 : URLStart-2]
|
||||
embed.Type = dg.EmbedTypeImage
|
||||
embed.Image = &dg.MessageEmbedImage{
|
||||
URL: md[URLStart : URLEnd+1],
|
||||
}
|
||||
} else {
|
||||
embed.URL = md[URLStart : URLEnd+1]
|
||||
}
|
||||
embeds = append(embeds, embed)
|
||||
} else {
|
||||
out += md[URLStart : URLEnd+1]
|
||||
}
|
||||
previousURLEnd = URLEnd
|
||||
// Removing links often leaves a load of extra newlines which look weird, this removes them.
|
||||
if links {
|
||||
next := 2
|
||||
for md[URLEnd+next] == '\n' {
|
||||
next++
|
||||
}
|
||||
if next >= 3 {
|
||||
previousURLEnd += next - 2
|
||||
}
|
||||
}
|
||||
altTextStart, URLStart, URLEnd = -1, -1, -1
|
||||
continue
|
||||
}
|
||||
@@ -36,11 +71,12 @@ func StripAltText(md string) string {
|
||||
out += md[previousURLEnd+2:]
|
||||
}
|
||||
if out == "" {
|
||||
return md
|
||||
return md, embeds
|
||||
}
|
||||
return out
|
||||
return out, embeds
|
||||
}
|
||||
|
||||
func stripMarkdown(md string) string {
|
||||
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(StripAltText(md)), "</p>"), "<p>")
|
||||
stripped, _ := StripAltText(md, false)
|
||||
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(stripped), "</p>"), "<p>")
|
||||
}
|
||||
|
||||
154
telegram.go
@@ -9,7 +9,7 @@ import (
|
||||
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
)
|
||||
|
||||
type VerifiedToken struct {
|
||||
type TelegramVerifiedToken struct {
|
||||
Token string
|
||||
ChatID int64
|
||||
Username string
|
||||
@@ -21,7 +21,7 @@ type TelegramDaemon struct {
|
||||
bot *tg.BotAPI
|
||||
username string
|
||||
tokens []string
|
||||
verifiedTokens []VerifiedToken
|
||||
verifiedTokens []TelegramVerifiedToken
|
||||
languages map[int64]string // Store of languages for chatIDs. Added to on first interaction, and loaded from app.storage.telegram on start.
|
||||
link string
|
||||
app *appContext
|
||||
@@ -37,12 +37,11 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
|
||||
return nil, err
|
||||
}
|
||||
td := &TelegramDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
bot: bot,
|
||||
username: bot.Self.UserName,
|
||||
tokens: []string{},
|
||||
verifiedTokens: []VerifiedToken{},
|
||||
verifiedTokens: []TelegramVerifiedToken{},
|
||||
languages: map[int64]string{},
|
||||
link: "https://t.me/" + bot.Self.UserName,
|
||||
app: app,
|
||||
@@ -55,23 +54,28 @@ func newTelegramDaemon(app *appContext) (*TelegramDaemon, error) {
|
||||
return td, nil
|
||||
}
|
||||
|
||||
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||
func (t *TelegramDaemon) NewAuthToken() string {
|
||||
func genAuthToken() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
pin := make([]rune, 8)
|
||||
for i := range pin {
|
||||
if i == 2 || i == 5 {
|
||||
if (i+1)%3 == 0 {
|
||||
pin[i] = '-'
|
||||
} else {
|
||||
pin[i] = runes[rand.Intn(len(runes))]
|
||||
}
|
||||
}
|
||||
t.tokens = append(t.tokens, string(pin))
|
||||
return string(pin)
|
||||
}
|
||||
|
||||
var runes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||
func (t *TelegramDaemon) NewAuthToken() string {
|
||||
pin := genAuthToken()
|
||||
t.tokens = append(t.tokens, pin)
|
||||
return pin
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) run() {
|
||||
t.app.info.Println("Starting Telegram bot daemon")
|
||||
u := tg.NewUpdate(0)
|
||||
@@ -79,6 +83,7 @@ func (t *TelegramDaemon) run() {
|
||||
updates, err := t.bot.GetUpdatesChan(u)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Failed to start Telegram daemon: %v", err)
|
||||
telegramEnabled = false
|
||||
return
|
||||
}
|
||||
for {
|
||||
@@ -111,66 +116,13 @@ func (t *TelegramDaemon) run() {
|
||||
}
|
||||
switch msg := sects[0]; msg {
|
||||
case "/start":
|
||||
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
||||
content += t.app.storage.lang.Telegram[lang].Strings.get("languageMessage")
|
||||
err := t.Reply(&upd, content)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
t.commandStart(&upd, sects, lang)
|
||||
continue
|
||||
case "/lang":
|
||||
if len(sects) == 1 {
|
||||
list := "/lang <lang>\n"
|
||||
for code := range t.app.storage.lang.Telegram {
|
||||
list += fmt.Sprintf("%s: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
|
||||
}
|
||||
err := t.Reply(&upd, list)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, ok := t.app.storage.lang.Telegram[sects[1]]; ok {
|
||||
t.languages[upd.Message.Chat.ID] = sects[1]
|
||||
for jfID, user := range t.app.storage.telegram {
|
||||
if user.ChatID == upd.Message.Chat.ID {
|
||||
user.Lang = sects[1]
|
||||
t.app.storage.telegram[jfID] = user
|
||||
err := t.app.storage.storeTelegramUsers()
|
||||
if err != nil {
|
||||
t.app.err.Printf("Failed to store Telegram users: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
t.commandLang(&upd, sects, lang)
|
||||
continue
|
||||
default:
|
||||
tokenIndex := -1
|
||||
for i, token := range t.tokens {
|
||||
if upd.Message.Text == token {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if tokenIndex == -1 {
|
||||
err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
err := t.QuoteReply(&upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
t.verifiedTokens = append(t.verifiedTokens, VerifiedToken{
|
||||
Token: upd.Message.Text,
|
||||
ChatID: upd.Message.Chat.ID,
|
||||
Username: upd.Message.Chat.UserName,
|
||||
})
|
||||
t.tokens[len(t.tokens)-1], t.tokens[tokenIndex] = t.tokens[tokenIndex], t.tokens[len(t.tokens)-1]
|
||||
t.tokens = t.tokens[:len(t.tokens)-1]
|
||||
t.commandPIN(&upd, sects, lang)
|
||||
}
|
||||
|
||||
case <-t.ShutdownChannel:
|
||||
@@ -194,6 +146,9 @@ func (t *TelegramDaemon) QuoteReply(upd *tg.Update, content string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var escapedChars = []string{"_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(", "\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>", "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|", "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!"}
|
||||
var escaper = strings.NewReplacer(escapedChars...)
|
||||
|
||||
// Send will send a telegram message to a list of chat IDs. message.text is used if no markdown is given.
|
||||
func (t *TelegramDaemon) Send(message *Message, ID ...int64) error {
|
||||
for _, id := range ID {
|
||||
@@ -201,7 +156,8 @@ func (t *TelegramDaemon) Send(message *Message, ID ...int64) error {
|
||||
if message.Markdown == "" {
|
||||
msg = tg.NewMessage(id, message.Text)
|
||||
} else {
|
||||
msg = tg.NewMessage(id, strings.ReplaceAll(message.Markdown, ".", "\\."))
|
||||
text := escaper.Replace(message.Markdown)
|
||||
msg = tg.NewMessage(id, text)
|
||||
msg.ParseMode = "MarkdownV2"
|
||||
}
|
||||
_, err := t.bot.Send(msg)
|
||||
@@ -218,3 +174,67 @@ func (t *TelegramDaemon) Shutdown() {
|
||||
<-t.ShutdownChannel
|
||||
close(t.ShutdownChannel)
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) commandStart(upd *tg.Update, sects []string, lang string) {
|
||||
content := t.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
||||
content += t.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "/lang"})
|
||||
err := t.Reply(upd, content)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) commandLang(upd *tg.Update, sects []string, lang string) {
|
||||
if len(sects) == 1 {
|
||||
list := "/lang `<lang>`\n"
|
||||
for code := range t.app.storage.lang.Telegram {
|
||||
list += fmt.Sprintf("`%s`: %s\n", code, t.app.storage.lang.Telegram[code].Meta.Name)
|
||||
}
|
||||
err := t.Reply(upd, list)
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, ok := t.app.storage.lang.Telegram[sects[1]]; ok {
|
||||
t.languages[upd.Message.Chat.ID] = sects[1]
|
||||
for jfID, user := range t.app.storage.telegram {
|
||||
if user.ChatID == upd.Message.Chat.ID {
|
||||
user.Lang = sects[1]
|
||||
t.app.storage.telegram[jfID] = user
|
||||
if err := t.app.storage.storeTelegramUsers(); err != nil {
|
||||
t.app.err.Printf("Failed to store Telegram users: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TelegramDaemon) commandPIN(upd *tg.Update, sects []string, lang string) {
|
||||
tokenIndex := -1
|
||||
for i, token := range t.tokens {
|
||||
if upd.Message.Text == token {
|
||||
tokenIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if tokenIndex == -1 {
|
||||
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"))
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err := t.QuoteReply(upd, t.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"))
|
||||
if err != nil {
|
||||
t.app.err.Printf("Telegram: Failed to send message to \"%s\": %v", upd.Message.From.UserName, err)
|
||||
}
|
||||
t.verifiedTokens = append(t.verifiedTokens, TelegramVerifiedToken{
|
||||
Token: upd.Message.Text,
|
||||
ChatID: upd.Message.Chat.ID,
|
||||
Username: upd.Message.Chat.UserName,
|
||||
})
|
||||
t.tokens[len(t.tokens)-1], t.tokens[tokenIndex] = t.tokens[tokenIndex], t.tokens[len(t.tokens)-1]
|
||||
t.tokens = t.tokens[:len(t.tokens)-1]
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
module github.com/hrfee/jfa-go/telegram
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
)
|
||||
@@ -1,4 +0,0 @@
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||
@@ -1,59 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
tg "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
)
|
||||
|
||||
const (
|
||||
TOKEN = "1785754648:AAG4G6PKZpGDEJM_-MeQHJqD-xUDrrLrTC4"
|
||||
USER = "johnikwock"
|
||||
|
||||
AUTH = "AB-CD-EF"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("Connecting...")
|
||||
bot, err := tg.NewBotAPI(TOKEN)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize bot: %v", err)
|
||||
}
|
||||
bot.Debug = false
|
||||
log.Printf("Authorized Telegram bot \"%s\"", bot.Self.UserName)
|
||||
|
||||
u := tg.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
|
||||
updates, err := bot.GetUpdatesChan(u)
|
||||
|
||||
for update := range updates {
|
||||
if update.Message == nil {
|
||||
continue
|
||||
}
|
||||
log.Printf("New message from \"@%s\": \"%s\"", update.Message.From.UserName, update.Message.Text)
|
||||
if update.Message.From.UserName != USER {
|
||||
continue
|
||||
}
|
||||
var msg tg.MessageConfig
|
||||
sects := strings.Split(update.Message.Text, " ")
|
||||
if sects[0] == "/start" {
|
||||
msg = tg.NewMessage(update.Message.Chat.ID, fmt.Sprintf("Enter this code on the sign-up page to continue: %s", AUTH))
|
||||
} else if sects[0] != "/auth" || sects[len(sects)-1] != AUTH {
|
||||
log.Println("Invalid command or auth token")
|
||||
msg = tg.NewMessage(update.Message.Chat.ID, "Invalid command or token")
|
||||
} else {
|
||||
msg = tg.NewMessage(update.Message.Chat.ID, "Success!")
|
||||
log.Println("Successful auth")
|
||||
}
|
||||
msg.ReplyToMessageID = update.Message.MessageID
|
||||
|
||||
_, err := bot.Send(msg)
|
||||
if err != nil {
|
||||
log.Printf("Send failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
API key: 1785754648:AAG4G6PKZpGDEJM_-MeQHJqD-xUDrrLrTC4
|
||||
|
||||
Name: jfa-bot
|
||||
Username: jfago_bot
|
||||
105
tray.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// +build tray
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
// "github.com/getlantern/systray"
|
||||
)
|
||||
|
||||
var TRAY = true
|
||||
|
||||
func RunTray() {
|
||||
systray.Run(onReady, onExit)
|
||||
}
|
||||
|
||||
func onExit() {
|
||||
if RUNNING {
|
||||
QUIT = true
|
||||
RESTART <- true
|
||||
}
|
||||
os.Remove(SOCK)
|
||||
}
|
||||
|
||||
func onReady() {
|
||||
icon, err := localFS.ReadFile("web/favicon.ico")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load favicon: %v", err)
|
||||
}
|
||||
systray.SetIcon(icon)
|
||||
systray.SetTitle("jfa-go")
|
||||
mStart := systray.AddMenuItem("Start", "Start jfa-go")
|
||||
mStop := systray.AddMenuItem("Stop", "Stop jfa-go")
|
||||
mRestart := systray.AddMenuItem("Restart", "Restart jfa-go")
|
||||
mOpenLogs := systray.AddMenuItem("Open logs", "Open jfa-go log file.")
|
||||
as := NewAutostart("jfa-go", "A user management system for Jellyfin", "Run on login", "Run jfa-go on user login.")
|
||||
mQuit := systray.AddMenuItem("Quit", "Quit jfa-go")
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
systray.Quit()
|
||||
os.Exit(1)
|
||||
}()
|
||||
defer func() {
|
||||
systray.Quit()
|
||||
}()
|
||||
|
||||
RESTART = make(chan bool, 1)
|
||||
TRAYRESTART = make(chan bool, 1)
|
||||
go start(false, true)
|
||||
mStart.Disable()
|
||||
mStop.Enable()
|
||||
mRestart.Enable()
|
||||
go as.HandleCheck()
|
||||
trayRestart := func() {
|
||||
if RUNNING {
|
||||
RESTART <- true
|
||||
mStop.Disable()
|
||||
mStart.Enable()
|
||||
mRestart.Disable()
|
||||
for {
|
||||
if !RUNNING {
|
||||
break
|
||||
}
|
||||
}
|
||||
go start(false, false)
|
||||
mStart.Disable()
|
||||
mStop.Enable()
|
||||
mRestart.Enable()
|
||||
}
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-mStart.ClickedCh:
|
||||
if !RUNNING {
|
||||
go start(false, false)
|
||||
mStart.Disable()
|
||||
mStop.Enable()
|
||||
mRestart.Enable()
|
||||
}
|
||||
case <-mStop.ClickedCh:
|
||||
if RUNNING {
|
||||
RESTART <- true
|
||||
mStop.Disable()
|
||||
mStart.Enable()
|
||||
mRestart.Disable()
|
||||
}
|
||||
case <-TRAYRESTART:
|
||||
trayRestart()
|
||||
case <-mRestart.ClickedCh:
|
||||
trayRestart()
|
||||
case <-mOpenLogs.ClickedCh:
|
||||
open.Start(logPath)
|
||||
case <-mQuit.ClickedCh:
|
||||
systray.Quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,9 +63,15 @@ window.availableProfiles = window.availableProfiles || [];
|
||||
|
||||
window.modals.updateInfo = new Modal(document.getElementById("modal-update"));
|
||||
|
||||
window.modals.matrix = new Modal(document.getElementById("modal-matrix"));
|
||||
|
||||
if (window.telegramEnabled) {
|
||||
window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
|
||||
}
|
||||
|
||||
if (window.discordEnabled) {
|
||||
window.modals.discord = new Modal(document.getElementById("modal-discord"));
|
||||
}
|
||||
})();
|
||||
|
||||
var inviteCreator = new createInvite();
|
||||
|
||||
39
ts/crash.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { toClipboard } from "./modules/common.js";
|
||||
|
||||
const buttonNormal = document.getElementById("button-log-normal") as HTMLInputElement;
|
||||
const buttonSanitized = document.getElementById("button-log-sanitized") as HTMLInputElement;
|
||||
|
||||
const logNormal = document.getElementById("log-normal") as HTMLInputElement;
|
||||
const logSanitized = document.getElementById("log-sanitized") as HTMLInputElement;
|
||||
|
||||
const buttonChange = (type: string) => {
|
||||
console.log("RUN");
|
||||
if (type == "normal") {
|
||||
logSanitized.classList.add("unfocused");
|
||||
logNormal.classList.remove("unfocused");
|
||||
buttonNormal.classList.add("!high");
|
||||
buttonNormal.classList.remove("!normal");
|
||||
buttonSanitized.classList.add("!normal");
|
||||
buttonSanitized.classList.remove("!high");
|
||||
} else {
|
||||
logNormal.classList.add("unfocused");
|
||||
logSanitized.classList.remove("unfocused");
|
||||
buttonSanitized.classList.add("!high");
|
||||
buttonSanitized.classList.remove("!normal");
|
||||
buttonNormal.classList.add("!normal");
|
||||
buttonNormal.classList.remove("!high");
|
||||
}
|
||||
}
|
||||
buttonNormal.onclick = () => buttonChange("normal");
|
||||
buttonSanitized.onclick = () => buttonChange("sanitized");
|
||||
|
||||
const copyButton = document.getElementById("copy-log") as HTMLSpanElement;
|
||||
copyButton.onclick = () => {
|
||||
if (logSanitized.classList.contains("unfocused")) {
|
||||
toClipboard("```\n" + logNormal.textContent + "```");
|
||||
} else {
|
||||
toClipboard("```\n" + logSanitized.textContent + "```");
|
||||
}
|
||||
copyButton.textContent = "Copied.";
|
||||
setTimeout(() => { copyButton.textContent = "Copy"; }, 1500);
|
||||
};
|
||||
300
ts/form.ts
@@ -1,19 +1,28 @@
|
||||
import { Modal } from "./modules/modal.js";
|
||||
import { notificationBox, whichAnimationEvent } from "./modules/common.js";
|
||||
import { _get, _post, toggleLoader, toDateString } from "./modules/common.js";
|
||||
import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from "./modules/common.js";
|
||||
import { loadLangSelector } from "./modules/lang.js";
|
||||
import { initValidator } from "./modules/validator.js";
|
||||
|
||||
interface formWindow extends Window {
|
||||
validationStrings: pwValStrings;
|
||||
invalidPassword: string;
|
||||
successModal: Modal;
|
||||
telegramModal: Modal;
|
||||
discordModal: Modal;
|
||||
matrixModal: Modal;
|
||||
confirmationModal: Modal
|
||||
code: string;
|
||||
messages: { [key: string]: string };
|
||||
confirmation: boolean;
|
||||
telegramRequired: boolean;
|
||||
telegramPIN: string;
|
||||
discordRequired: boolean;
|
||||
discordPIN: string;
|
||||
discordStartCommand: string;
|
||||
discordInviteLink: boolean;
|
||||
discordServerName: string;
|
||||
matrixRequired: boolean;
|
||||
matrixUserID: string;
|
||||
userExpiryEnabled: boolean;
|
||||
userExpiryMonths: number;
|
||||
userExpiryDays: number;
|
||||
@@ -22,20 +31,6 @@ interface formWindow extends Window {
|
||||
userExpiryMessage: string;
|
||||
}
|
||||
|
||||
interface pwValString {
|
||||
singular: string;
|
||||
plural: string;
|
||||
}
|
||||
|
||||
interface pwValStrings {
|
||||
length: pwValString;
|
||||
uppercase: pwValString;
|
||||
lowercase: pwValString;
|
||||
number: pwValString;
|
||||
special: pwValString;
|
||||
[ type: string ]: pwValString;
|
||||
}
|
||||
|
||||
loadLangSelector("form");
|
||||
|
||||
window.notifications = new notificationBox(document.getElementById("notification-box") as HTMLDivElement);
|
||||
@@ -68,7 +63,7 @@ if (window.telegramEnabled) {
|
||||
telegramVerified = true;
|
||||
waiting.classList.add("~positive");
|
||||
waiting.classList.remove("~info");
|
||||
window.notifications.customPositive("telegramVerified", "", window.messages["telegramVerified"]);
|
||||
window.notifications.customPositive("telegramVerified", "", window.messages["verified"]);
|
||||
setTimeout(window.telegramModal.close, 2000);
|
||||
telegramButton.classList.add("unfocused");
|
||||
document.getElementById("contact-via").classList.remove("unfocused");
|
||||
@@ -84,6 +79,129 @@ if (window.telegramEnabled) {
|
||||
};
|
||||
}
|
||||
|
||||
interface DiscordInvite {
|
||||
invite: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
var discordVerified = false;
|
||||
if (window.discordEnabled) {
|
||||
window.discordModal = new Modal(document.getElementById("modal-discord"), window.discordRequired);
|
||||
const discordButton = document.getElementById("link-discord") as HTMLSpanElement;
|
||||
if (window.discordInviteLink) {
|
||||
_get("/invite/" + window.code + "/discord/invite", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200) {
|
||||
return;
|
||||
}
|
||||
const inv = req.response as DiscordInvite;
|
||||
const link = document.getElementById("discord-invite") as HTMLAnchorElement;
|
||||
link.classList.add("subheading", "link-center");
|
||||
link.href = inv.invite;
|
||||
link.target = "_blank";
|
||||
link.innerHTML = `<span class="img-circle lg mr-1"><img class="img-circle" src="${inv.icon}" width="64" height="64"></span>${window.discordServerName}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
discordButton.onclick = () => {
|
||||
const waiting = document.getElementById("discord-waiting") as HTMLSpanElement;
|
||||
toggleLoader(waiting);
|
||||
window.discordModal.show();
|
||||
let modalClosed = false;
|
||||
window.discordModal.onclose = () => {
|
||||
modalClosed = true;
|
||||
toggleLoader(waiting);
|
||||
}
|
||||
const checkVerified = () => _get("/invite/" + window.code + "/discord/verified/" + window.discordPIN, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status == 401) {
|
||||
window.discordModal.close();
|
||||
window.notifications.customError("invalidCodeError", window.messages["errorInvalidCode"]);
|
||||
return;
|
||||
} else if (req.status == 200) {
|
||||
if (req.response["success"] as boolean) {
|
||||
discordVerified = true;
|
||||
waiting.classList.add("~positive");
|
||||
waiting.classList.remove("~info");
|
||||
window.notifications.customPositive("discordVerified", "", window.messages["verified"]);
|
||||
setTimeout(window.discordModal.close, 2000);
|
||||
discordButton.classList.add("unfocused");
|
||||
document.getElementById("contact-via").classList.remove("unfocused");
|
||||
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
|
||||
radio.checked = true;
|
||||
} else if (!modalClosed) {
|
||||
setTimeout(checkVerified, 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
checkVerified();
|
||||
};
|
||||
}
|
||||
|
||||
var matrixVerified = false;
|
||||
var matrixPIN = "";
|
||||
if (window.matrixEnabled) {
|
||||
window.matrixModal = new Modal(document.getElementById("modal-matrix"), window.matrixRequired);
|
||||
const matrixButton = document.getElementById("link-matrix") as HTMLSpanElement;
|
||||
matrixButton.onclick = window.matrixModal.show;
|
||||
const submitButton = document.getElementById("matrix-send") as HTMLSpanElement;
|
||||
const input = document.getElementById("matrix-userid") as HTMLInputElement;
|
||||
let userID = "";
|
||||
submitButton.onclick = () => {
|
||||
addLoader(submitButton);
|
||||
if (userID == "") {
|
||||
const send = {
|
||||
user_id: input.value
|
||||
};
|
||||
_post("/invite/" + window.code + "/matrix/user", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
removeLoader(submitButton);
|
||||
userID = input.value;
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("errorUnknown", window.messages["errorUnknown"]);
|
||||
window.matrixModal.close();
|
||||
return;
|
||||
}
|
||||
submitButton.classList.add("~positive");
|
||||
submitButton.classList.remove("~info");
|
||||
setTimeout(() => {
|
||||
submitButton.classList.add("~info");
|
||||
submitButton.classList.remove("~positive");
|
||||
}, 2000);
|
||||
input.placeholder = "PIN";
|
||||
input.value = "";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_get("/invite/" + window.code + "/matrix/verified/" + userID + "/" + input.value, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
removeLoader(submitButton)
|
||||
const valid = req.response["success"] as boolean;
|
||||
if (valid) {
|
||||
window.matrixModal.close();
|
||||
window.notifications.customPositive("successVerified", "", window.messages["verified"]);
|
||||
matrixVerified = true;
|
||||
matrixPIN = input.value;
|
||||
matrixButton.classList.add("unfocused");
|
||||
document.getElementById("contact-via").classList.remove("unfocused");
|
||||
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
|
||||
radio.checked = true;
|
||||
} else {
|
||||
window.notifications.customError("errorInvalidPIN", window.messages["errorInvalidPIN"]);
|
||||
submitButton.classList.add("~critical");
|
||||
submitButton.classList.remove("~info");
|
||||
setTimeout(() => {
|
||||
submitButton.classList.add("~info");
|
||||
submitButton.classList.remove("~critical");
|
||||
}, 800);
|
||||
}
|
||||
}
|
||||
},);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (window.confirmation) {
|
||||
window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true);
|
||||
}
|
||||
@@ -103,29 +221,6 @@ if (window.userExpiryEnabled) {
|
||||
calculateTime();
|
||||
}
|
||||
|
||||
var defaultPwValStrings: pwValStrings = {
|
||||
length: {
|
||||
singular: "Must have at least {n} character",
|
||||
plural: "Must have at least {n} characters"
|
||||
},
|
||||
uppercase: {
|
||||
singular: "Must have at least {n} uppercase character",
|
||||
plural: "Must have at least {n} uppercase characters"
|
||||
},
|
||||
lowercase: {
|
||||
singular: "Must have at least {n} lowercase character",
|
||||
plural: "Must have at least {n} lowercase characters"
|
||||
},
|
||||
number: {
|
||||
singular: "Must have at least {n} number",
|
||||
plural: "Must have at least {n} numbers"
|
||||
},
|
||||
special: {
|
||||
singular: "Must have at least {n} special character",
|
||||
plural: "Must have at least {n} special characters"
|
||||
}
|
||||
}
|
||||
|
||||
const form = document.getElementById("form-create") as HTMLFormElement;
|
||||
const submitButton = form.querySelector("input[type=submit]") as HTMLInputElement;
|
||||
const submitSpan = form.querySelector("span.submit") as HTMLSpanElement;
|
||||
@@ -135,19 +230,7 @@ if (!window.usernameEnabled) { usernameField.parentElement.remove(); usernameFie
|
||||
const passwordField = document.getElementById("create-password") as HTMLInputElement;
|
||||
const rePasswordField = document.getElementById("create-reenter-password") as HTMLInputElement;
|
||||
|
||||
const checkPasswords = () => {
|
||||
if (passwordField.value != rePasswordField.value) {
|
||||
rePasswordField.setCustomValidity(window.invalidPassword);
|
||||
submitButton.disabled = true;
|
||||
submitSpan.setAttribute("disabled", "");
|
||||
} else {
|
||||
rePasswordField.setCustomValidity("");
|
||||
submitButton.disabled = false;
|
||||
submitSpan.removeAttribute("disabled");
|
||||
}
|
||||
};
|
||||
rePasswordField.addEventListener("keyup", checkPasswords);
|
||||
passwordField.addEventListener("keyup", checkPasswords);
|
||||
var requirements = initValidator(passwordField, rePasswordField, submitButton, submitSpan)
|
||||
|
||||
interface respDTO {
|
||||
response: boolean;
|
||||
@@ -161,6 +244,10 @@ interface sendDTO {
|
||||
password: string;
|
||||
telegram_pin?: string;
|
||||
telegram_contact?: boolean;
|
||||
discord_pin?: string;
|
||||
discord_contact?: boolean;
|
||||
matrix_pin?: string;
|
||||
matrix_contact?: boolean;
|
||||
}
|
||||
|
||||
const create = (event: SubmitEvent) => {
|
||||
@@ -179,6 +266,20 @@ const create = (event: SubmitEvent) => {
|
||||
send.telegram_contact = true;
|
||||
}
|
||||
}
|
||||
if (discordVerified) {
|
||||
send.discord_pin = window.discordPIN;
|
||||
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
|
||||
if (radio.checked) {
|
||||
send.discord_contact = true;
|
||||
}
|
||||
}
|
||||
if (matrixVerified) {
|
||||
send.matrix_pin = matrixPIN;
|
||||
const radio = document.getElementById("contact-via-matrix") as HTMLInputElement;
|
||||
if (radio.checked) {
|
||||
send.matrix_contact = true;
|
||||
}
|
||||
}
|
||||
_post("/newUser", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
let vals = req.response as respDTO;
|
||||
@@ -221,96 +322,3 @@ const create = (event: SubmitEvent) => {
|
||||
};
|
||||
|
||||
form.onsubmit = create;
|
||||
|
||||
class Requirement {
|
||||
private _name: string;
|
||||
protected _minCount: number;
|
||||
private _content: HTMLSpanElement;
|
||||
private _valid: HTMLSpanElement;
|
||||
private _li: HTMLLIElement;
|
||||
|
||||
get valid(): boolean { return this._valid.classList.contains("~positive"); }
|
||||
set valid(state: boolean) {
|
||||
if (state) {
|
||||
this._valid.classList.add("~positive");
|
||||
this._valid.classList.remove("~critical");
|
||||
this._valid.innerHTML = `<i class="icon ri-check-line" title="valid"></i>`;
|
||||
} else {
|
||||
this._valid.classList.add("~critical");
|
||||
this._valid.classList.remove("~positive");
|
||||
this._valid.innerHTML = `<i class="icon ri-close-line" title="invalid"></i>`;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(name: string, el: HTMLLIElement) {
|
||||
this._name = name;
|
||||
this._li = el;
|
||||
this._content = this._li.querySelector("span.requirement-content") as HTMLSpanElement;
|
||||
this._valid = this._li.querySelector("span.requirement-valid") as HTMLSpanElement;
|
||||
this.valid = false;
|
||||
this._minCount = +this._li.getAttribute("min");
|
||||
|
||||
let text = "";
|
||||
if (this._minCount == 1) {
|
||||
text = window.validationStrings[this._name].singular.replace("{n}", "1");
|
||||
} else {
|
||||
text = window.validationStrings[this._name].plural.replace("{n}", ""+this._minCount);
|
||||
}
|
||||
this._content.textContent = text;
|
||||
}
|
||||
|
||||
validate = (count: number) => { this.valid = (count >= this._minCount); }
|
||||
}
|
||||
|
||||
// Incredible code right here
|
||||
const isInt = (s: string): boolean => { return (s in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]); }
|
||||
|
||||
const testStrings = (f: pwValString): boolean => {
|
||||
const testString = (s: string): boolean => {
|
||||
if (s == "" || !s.includes("{n}")) { return false; }
|
||||
return true;
|
||||
}
|
||||
return testString(f.singular) && testString(f.plural);
|
||||
}
|
||||
|
||||
interface Validation { [name: string]: number }
|
||||
|
||||
const validate = (s: string): Validation => {
|
||||
let v: Validation = {};
|
||||
for (let criteria of ["length", "lowercase", "uppercase", "number", "special"]) { v[criteria] = 0; }
|
||||
v["length"] = s.length;
|
||||
for (let c of s) {
|
||||
if (isInt(c)) { v["number"]++; }
|
||||
else {
|
||||
const upper = c.toUpperCase();
|
||||
if (upper == c.toLowerCase()) { v["special"]++; }
|
||||
else {
|
||||
if (upper == c) { v["uppercase"]++; }
|
||||
else if (upper != c) { v["lowercase"]++; }
|
||||
}
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
passwordField.addEventListener("keyup", () => {
|
||||
const v = validate(passwordField.value);
|
||||
for (let criteria in requirements) {
|
||||
requirements[criteria].validate(v[criteria]);
|
||||
}
|
||||
});
|
||||
|
||||
var requirements: { [category: string]: Requirement} = {};
|
||||
|
||||
if (!window.validationStrings) {
|
||||
window.validationStrings = defaultPwValStrings;
|
||||
} else {
|
||||
for (let category in window.validationStrings) {
|
||||
if (!testStrings(window.validationStrings[category])) {
|
||||
window.validationStrings[category] = defaultPwValStrings[category];
|
||||
}
|
||||
const el = document.getElementById("requirement-" + category);
|
||||
if (el) {
|
||||
requirements[category] = new Requirement(category, el as HTMLLIElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||