Compare commits
134 Commits
jellyseerr
...
paramateri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
632393b88d | ||
|
|
d2da9048d7 | ||
|
|
f1b56268bb | ||
|
|
acba411c3a | ||
|
|
f26042a21e | ||
|
|
0967d471ee | ||
|
|
302c4c189c | ||
|
|
c52ba2162e | ||
|
|
2d98c6cff4 | ||
|
|
d710b9ad4d | ||
|
|
5cc97eaf17 | ||
|
|
dca83dcc8e | ||
|
|
3c0f3e90d8 | ||
|
|
d6f5c91d78 | ||
|
|
0c257b7342 | ||
|
|
c5f4098b5b | ||
|
|
0b9206012f | ||
|
|
41dff3d5bb | ||
|
|
8f3f1fcda8 | ||
|
|
93ae2ac6d8 | ||
|
|
6404fda04d | ||
|
|
3133996f33 | ||
|
|
cef84fad10 | ||
|
|
58c2fa3dde | ||
|
|
56ee54811d | ||
|
|
c1a2fb2d4a | ||
|
|
0b73e3ff2b | ||
|
|
0e9a7d0641 | ||
|
|
dda363b344 | ||
|
|
0ccc314833 | ||
|
|
da4470bc4f | ||
|
|
11eb907ced | ||
|
|
ea57d657fe | ||
|
|
71922212d9 | ||
|
|
bb41bc3844 | ||
|
|
39f6d14163 | ||
|
|
941367f77b | ||
|
|
2f4e68969a | ||
|
|
275d9188bf | ||
|
|
4b564d7f4a | ||
|
|
c7995cdbba | ||
|
|
e623897fc1 | ||
|
|
ee05f4fc19 | ||
|
|
436a1db087 | ||
|
|
f4a7238110 | ||
|
|
65662c57bc | ||
|
|
3559e32c2f | ||
|
|
a1612949bf | ||
|
|
ae808c5109 | ||
|
|
418f3c4566 | ||
|
|
399ce3b044 | ||
|
|
1aa100dc7d | ||
|
|
6347495b5b | ||
|
|
02f4ba6e8e | ||
|
|
d2e5209832 | ||
|
|
b5dea7755b | ||
|
|
848b532b3c | ||
|
|
a11ac1b1a3 | ||
|
|
73197df8d9 | ||
|
|
a492c06077 | ||
|
|
bc66f46d9e | ||
|
|
eefd350754 | ||
|
|
494b8b2399 | ||
|
|
e901ba6bb5 | ||
|
|
4455c15bca | ||
|
|
c2bdc67242 | ||
|
|
37545e1e36 | ||
|
|
19495be6e9 | ||
|
|
37a062e24d | ||
|
|
a4c60c71ea | ||
|
|
9e9f46d97b | ||
|
|
f063b970b4 | ||
|
|
711b817cff | ||
|
|
fcdd4e4518 | ||
|
|
6c30a1ff40 | ||
|
|
a7aa3fd53e | ||
|
|
32161139b2 | ||
|
|
7c808b56f7 | ||
|
|
2057823b7a | ||
|
|
e5f79c60ae | ||
|
|
8307d3da90 | ||
|
|
6bad293f74 | ||
|
|
b2771e6cc5 | ||
|
|
e71d492495 | ||
|
|
0e7245e6b9 | ||
|
|
59e9d457c2 | ||
|
|
48be756e48 | ||
|
|
ab3989f233 | ||
|
|
d2c7bf06f7 | ||
|
|
3f59312dfc | ||
|
|
d62add0195 | ||
|
|
fd32b73132 | ||
|
|
2d7f44eeec | ||
|
|
cf5ec3b319 | ||
|
|
2237286656 | ||
|
|
3a0f61e324 | ||
|
|
69569e556a | ||
|
|
86c7551ff8 | ||
|
|
a52dd26ec6 | ||
|
|
6308db495a | ||
|
|
ef7132bf3d | ||
|
|
790accc007 | ||
|
|
2310130e6b | ||
|
|
284312713c | ||
|
|
b40211a6e0 | ||
|
|
ce6a5772b1 | ||
|
|
86c37fb423 | ||
|
|
3c3297c8e7 | ||
|
|
a9dc601751 | ||
|
|
51e3c37694 | ||
|
|
62e27c394d | ||
|
|
8f3c723b07 | ||
|
|
3c28537498 | ||
|
|
b0e94a4ef6 | ||
|
|
baeb89b694 | ||
|
|
016263894f | ||
|
|
5fe532fb78 | ||
|
|
598859ae31 | ||
|
|
d8dcb84870 | ||
|
|
28ca02272c | ||
|
|
448955c915 | ||
|
|
ffd46ff190 | ||
|
|
44311162a6 | ||
|
|
f289680d98 | ||
|
|
280c6e4f16 | ||
|
|
54e4a51a7f | ||
|
|
711394232b | ||
|
|
15a317f84f | ||
|
|
f348262f88 | ||
|
|
e9b8d970d1 | ||
|
|
fb5d3c4165 | ||
|
|
c442ff5f98 | ||
|
|
2d066ea7cd | ||
|
|
efa113ab5f |
172
.drone.yml.old
@@ -1,172 +0,0 @@
|
||||
---
|
||||
name: jfa-go
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: fetch
|
||||
image: docker:git
|
||||
commands:
|
||||
- git fetch --tags
|
||||
- name: release
|
||||
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
|
||||
JFA_GO_BUILT_BY:
|
||||
from_secret: BUILT_BY
|
||||
commands:
|
||||
- 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 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "rm /repo/incoming/*.deb"'
|
||||
- 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
|
||||
---
|
||||
name: docker-buildx
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: build-deploy
|
||||
image: appleboy/drone-ssh
|
||||
environment:
|
||||
BUILDRONE_KEY:
|
||||
from_secret: BUILDRONE_KEY
|
||||
settings:
|
||||
host:
|
||||
from_secret: ssh2_host
|
||||
username:
|
||||
from_secret: ssh2_username
|
||||
port:
|
||||
from_secret: ssh2_port
|
||||
envs:
|
||||
- buildrone_key
|
||||
key:
|
||||
from_secret: ssh2_key
|
||||
command_timeout: 50m
|
||||
script:
|
||||
- /mnt/buildx/jfa-go/build.sh stable
|
||||
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||
- pip3 install requests
|
||||
- bash -c 'cd /mnt/buildx/jfa-go/jfa-go && python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-stable=true'
|
||||
- rm -f /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
---
|
||||
name: jfa-go-git
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: hrfee/jfa-go-build-docker:latest
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
path: /id_rsa
|
||||
- name: ssh_key2
|
||||
path: /id_rsa2
|
||||
commands:
|
||||
- curl -sL https://git.io/goreleaser > goreleaser
|
||||
- chmod +x goreleaser
|
||||
- ./scripts/version.sh ./goreleaser --snapshot --skip=publish --clean
|
||||
- wget https://builds.hrfee.pw/upload.py
|
||||
- pip3 install requests
|
||||
- bash -c 'sftp -i /id_rsa2 -o StrictHostKeyChecking=no root@161.97.102.153:/mnt/redoc <<< $"put docs/swagger.json jfa-go.json"'
|
||||
- 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 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "rm /repo/incoming/*.deb"'
|
||||
- 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
|
||||
JFA_GO_BUILT_BY:
|
||||
from_secret: BUILT_BY
|
||||
JFA_GO_SNAPSHOT: y
|
||||
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
host:
|
||||
path: /root/.ssh/id_rsa_packaging
|
||||
- name: ssh_key2
|
||||
host:
|
||||
path: /root/.ssh/docker-build
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
- go1.16
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
---
|
||||
name: docker-buildx-unstable
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: build-deploy
|
||||
image: appleboy/drone-ssh
|
||||
environment:
|
||||
BUILDRONE_KEY:
|
||||
from_secret: BUILDRONE_KEY
|
||||
settings:
|
||||
host:
|
||||
from_secret: ssh2_host
|
||||
username:
|
||||
from_secret: ssh2_username
|
||||
port:
|
||||
from_secret: ssh2_port
|
||||
envs:
|
||||
- buildrone_key
|
||||
key:
|
||||
from_secret: ssh2_key
|
||||
command_timeout: 50m
|
||||
script:
|
||||
- /mnt/buildx/jfa-go/build.sh
|
||||
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||
- pip3 install requests
|
||||
- bash -c 'cd /mnt/buildx/jfa-go/jfa-go && python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-unstable=true'
|
||||
- rm -f /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
---
|
||||
name: jfa-go-pr
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: hrfee/jfa-go-build-docker:latest
|
||||
commands:
|
||||
- curl -sL https://git.io/goreleaser > goreleaser
|
||||
- chmod +x goreleaser
|
||||
- ./scripts/version.sh ./goreleaser --snapshot --skip=publish --clean
|
||||
|
||||
trigger:
|
||||
event:
|
||||
include:
|
||||
- pull_request
|
||||
2
.gitignore
vendored
@@ -25,3 +25,5 @@ scripts/langmover/lang
|
||||
scripts/langmover/lang2
|
||||
scripts/langmover/out
|
||||
tinyproxy.conf
|
||||
static/banner.svg
|
||||
start.sh
|
||||
|
||||
118
.goreleaser.yml
@@ -8,52 +8,15 @@ release:
|
||||
name_template: "v{{.Version}}"
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
- rm -rf data/web
|
||||
- mkdir -p data/web/css
|
||||
- bash -c 'cp -r static/* data/web/'
|
||||
- npm install
|
||||
- npm install esbuild
|
||||
- cp node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 data/web/css/
|
||||
- cp -r html data/
|
||||
- node scripts/missing-colors.js html data/html
|
||||
- cp -r lang data/
|
||||
- cp LICENSE data/
|
||||
- cp jfa-go.service data/
|
||||
- python3 scripts/enumerate_config.py -i config/config-base.json -o data/config-base.json
|
||||
- python3 scripts/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
||||
- python3 scripts/compile_mjml.py -o data/
|
||||
- rm -rf tempts
|
||||
- cp -r ts tempts
|
||||
- scripts/dark-variant.sh tempts
|
||||
- scripts/dark-variant.sh tempts/modules
|
||||
- mkdir -p data/web/js
|
||||
- npx esbuild --target=es6 --bundle tempts/admin.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/admin.js {{.Env.JFA_GO_MINIFY}}
|
||||
- npx esbuild --target=es6 --bundle tempts/user.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/user.js {{.Env.JFA_GO_MINIFY}}
|
||||
- npx esbuild --target=es6 --bundle tempts/pwr.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/pwr.js {{.Env.JFA_GO_MINIFY}}
|
||||
- npx esbuild --target=es6 --bundle tempts/pwr-pin.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/pwr-pin.js {{.Env.JFA_GO_MINIFY}}
|
||||
- npx esbuild --target=es6 --bundle tempts/form.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/form.js {{.Env.JFA_GO_MINIFY}}
|
||||
- npx esbuild --target=es6 --bundle tempts/setup.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/web/js/setup.js {{.Env.JFA_GO_MINIFY}}
|
||||
- npx esbuild --target=es6 --bundle tempts/crash.ts {{.Env.JFA_GO_SOURCEMAP}} --outfile=./data/crash.js {{.Env.JFA_GO_MINIFY}}
|
||||
- bash -c "{{.Env.JFA_GO_COPYTS}}"
|
||||
- rm -r tempts
|
||||
- npx esbuild --bundle css/base.css --outfile=./data/web/css/bundle.css --external:remixicon.css --external:../fonts/hanken* --minify
|
||||
- cp html/crash.html data/
|
||||
- npx tailwindcss -i data/web/css/bundle.css -o data/bundle.css --content "html/crash.html"
|
||||
- node scripts/inline.js root data data/crash.html data/crash.html
|
||||
- rm data/bundle.css
|
||||
- npx tailwindcss -i data/web/css/bundle.css -o data/web/css/bundle.css
|
||||
- mv data/crash.html data/html/
|
||||
- go install github.com/swaggo/swag/cmd/swag@latest
|
||||
- swag init -g main.go
|
||||
- mv data/web/css/bundle.css data/web/css/{{.Env.JFA_GO_CSS_VERSION}}bundle.css
|
||||
- npm i
|
||||
- make precompile
|
||||
builds:
|
||||
- id: notray
|
||||
dir: ./
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
flags:
|
||||
- -tags={{ .Env.JFA_GO_TAG }}
|
||||
ldflags:
|
||||
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
|
||||
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater={{.Env.JFA_GO_UPDATER}} {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
@@ -62,6 +25,24 @@ builds:
|
||||
- arm
|
||||
- arm64
|
||||
- amd64
|
||||
- id: notray-e2ee
|
||||
dir: ./
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC={{ if eq .Arch "amd64" }}x86_64{{ else if eq .Arch "arm64" }}aarch64{{ else }}{{ .Arch }}{{ end }}-linux-gnu{{ if eq .Arch "arm" }}eabihf{{ end }}-gcc
|
||||
- CXX={{ if eq .Arch "amd64" }}x86_64{{ else if eq .Arch "arm64" }}aarch64{{ else }}{{ .Arch }}{{ end }}-linux-gnu{{ if eq .Arch "arm" }}eabihf{{ end }}-gcc
|
||||
- PKG_CONFIG_PATH=/usr/lib/{{ if eq .Arch "amd64" }}x86_64{{ else if eq .Arch "arm64" }}aarch64{{ else }}{{ .Arch }}{{ end }}-linux-gnu{{ if eq .Arch "arm" }}eabihf{{ end }}/pkgconfig:$PKG_CONFIG_PATH
|
||||
- GOARM={{ if eq .Arch "arm" }}7{{ end }}
|
||||
flags:
|
||||
- -tags=e2ee,{{ .Env.JFA_GO_TAG }}
|
||||
ldflags:
|
||||
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater={{.Env.JFA_GO_UPDATER}} {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- arm
|
||||
- arm64
|
||||
- amd64
|
||||
- id: windows-tray
|
||||
dir: ./
|
||||
env:
|
||||
@@ -69,9 +50,9 @@ builds:
|
||||
- CC=x86_64-w64-mingw32-gcc
|
||||
- CXX=x86_64-w64-mingw32-g++
|
||||
flags:
|
||||
- -tags=tray
|
||||
- -tags=tray,{{ .Env.JFA_GO_TAG }}
|
||||
ldflags:
|
||||
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}" -H=windowsgui
|
||||
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater={{.Env.JFA_GO_UPDATER}} {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}" -H=windowsgui
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
@@ -82,20 +63,20 @@ builds:
|
||||
- CGO_ENABLED=1
|
||||
- CC=x86_64-linux-gnu-gcc
|
||||
- CXX=x86_64-linux-gnu-gcc
|
||||
- PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:$PKG_CONFIG_PATH
|
||||
- PKG_CONFIG_PATH=/usr/lib/{{ if eq .Arch "amd64" }}x86_64{{ else if eq .Arch "arm64" }}aarch64{{ else }}{{ .Arch }}{{ end }}-linux-gnu{{ if eq .Arch "arm" }}eabihf{{ end }}/pkgconfig:$PKG_CONFIG_PATH
|
||||
flags:
|
||||
- -tags=tray
|
||||
- -tags=tray,e2ee,{{ .Env.JFA_GO_TAG }}
|
||||
ldflags:
|
||||
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
|
||||
- -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater={{.Env.JFA_GO_UPDATER}} {{.Env.JFA_GO_STRIP}} -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
archives:
|
||||
- id: windows-tray
|
||||
builds:
|
||||
ids:
|
||||
- windows-tray
|
||||
format: zip
|
||||
formats: [ "zip" ]
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_{{ .Version }}_TrayIcon_
|
||||
{{- if eq .Os "darwin" }}macOS
|
||||
@@ -103,9 +84,9 @@ archives:
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
- id: linux-tray
|
||||
builds:
|
||||
ids:
|
||||
- linux-tray
|
||||
format: zip
|
||||
formats: [ "zip" ]
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_{{ .Version }}_TrayIcon_
|
||||
{{- if eq .Os "darwin" }}macOS
|
||||
@@ -113,19 +94,29 @@ archives:
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
- id: notray
|
||||
builds:
|
||||
ids:
|
||||
- notray
|
||||
format: zip
|
||||
formats: [ "zip" ]
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_{{ .Version }}_
|
||||
{{- if eq .Os "darwin" }}macOS
|
||||
{{- else }}{{- title .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
- id: notray-e2ee
|
||||
ids:
|
||||
- notray-e2ee
|
||||
formats: [ "zip" ]
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_{{ .Version }}_MatrixE2EE_
|
||||
{{- if eq .Os "darwin" }}macOS
|
||||
{{- else }}{{- title .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "0.0.0-{{ .Env.JFA_GO_NFPM_EPOCH }}"
|
||||
version_template: "0.0.0-{{ .Env.JFA_GO_NFPM_EPOCH }}"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
@@ -142,8 +133,8 @@ nfpms:
|
||||
license: MIT
|
||||
vendor: hrfee.dev
|
||||
version_metadata: git
|
||||
builds:
|
||||
- notray
|
||||
ids:
|
||||
- notray-e2ee
|
||||
contents:
|
||||
- src: ./LICENSE
|
||||
dst: /usr/share/licenses/jfa-go
|
||||
@@ -151,6 +142,16 @@ nfpms:
|
||||
- apk
|
||||
- deb
|
||||
- rpm
|
||||
overrides:
|
||||
deb:
|
||||
dependencies:
|
||||
- libolm-dev
|
||||
rpm:
|
||||
dependencies:
|
||||
- libolm
|
||||
apk:
|
||||
dependencies:
|
||||
- olm
|
||||
- id: tray
|
||||
file_name_template: '{{ .ProjectName }}{{ if .IsSnapshot }}-git{{ end }}_TrayIcon_{{ .Arch }}_{{ if .IsSnapshot }}{{ .ShortCommit }}{{ else }}v{{ .Version }}{{ end }}'
|
||||
package_name: jfa-go-tray
|
||||
@@ -160,7 +161,7 @@ nfpms:
|
||||
license: MIT
|
||||
vendor: hrfee.dev
|
||||
version_metadata: git
|
||||
builds:
|
||||
ids:
|
||||
- linux-tray
|
||||
contents:
|
||||
- src: ./LICENSE
|
||||
@@ -177,9 +178,12 @@ nfpms:
|
||||
- jfa-go
|
||||
dependencies:
|
||||
- libayatana-appindicator
|
||||
- libolm-dev
|
||||
rpm:
|
||||
dependencies:
|
||||
- libappindicator-gtk3
|
||||
- libolm
|
||||
apk:
|
||||
dependencies:
|
||||
- libayatana-appindicator
|
||||
- olm
|
||||
|
||||
@@ -12,14 +12,6 @@ clone:
|
||||
depth: 0
|
||||
|
||||
steps:
|
||||
- name: redoc
|
||||
image: docker.io/hrfee/jfa-go-build-docker:latest
|
||||
environment:
|
||||
REDOC_SSH_ID:
|
||||
from_secret: REDOC_SSH_ID
|
||||
commands:
|
||||
- sh -c "echo \"$REDOC_SSH_ID\" > /tmp/id_redoc && chmod 600 /tmp/id_redoc"
|
||||
- bash -c 'sftp -P 3625 -i /tmp/id_redoc -o StrictHostKeyChecking=no redoc@api.jfa-go.com:/home/redoc <<< $"put docs/swagger.json jfa-go.json"'
|
||||
- name: build
|
||||
image: docker.io/hrfee/jfa-go-build-docker:latest
|
||||
environment:
|
||||
@@ -30,6 +22,14 @@ steps:
|
||||
- curl -sfL https://goreleaser.com/static/run > goreleaser
|
||||
- chmod +x goreleaser
|
||||
- ./scripts/version.sh ./goreleaser --snapshot --skip=publish --clean
|
||||
- name: redoc
|
||||
image: docker.io/hrfee/jfa-go-build-docker:latest
|
||||
environment:
|
||||
REDOC_SSH_ID:
|
||||
from_secret: REDOC_SSH_ID
|
||||
commands:
|
||||
- sh -c "echo \"$REDOC_SSH_ID\" > /tmp/id_redoc && chmod 600 /tmp/id_redoc"
|
||||
- bash -c 'sftp -P 3625 -i /tmp/id_redoc -o StrictHostKeyChecking=no redoc@api.jfa-go.com:/home/redoc <<< $"put docs/swagger.json jfa-go.json"'
|
||||
- name: deb-repo
|
||||
image: docker.io/hrfee/jfa-go-build-docker:latest
|
||||
environment:
|
||||
|
||||
32
Dockerfile
@@ -1,34 +1,26 @@
|
||||
# Use this instead if hrfee/jfa-go-build-docker doesn't support your architecture
|
||||
# FROM --platform=$BUILDPLATFORM golang:latest AS support
|
||||
FROM --platform=$BUILDPLATFORM hrfee/jfa-go-build-docker AS support
|
||||
FROM --platform=$BUILDPLATFORM docker.io/hrfee/jfa-go-build-docker:latest AS support
|
||||
# FROM --platform=$BUILDPLATFORM jfa-go-bd AS support
|
||||
ARG BUILT_BY
|
||||
ENV JFA_GO_BUILT_BY=$BUILT_BY
|
||||
|
||||
COPY . /opt/build
|
||||
|
||||
# Uncomment this if hrfee/jfa-go-build-docker doesn't support your architecture
|
||||
# RUN apt-get update -y \
|
||||
# && apt-get install build-essential python3-pip -y \
|
||||
# && (curl -sL https://deb.nodesource.com/setup_current.x | bash -) \
|
||||
# && apt-get install nodejs
|
||||
RUN (cd /opt/build; make configuration npm email typescript variants-html bundle-css inline-css swagger copy INTERNAL=off GOESBUILD=on) \
|
||||
&& sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' /opt/build/build/data/html/setup.html
|
||||
# RUN curl -sfL https://goreleaser.com/static/run > /goreleaser && chmod +x /goreleaser
|
||||
RUN cd /opt/build; INTERNAL=off UPDATER=docker ./scripts/version.sh /goreleaser build --snapshot --skip=validate --clean --id notray-e2ee
|
||||
RUN mv /opt/build/dist/*_linux_arm_6 /opt/build/dist/placeholder_linux_arm
|
||||
RUN sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' /opt/build/build/data/html/setup.html
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:latest AS build
|
||||
FROM golang:bookworm AS final
|
||||
ARG TARGETARCH
|
||||
ENV GOARCH=$TARGETARCH
|
||||
ARG BUILT_BY
|
||||
ENV BUILTBY=$BUILT_BY
|
||||
|
||||
COPY --from=support /opt/build /opt/build
|
||||
COPY --from=support /opt/build/dist/*_linux_${TARGETARCH}* /opt/jfa-go
|
||||
COPY --from=support /opt/build/build/data /opt/jfa-go/data
|
||||
|
||||
RUN (cd /opt/build; make compile INTERNAL=off UPDATER=docker)
|
||||
|
||||
FROM golang:latest
|
||||
|
||||
COPY --from=build /opt/build/build /opt/jfa-go
|
||||
RUN apt-get update -y && apt-get install libolm-dev -y
|
||||
|
||||
EXPOSE 8056
|
||||
EXPOSE 8057
|
||||
|
||||
CMD [ "/opt/jfa-go/jfa-go", "-data", "/data" ]
|
||||
|
||||
|
||||
|
||||
175
Makefile
@@ -1,3 +1,6 @@
|
||||
.PHONY: configuration email typescript swagger copy compile compress inline-css variants-html install clean npm config-description config-default precompile
|
||||
.DEFAULT_GOAL := all
|
||||
|
||||
GOESBUILD ?= off
|
||||
ifeq ($(GOESBUILD), on)
|
||||
ESBUILD := esbuild
|
||||
@@ -7,6 +10,7 @@ endif
|
||||
GOBINARY ?= go
|
||||
|
||||
CSSVERSION ?= v3
|
||||
CSS_BUNDLE = $(DATA)/web/css/$(CSSVERSION)bundle.css
|
||||
|
||||
VERSION ?= $(shell git describe --exact-match HEAD 2> /dev/null || echo vgit)
|
||||
VERSION := $(shell echo $(VERSION) | sed 's/v//g')
|
||||
@@ -25,14 +29,16 @@ endif
|
||||
|
||||
INTERNAL ?= on
|
||||
TRAY ?= off
|
||||
E2EE ?= off
|
||||
E2EE ?= on
|
||||
TAGS := -tags "
|
||||
|
||||
ifeq ($(INTERNAL), on)
|
||||
DATA := data
|
||||
COMPDEPS := $(BUILDDEPS)
|
||||
else
|
||||
DATA := build/data
|
||||
TAGS := $(TAGS) external
|
||||
COMPDEPS :=
|
||||
endif
|
||||
|
||||
ifeq ($(TRAY), on)
|
||||
@@ -53,17 +59,19 @@ endif
|
||||
DEBUG ?= off
|
||||
ifeq ($(DEBUG), on)
|
||||
SOURCEMAP := --sourcemap
|
||||
MINIFY :=
|
||||
TYPECHECK := npx tsc -noEmit --project ts/tsconfig.json
|
||||
# jank
|
||||
COPYTS := rm -r $(DATA)/web/js/ts; cp -r tempts $(DATA)/web/js/ts
|
||||
UNCSS := cp $(DATA)/web/css/bundle.css $(DATA)/bundle.css
|
||||
UNCSS := cp $(CSS_BUNDLE) $(DATA)/bundle.css
|
||||
# TAILWIND := --content ""
|
||||
else
|
||||
LDFLAGS := -s -w $(LDFLAGS)
|
||||
SOURCEMAP :=
|
||||
MINIFY := --minify
|
||||
COPYTS :=
|
||||
TYPECHECK :=
|
||||
UNCSS := npx tailwindcss -i $(DATA)/web/css/bundle.css -o $(DATA)/bundle.css --content "html/crash.html"
|
||||
UNCSS := npx tailwindcss -i $(CSS_BUNDLE) -o $(DATA)/bundle.css --content "html/crash.html"
|
||||
# UNCSS := npx uncss $(DATA)/crash.html --csspath web/css --output $(DATA)/bundle.css
|
||||
TAILWIND :=
|
||||
endif
|
||||
@@ -94,95 +102,132 @@ else
|
||||
SWAGINSTALL :=
|
||||
endif
|
||||
|
||||
npm:
|
||||
$(info installing npm dependencies)
|
||||
npm install $(NPMOPTS)
|
||||
CONFIG_BASE = config/config-base.yaml
|
||||
|
||||
configuration:
|
||||
$(info Fixing config-base)
|
||||
-mkdir -p $(DATA)
|
||||
python3 scripts/enumerate_config.py -i config/config-base.json -o $(DATA)/config-base.json
|
||||
# CONFIG_DESCRIPTION = $(DATA)/config-base.json
|
||||
CONFIG_DEFAULT = $(DATA)/config-default.ini
|
||||
# $(CONFIG_DESCRIPTION) &: $(CONFIG_BASE)
|
||||
# $(info Fixing config-base)
|
||||
# -mkdir -p $(DATA)
|
||||
|
||||
$(DATA):
|
||||
mkdir -p $(DATA)/web/js
|
||||
mkdir -p $(DATA)/web/css
|
||||
|
||||
$(CONFIG_DEFAULT): $(CONFIG_BASE)
|
||||
$(info Generating config-default.ini)
|
||||
python3 scripts/generate_ini.py -i config/config-base.json -o $(DATA)/config-default.ini
|
||||
go run scripts/ini/main.go -in $(CONFIG_BASE) -out $(DATA)/config-default.ini
|
||||
|
||||
email:
|
||||
configuration: $(CONFIG_DEFAULT)
|
||||
|
||||
EMAIL_SRC = $(wildcard mail/*)
|
||||
EMAIL_TARGET = $(DATA)/confirmation.html
|
||||
$(EMAIL_TARGET): $(EMAIL_SRC)
|
||||
$(info Generating email html)
|
||||
python3 scripts/compile_mjml.py -o $(DATA)/
|
||||
npx mjml mail/*.mjml -o $(DATA)/
|
||||
$(info Copying plaintext mail)
|
||||
cp mail/*.txt $(DATA)/
|
||||
|
||||
typescript:
|
||||
TYPESCRIPT_FULLSRC = $(shell find ts/ -type f -name "*.ts")
|
||||
TYPESCRIPT_SRC = $(wildcard ts/*.ts)
|
||||
TYPESCRIPT_TEMPSRC = $(TYPESCRIPT_SRC:ts/%=tempts/%)
|
||||
# TYPESCRIPT_TARGET = $(patsubst %.ts,%.js,$(subst tempts/,./$(DATA)/web/js/,$(TYPESCRIPT_TEMPSRC)))
|
||||
TYPESCRIPT_TARGET = $(DATA)/web/js/admin.js
|
||||
$(TYPESCRIPT_TARGET): $(TYPESCRIPT_FULLSRC) ts/tsconfig.json
|
||||
$(TYPECHECK)
|
||||
$(adding dark variants to typescript)
|
||||
rm -rf tempts
|
||||
cp -r ts tempts
|
||||
$(adding dark variants to typescript)
|
||||
scripts/dark-variant.sh tempts
|
||||
scripts/dark-variant.sh tempts/modules
|
||||
$(info compiling typescript)
|
||||
mkdir -p $(DATA)/web/js
|
||||
$(ESBUILD) --target=es6 --bundle tempts/admin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/admin.js --minify
|
||||
$(ESBUILD) --target=es6 --bundle tempts/user.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/user.js --minify
|
||||
$(ESBUILD) --target=es6 --bundle tempts/pwr.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr.js --minify
|
||||
$(ESBUILD) --target=es6 --bundle tempts/pwr-pin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr-pin.js --minify
|
||||
$(ESBUILD) --target=es6 --bundle tempts/form.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/form.js --minify
|
||||
$(ESBUILD) --target=es6 --bundle tempts/setup.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/setup.js --minify
|
||||
$(ESBUILD) --target=es6 --bundle tempts/crash.ts --outfile=./$(DATA)/crash.js --minify
|
||||
$(foreach tempsrc,$(TYPESCRIPT_TEMPSRC),$(ESBUILD) --target=es6 --bundle $(tempsrc) $(SOURCEMAP) --outfile=$(patsubst %.ts,%.js,$(subst tempts/,./$(DATA)/web/js/,$(tempsrc))) $(MINIFY);)
|
||||
$(COPYTS)
|
||||
|
||||
swagger:
|
||||
SWAGGER_SRC = $(wildcard api*.go) $(wildcard *auth.go) views.go
|
||||
SWAGGER_TARGET = docs/docs.go
|
||||
$(SWAGGER_TARGET): $(SWAGGER_SRC)
|
||||
$(SWAGINSTALL)
|
||||
swag init -g main.go
|
||||
|
||||
compile:
|
||||
$(info Downloading deps)
|
||||
$(GOBINARY) mod download
|
||||
$(info Building)
|
||||
mkdir -p build
|
||||
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o build/jfa-go
|
||||
|
||||
compress:
|
||||
upx --lzma build/jfa-go
|
||||
|
||||
bundle-css:
|
||||
mkdir -p $(DATA)/web/css
|
||||
$(info copying fonts)
|
||||
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
|
||||
$(info bundling css)
|
||||
$(ESBUILD) --bundle css/base.css --outfile=$(DATA)/web/css/bundle.css --external:remixicon.css --external:../fonts/hanken* --minify
|
||||
npx tailwindcss -i $(DATA)/web/css/bundle.css -o $(DATA)/web/css/bundle.css $(TAILWIND)
|
||||
# npx postcss -o $(DATA)/web/css/bundle.css $(DATA)/web/css/bundle.css
|
||||
|
||||
inline-css:
|
||||
cp html/crash.html $(DATA)/crash.html
|
||||
$(UNCSS)
|
||||
node scripts/inline.js root $(DATA) $(DATA)/crash.html $(DATA)/crash.html
|
||||
rm $(DATA)/bundle.css
|
||||
|
||||
variants-html:
|
||||
VARIANTS_SRC = $(wildcard html/*.html)
|
||||
VARIANTS_TARGET = $(DATA)/html/admin.html
|
||||
$(VARIANTS_TARGET): $(VARIANTS_SRC)
|
||||
$(info copying html)
|
||||
cp -r html $(DATA)/
|
||||
$(info adding dark variants to html)
|
||||
node scripts/missing-colors.js html $(DATA)/html
|
||||
|
||||
copy:
|
||||
ICON_SRC = node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2
|
||||
ICON_TARGET = $(ICON_SRC:node_modules/remixicon/fonts/%=$(DATA)/web/css/%)
|
||||
CSS_SRC = $(wildcard css/*.css)
|
||||
CSS_TARGET = $(DATA)/web/css/part-bundle.css
|
||||
CSS_FULLTARGET = $(CSS_BUNDLE)
|
||||
ALL_CSS_SRC = $(ICON_SRC) $(CSS_SRC)
|
||||
ALL_CSS_TARGET = $(ICON_TARGET)
|
||||
|
||||
$(CSS_FULLTARGET): $(TYPESCRIPT_TARGET) $(VARIANTS_TARGET) $(ALL_CSS_SRC) $(wildcard html/*.html)
|
||||
$(info copying fonts)
|
||||
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
|
||||
$(info bundling css)
|
||||
rm -f $(CSS_TARGET) $(CSS_FULLTARGET)
|
||||
$(ESBUILD) --bundle css/base.css --outfile=$(CSS_TARGET) --external:remixicon.css --external:../fonts/hanken* --minify
|
||||
|
||||
npx tailwindcss -i $(CSS_TARGET) -o $(CSS_FULLTARGET) $(TAILWIND)
|
||||
rm $(CSS_TARGET)
|
||||
# mv $(CSS_BUNDLE) $(DATA)/web/css/$(CSSVERSION)bundle.css
|
||||
# npx postcss -o $(CSS_TARGET) $(CSS_TARGET)
|
||||
|
||||
INLINE_SRC = html/crash.html
|
||||
INLINE_TARGET = $(DATA)/crash.html
|
||||
$(INLINE_TARGET): $(CSS_FULLTARGET) $(INLINE_SRC)
|
||||
cp html/crash.html $(DATA)/crash.html
|
||||
$(UNCSS) # generates $(DATA)/bundle.css for us
|
||||
node scripts/inline.js root $(DATA) $(DATA)/crash.html $(DATA)/crash.html
|
||||
rm $(DATA)/bundle.css
|
||||
|
||||
LANG_SRC = $(shell find ./lang)
|
||||
LANG_TARGET = $(LANG_SRC:lang/%=$(DATA)/lang/%)
|
||||
STATIC_SRC = $(wildcard static/*)
|
||||
STATIC_TARGET = $(STATIC_SRC:static/%=$(DATA)/web/%)
|
||||
COPY_SRC = images/banner.svg jfa-go.service LICENSE $(LANG_SRC) $(STATIC_SRC)
|
||||
COPY_TARGET = $(DATA)/jfa-go.service
|
||||
# $(DATA)/LICENSE $(LANG_TARGET) $(STATIC_TARGET) $(DATA)/web/css/$(CSSVERSION)bundle.css
|
||||
$(COPY_TARGET): $(INLINE_TARGET) $(STATIC_SRC) $(LANG_SRC) $(CONFIG_BASE)
|
||||
$(info copying $(CONFIG_BASE))
|
||||
cp $(CONFIG_BASE) $(DATA)/
|
||||
$(info copying crash page)
|
||||
mv $(DATA)/crash.html $(DATA)/html/
|
||||
cp $(DATA)/crash.html $(DATA)/html/
|
||||
$(info copying static data)
|
||||
mkdir -p $(DATA)/web
|
||||
cp images/banner.svg static/banner.svg
|
||||
cp -r static/* $(DATA)/web/
|
||||
$(info copying systemd service)
|
||||
cp jfa-go.service $(DATA)/
|
||||
$(info copying language files)
|
||||
cp -r lang $(DATA)/
|
||||
cp LICENSE $(DATA)/
|
||||
mv $(DATA)/web/css/bundle.css $(DATA)/web/css/$(CSSVERSION)bundle.css
|
||||
|
||||
# internal-files:
|
||||
# python3 scripts/embed.py internal
|
||||
#
|
||||
# external-files:
|
||||
# python3 scripts/embed.py external
|
||||
# -mkdir -p build
|
||||
# $(info copying internal data into build/)
|
||||
# cp -r data build/
|
||||
BUILDDEPS := $(DATA) $(CONFIG_DEFAULT) $(EMAIL_TARGET) $(COPY_TARGET) $(SWAGGER_TARGET) $(INLINE_TARGET) $(CSS_FULLTARGET) $(TYPESCRIPT_TARGET)
|
||||
precompile: $(BUILDDEPS)
|
||||
|
||||
COMPDEPS =
|
||||
ifeq ($(INTERNAL), on)
|
||||
COMPDEPS = $(BUILDDEPS)
|
||||
endif
|
||||
|
||||
GO_SRC = $(shell find ./ -name "*.go")
|
||||
GO_TARGET = build/jfa-go
|
||||
$(GO_TARGET): $(COMPDEPS) $(SWAGGER_TARGET) $(GO_SRC) go.mod go.sum
|
||||
$(info Downloading deps)
|
||||
$(GOBINARY) mod download
|
||||
$(info Building)
|
||||
mkdir -p build
|
||||
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o $(GO_TARGET)
|
||||
|
||||
all: $(BUILDDEPS) $(GO_TARGET)
|
||||
|
||||
compress:
|
||||
upx --lzma $(GO_TARGET)
|
||||
|
||||
install:
|
||||
cp -r build $(DESTDIR)/jfa-go
|
||||
@@ -194,6 +239,6 @@ clean:
|
||||
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
|
||||
go clean
|
||||
|
||||
quick: configuration typescript variants-html bundle-css inline-css copy compile
|
||||
|
||||
all: configuration npm email typescript variants-html bundle-css inline-css swagger copy compile
|
||||
npm:
|
||||
$(info installing npm dependencies)
|
||||
npm install $(NPMOPTS)
|
||||
|
||||
13
README.md
@@ -1,5 +1,5 @@
|
||||

|
||||
[](https://drone.hrfee.dev/hrfee/jfa-go)
|
||||
[](https://ci.hrfee.dev/repos/3)
|
||||
[](https://hub.docker.com/r/hrfee/jfa-go)
|
||||
[](https://weblate.jfa-go.com/engage/jfa-go/)
|
||||
[](https://wiki.jfa-go.com)
|
||||
@@ -13,13 +13,14 @@
|
||||
Studies mean I can't work on this project a lot outside of breaks, however I hope i'll be able to fit in general support and things like bug fixes into my time. New features and such will likely come in short bursts throughout the year (if they do at all).
|
||||
|
||||
#### Does/Will it still work?
|
||||
jfa-go currently works on Jellyfin 10.8.13, the latest version as of 26/12/23. I should be able to maintain compatability in the future, unless any big changes occur.
|
||||
jfa-go currently works on Jellyfin 10.9.8, the latest version as of 31/07/2024. I should be able to maintain compatability in the future, unless any big changes occur.
|
||||
|
||||
#### Alternatives
|
||||
If you want a bit more of a guarantee of support, I've seen these projects mentioned although haven't tried them myself.
|
||||
|
||||
* [Wizarr](https://github.com/Wizarrrr/wizarr) focuses on invites, and also includes some Discord & Ombi integration.
|
||||
* [Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is a fork of Overseerr, which can manage users and mainly acts as an Ombi alternative.
|
||||
* [Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is a fork of Overseerr which can manage users and mainly acts as an Ombi alternative.
|
||||
* [jfa-go now integrates with Jellyseerr, much like Ombi, but better.](https://github.com/hrfee/jfa-go/pull/351)
|
||||
* [Organizr](https://github.com/causefx/Organizr) doesn't focus on Jellyfin, but allows putting self-hosted services into "tabs" on a central page, and allows creating users, which lets one control who can access what.
|
||||
---
|
||||
jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) (and [Emby](https://emby.media/) as 2nd class) that provides invite-based account creation as well as other features that make one's instance much easier to manage.
|
||||
@@ -32,7 +33,7 @@ jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jelly
|
||||
* Password validation: Ensure users choose a strong password.
|
||||
* CAPTCHAs and contact method verificatoin can be enabled to avoid bots.
|
||||
* ⌛ 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. See [wiki](https://wiki.jfa-go.com/docs/ombi/) for a warning on this one.
|
||||
* 🔗 Ombi/Jellyseerr Integration: Automatically creates and synchronizes details for new accounts. Supports setting permissions with the Profiles feature. **Ombi integration use is risky, see [wiki](https://wiki.jfa-go.com/docs/ombi/)**.
|
||||
* Account management: Bulk or individually; apply settings, delete, disable/enable, send messages and much more.
|
||||
* 📣 Announcements: Bulk message your users with announcements about your server.
|
||||
* Telegram/Discord/Matrix Integration: Verify users via a chat bot, and send Password Resets, Announcements, etc. through it.
|
||||
@@ -58,6 +59,8 @@ jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jelly
|
||||
|
||||
**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.
|
||||
|
||||
`MatrixE2EE` builds (and Linux `TrayIcon` builds) include support for end-to-end encryption for the Matrix bot, but require the `libolm(-dev)` dependency. `.deb/.rpm/.apk` packages list this dependency, and docker images include it.
|
||||
|
||||
##### [Docker](https://hub.docker.com/r/hrfee/jfa-go)
|
||||
```sh
|
||||
docker create \
|
||||
@@ -87,7 +90,7 @@ sudo apt-get update
|
||||
# For servers
|
||||
sudo apt-get install jfa-go
|
||||
# ------
|
||||
# For desktops/servers with GUI (has dependencies)
|
||||
# For desktops/servers with GUI (may pull in lots of dependencies)
|
||||
sudo apt-get install jfa-go-tray
|
||||
# ------
|
||||
```
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
@@ -85,7 +86,7 @@ func activitySourceToString(v ActivitySource) string {
|
||||
return "anon"
|
||||
}
|
||||
|
||||
// @Summary Get the requested set of activities, Paginated, filtered and sorted.
|
||||
// @Summary Get the requested set of activities, Paginated, filtered and sorted. Is a POST because of some issues I was having, ideally should be a GET.
|
||||
// @Produce json
|
||||
// @Param GetActivitiesDTO body GetActivitiesDTO true "search parameters"
|
||||
// @Success 200 {object} GetActivitiesRespDTO
|
||||
@@ -120,7 +121,7 @@ func (app *appContext) GetActivities(gc *gin.Context) {
|
||||
err := app.storage.db.Find(&results, query)
|
||||
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to read activities from DB: %v\n", err)
|
||||
app.err.Printf(lm.FailedDBReadActivities, err)
|
||||
}
|
||||
|
||||
resp := GetActivitiesRespDTO{
|
||||
@@ -143,13 +144,13 @@ func (app *appContext) GetActivities(gc *gin.Context) {
|
||||
if act.Type == ActivityDeletion || act.Type == ActivityCreation {
|
||||
resp.Activities[i].Username = act.Value
|
||||
resp.Activities[i].Value = ""
|
||||
} else if user, status, err := app.jf.UserByID(act.UserID, false); status == 200 && err == nil {
|
||||
} else if user, err := app.jf.UserByID(act.UserID, false); err == nil {
|
||||
resp.Activities[i].Username = user.Name
|
||||
}
|
||||
|
||||
if (act.SourceType == ActivityUser || act.SourceType == ActivityAdmin) && act.Source != "" {
|
||||
user, status, err := app.jf.UserByID(act.Source, false)
|
||||
if status == 200 && err == nil {
|
||||
user, err := app.jf.UserByID(act.Source, false)
|
||||
if err == nil {
|
||||
resp.Activities[i].SourceUsername = user.Name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
// @Summary Creates a backup of the database.
|
||||
@@ -32,10 +31,10 @@ func (app *appContext) CreateBackup(gc *gin.Context) {
|
||||
func (app *appContext) GetBackup(gc *gin.Context) {
|
||||
fname := gc.Param("fname")
|
||||
// Hopefully this is enough to ensure the path isn't malicious. Hidden behind bearer auth anyway so shouldn't matter too much I guess.
|
||||
ok := (strings.HasPrefix(fname, BACKUP_PREFIX) || strings.HasPrefix(fname, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX)) && strings.HasSuffix(fname, BACKUP_SUFFIX)
|
||||
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(fname, BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX))
|
||||
if !ok || err != nil || t.IsZero() {
|
||||
app.debug.Printf("Ignoring backup DL request due to fname: %v\n", err)
|
||||
b := Backup{}
|
||||
err := b.FromString(fname)
|
||||
if err != nil || b.Date.IsZero() {
|
||||
app.debug.Printf(lm.IgnoreInvalidFilename, fname, err)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -61,7 +60,8 @@ func (app *appContext) GetBackups(gc *gin.Context) {
|
||||
resp.Backups[i].Name = item.Name()
|
||||
fullpath := filepath.Join(path, item.Name())
|
||||
resp.Backups[i].Path = fullpath
|
||||
resp.Backups[i].Date = backups.dates[i].Unix()
|
||||
resp.Backups[i].Date = backups.info[i].Date.Unix()
|
||||
resp.Backups[i].Commit = backups.info[i].Commit
|
||||
fstat, err := os.Stat(fullpath)
|
||||
if err == nil {
|
||||
resp.Backups[i].Size = fileSize(fstat.Size())
|
||||
@@ -80,10 +80,10 @@ func (app *appContext) GetBackups(gc *gin.Context) {
|
||||
func (app *appContext) RestoreLocalBackup(gc *gin.Context) {
|
||||
fname := gc.Param("fname")
|
||||
// Hopefully this is enough to ensure the path isn't malicious. Hidden behind bearer auth anyway so shouldn't matter too much I guess.
|
||||
ok := strings.HasPrefix(fname, BACKUP_PREFIX) && strings.HasSuffix(fname, BACKUP_SUFFIX)
|
||||
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(fname, BACKUP_PREFIX), BACKUP_SUFFIX))
|
||||
if !ok || err != nil || t.IsZero() {
|
||||
app.debug.Printf("Ignoring backup DL request due to fname: %v\n", err)
|
||||
b := Backup{}
|
||||
err := b.FromString(fname)
|
||||
if err != nil || b.Date.IsZero() {
|
||||
app.debug.Printf(lm.IgnoreInvalidFilename, fname, err)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -103,15 +103,16 @@ func (app *appContext) RestoreLocalBackup(gc *gin.Context) {
|
||||
func (app *appContext) RestoreBackup(gc *gin.Context) {
|
||||
file, err := gc.FormFile("backups-file")
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to get file from form data: %v\n", err)
|
||||
app.err.Printf(lm.FailedGetUpload, err)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
app.debug.Printf("Got uploaded file \"%s\"\n", file.Filename)
|
||||
app.debug.Printf(lm.GetUpload, file.Filename)
|
||||
path := app.config.Section("backups").Key("path").String()
|
||||
fullpath := filepath.Join(path, BACKUP_UPLOAD_PREFIX+BACKUP_PREFIX+time.Now().Local().Format(BACKUP_DATEFMT)+BACKUP_SUFFIX)
|
||||
b := Backup{Upload: true}
|
||||
fullpath := filepath.Join(path, b.String())
|
||||
gc.SaveUploadedFile(file, fullpath)
|
||||
app.debug.Printf("Saved to \"%s\"\n", fullpath)
|
||||
app.debug.Printf(lm.Write, fullpath)
|
||||
LOADBAK = fullpath
|
||||
app.restart(gc)
|
||||
}
|
||||
|
||||
241
api-invites.go
@@ -8,9 +8,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/itchyny/timefmt-go"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,6 +29,7 @@ func GenerateInviteCode() string {
|
||||
return inviteCode
|
||||
}
|
||||
|
||||
// checkInvites performs general housekeeping on invites, i.e. deleting expired ones and cleaning captcha data.
|
||||
func (app *appContext) checkInvites() {
|
||||
currentTime := time.Now()
|
||||
for _, data := range app.storage.GetInvites() {
|
||||
@@ -52,60 +53,11 @@ func (app *appContext) checkInvites() {
|
||||
if !currentTime.After(expiry) {
|
||||
continue
|
||||
}
|
||||
|
||||
app.debug.Printf("Housekeeping: Deleting old invite %s", data.Code)
|
||||
|
||||
// Disable referrals for the user if UseReferralExpiry is enabled, so no new ones are made.
|
||||
if data.IsReferral && data.UseReferralExpiry && data.ReferrerJellyfinID != "" {
|
||||
user, ok := app.storage.GetEmailsKey(data.ReferrerJellyfinID)
|
||||
if ok {
|
||||
user.ReferralTemplateKey = ""
|
||||
app.storage.SetEmailsKey(data.ReferrerJellyfinID, user)
|
||||
}
|
||||
}
|
||||
notify := data.Notify
|
||||
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
app.debug.Printf("%s: Expiry notification", data.Code)
|
||||
var wait sync.WaitGroup
|
||||
for address, settings := range notify {
|
||||
if !settings["notify-expiry"] {
|
||||
continue
|
||||
}
|
||||
wait.Add(1)
|
||||
go func(addr string) {
|
||||
defer wait.Done()
|
||||
msg, err := app.email.constructExpiry(data.Code, data, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification: %v", data.Code, err)
|
||||
} else {
|
||||
// Check whether notify "address" is an email address of Jellyfin ID
|
||||
if strings.Contains(addr, "@") {
|
||||
err = app.email.send(msg, addr)
|
||||
} else {
|
||||
err = app.sendByID(msg, addr)
|
||||
}
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification: %v", data.Code, err)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to %s", addr)
|
||||
}
|
||||
}
|
||||
}(address)
|
||||
}
|
||||
wait.Wait()
|
||||
}
|
||||
app.storage.DeleteInvitesKey(data.Code)
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityDeleteInvite,
|
||||
SourceType: ActivityDaemon,
|
||||
InviteCode: data.Code,
|
||||
Value: data.Label,
|
||||
Time: time.Now(),
|
||||
}, nil, false)
|
||||
app.deleteExpiredInvite(data)
|
||||
}
|
||||
}
|
||||
|
||||
// checkInvite checks the validity of a specific invite, optionally removing it if invalid(ated).
|
||||
func (app *appContext) checkInvite(code string, used bool, username string) bool {
|
||||
currentTime := time.Now()
|
||||
inv, match := app.storage.GetInvitesKey(code)
|
||||
@@ -114,54 +66,8 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
}
|
||||
expiry := inv.ValidTill
|
||||
if currentTime.After(expiry) {
|
||||
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
||||
notify := inv.Notify
|
||||
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
app.debug.Printf("%s: Expiry notification", code)
|
||||
var wait sync.WaitGroup
|
||||
for address, settings := range notify {
|
||||
if !settings["notify-expiry"] {
|
||||
continue
|
||||
}
|
||||
wait.Add(1)
|
||||
go func(addr string) {
|
||||
defer wait.Done()
|
||||
msg, err := app.email.constructExpiry(code, inv, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
|
||||
} else {
|
||||
// Check whether notify "address" is an email address of Jellyfin ID
|
||||
if strings.Contains(addr, "@") {
|
||||
err = app.email.send(msg, addr)
|
||||
} else {
|
||||
err = app.sendByID(msg, addr)
|
||||
}
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to %s", addr)
|
||||
}
|
||||
}
|
||||
}(address)
|
||||
}
|
||||
wait.Wait()
|
||||
}
|
||||
if inv.IsReferral && inv.ReferrerJellyfinID != "" && inv.UseReferralExpiry {
|
||||
user, ok := app.storage.GetEmailsKey(inv.ReferrerJellyfinID)
|
||||
if ok {
|
||||
user.ReferralTemplateKey = ""
|
||||
app.storage.SetEmailsKey(inv.ReferrerJellyfinID, user)
|
||||
}
|
||||
}
|
||||
app.deleteExpiredInvite(inv)
|
||||
match = false
|
||||
app.storage.DeleteInvitesKey(code)
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityDeleteInvite,
|
||||
SourceType: ActivityDaemon,
|
||||
InviteCode: code,
|
||||
Value: inv.Label,
|
||||
Time: time.Now(),
|
||||
}, nil, false)
|
||||
} else if used {
|
||||
del := false
|
||||
newInv := inv
|
||||
@@ -187,6 +93,67 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
return match
|
||||
}
|
||||
|
||||
func (app *appContext) deleteExpiredInvite(data Invite) {
|
||||
app.debug.Printf(lm.DeleteOldInvite, data.Code)
|
||||
|
||||
// Disable referrals for the user if UseReferralExpiry is enabled, so no new ones are made.
|
||||
if data.IsReferral && data.UseReferralExpiry && data.ReferrerJellyfinID != "" {
|
||||
user, ok := app.storage.GetEmailsKey(data.ReferrerJellyfinID)
|
||||
if ok {
|
||||
user.ReferralTemplateKey = ""
|
||||
app.storage.SetEmailsKey(data.ReferrerJellyfinID, user)
|
||||
}
|
||||
}
|
||||
wait := app.sendAdminExpiryNotification(data)
|
||||
app.storage.DeleteInvitesKey(data.Code)
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
Type: ActivityDeleteInvite,
|
||||
SourceType: ActivityDaemon,
|
||||
InviteCode: data.Code,
|
||||
Value: data.Label,
|
||||
Time: time.Now(),
|
||||
}, nil, false)
|
||||
|
||||
if wait != nil {
|
||||
wait.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) sendAdminExpiryNotification(data Invite) *sync.WaitGroup {
|
||||
notify := data.Notify
|
||||
if !emailEnabled || !app.config.Section("notifications").Key("enabled").MustBool(false) || len(notify) != 0 {
|
||||
return nil
|
||||
}
|
||||
var wait sync.WaitGroup
|
||||
for address, settings := range notify {
|
||||
if !settings["notify-expiry"] {
|
||||
continue
|
||||
}
|
||||
wait.Add(1)
|
||||
go func(addr string) {
|
||||
defer wait.Done()
|
||||
msg, err := app.email.constructExpiry(data.Code, data, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedConstructExpiryAdmin, data.Code, err)
|
||||
} else {
|
||||
// Check whether notify "address" is an email address or Jellyfin ID
|
||||
if strings.Contains(addr, "@") {
|
||||
err = app.email.send(msg, addr)
|
||||
} else {
|
||||
err = app.sendByID(msg, addr)
|
||||
}
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedSendExpiryAdmin, data.Code, addr, err)
|
||||
} else {
|
||||
app.info.Printf(lm.SentExpiryAdmin, data.Code, addr)
|
||||
}
|
||||
}
|
||||
}(address)
|
||||
}
|
||||
return &wait
|
||||
}
|
||||
|
||||
// @Summary Create a new invite.
|
||||
// @Produce json
|
||||
// @Param generateInviteDTO body generateInviteDTO true "New invite request object"
|
||||
@@ -196,7 +163,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
|
||||
// @tags Invites
|
||||
func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
var req generateInviteDTO
|
||||
app.debug.Println("Generating new invite")
|
||||
app.debug.Println(lm.GenerateInvite)
|
||||
gc.BindJSON(&req)
|
||||
currentTime := time.Now()
|
||||
validTill := currentTime.AddDate(0, req.Months, req.Days)
|
||||
@@ -230,13 +197,12 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||
addressValid := false
|
||||
discord := ""
|
||||
app.debug.Printf("%s: Sending invite message", invite.Code)
|
||||
if discordEnabled && (!strings.Contains(req.SendTo, "@") || strings.HasPrefix(req.SendTo, "@")) {
|
||||
users := app.discord.GetUsers(req.SendTo)
|
||||
if len(users) == 0 {
|
||||
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
|
||||
invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipNoUser, req.SendTo)
|
||||
} else if len(users) > 1 {
|
||||
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
|
||||
invite.SendTo = fmt.Sprintf(lm.FailedSendToTooltipMultiUser, req.SendTo)
|
||||
} else {
|
||||
invite.SendTo = req.SendTo
|
||||
addressValid = true
|
||||
@@ -249,8 +215,10 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
if addressValid {
|
||||
msg, err := app.email.constructInvite(invite.Code, 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", invite.Code, err)
|
||||
// Slight misuse of the template
|
||||
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, req.SendTo, err)
|
||||
|
||||
app.err.Printf(lm.FailedConstructInviteMessage, invite.Code, err)
|
||||
} else {
|
||||
var err error
|
||||
if discord != "" {
|
||||
@@ -259,10 +227,10 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
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", invite.Code, invite.SendTo, err)
|
||||
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, req.SendTo, err)
|
||||
app.err.Println(invite.SendTo)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent invite email to \"%s\"", invite.Code, req.SendTo)
|
||||
app.info.Printf(lm.SentInviteMessage, invite.Code, req.SendTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,7 +265,6 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @tags Invites
|
||||
func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
app.debug.Println("Invites requested")
|
||||
currentTime := time.Now()
|
||||
app.checkInvites()
|
||||
var invites []inviteDTO
|
||||
@@ -332,7 +299,7 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
if err != nil {
|
||||
date, err := timefmt.Parse(pair[1], app.datePattern+" "+app.timePattern)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to parse usedBy time: %v", err)
|
||||
app.err.Printf(lm.FailedParseTime, err)
|
||||
}
|
||||
unix = date.Unix()
|
||||
}
|
||||
@@ -347,7 +314,6 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
invite.SendTo = inv.SendTo
|
||||
}
|
||||
if len(inv.Notify) != 0 {
|
||||
// app.err.Printf("%s has notify section: %+v, you are %s\n", inv.Code, inv.Notify, gc.GetString("jfId"))
|
||||
var addressOrID string
|
||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
addressOrID = gc.GetString("jfId")
|
||||
@@ -365,23 +331,8 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
}
|
||||
invites = append(invites, invite)
|
||||
}
|
||||
fullProfileList := app.storage.GetProfiles()
|
||||
profiles := make([]string, len(fullProfileList))
|
||||
if len(profiles) != 0 {
|
||||
defaultProfile := app.storage.GetDefaultProfile()
|
||||
profiles[0] = defaultProfile.Name
|
||||
i := 1
|
||||
if len(fullProfileList) > 1 {
|
||||
app.storage.db.ForEach(badgerhold.Where("Name").Ne(profiles[0]), func(p *Profile) error {
|
||||
profiles[i] = p.Name
|
||||
i++
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
resp := getInvitesDTO{
|
||||
Profiles: profiles,
|
||||
Invites: invites,
|
||||
Invites: invites,
|
||||
}
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
@@ -393,14 +344,13 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /invites/profile [post]
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
// @tags Invites
|
||||
func (app *appContext) SetProfile(gc *gin.Context) {
|
||||
var req inviteProfileDTO
|
||||
gc.BindJSON(&req)
|
||||
app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile)
|
||||
// "" means "Don't apply profile"
|
||||
if _, ok := app.storage.GetProfileKey(req.Profile); !ok && req.Profile != "" {
|
||||
app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile)
|
||||
app.err.Printf(lm.FailedGetProfile, req.Profile)
|
||||
respond(500, "Profile not found", gc)
|
||||
return
|
||||
}
|
||||
@@ -424,11 +374,11 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
gc.BindJSON(&req)
|
||||
changed := false
|
||||
for code, settings := range req {
|
||||
app.debug.Printf("%s: Notification settings change requested", code)
|
||||
invite, ok := app.storage.GetInvitesKey(code)
|
||||
if !ok {
|
||||
app.err.Printf("%s Notification setting change failed: Invalid code", code)
|
||||
respond(400, "Invalid invite code", gc)
|
||||
msg := fmt.Sprintf(lm.InvalidInviteCode, code)
|
||||
app.err.Println(msg)
|
||||
respond(400, msg, gc)
|
||||
return
|
||||
}
|
||||
var address string
|
||||
@@ -436,9 +386,8 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
if jellyfinLogin {
|
||||
var addressAvailable bool = app.getAddressOrName(gc.GetString("jfId")) != ""
|
||||
if !addressAvailable {
|
||||
app.err.Printf("%s: Couldn't find contact method for admin. Make sure one is set.", code)
|
||||
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
||||
respond(500, "Missing user contact method", gc)
|
||||
app.err.Printf(lm.FailedGetContactMethod, gc.GetString("jfId"))
|
||||
respond(500, fmt.Sprintf(lm.FailedGetContactMethod, "admin"), gc)
|
||||
return
|
||||
}
|
||||
address = gc.GetString("jfId")
|
||||
@@ -453,15 +402,12 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
} /*else {
|
||||
if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
|
||||
*/
|
||||
if _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] {
|
||||
invite.Notify[address]["notify-expiry"] = settings["notify-expiry"]
|
||||
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address)
|
||||
changed = true
|
||||
}
|
||||
if _, ok := settings["notify-creation"]; ok && invite.Notify[address]["notify-creation"] != settings["notify-creation"] {
|
||||
invite.Notify[address]["notify-creation"] = settings["notify-creation"]
|
||||
app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings["notify-creation"], address)
|
||||
changed = true
|
||||
for _, notifyType := range []string{"notify-expiry", "notify-creation"} {
|
||||
if _, ok := settings[notifyType]; ok && invite.Notify[address][notifyType] != settings[notifyType] {
|
||||
invite.Notify[address][notifyType] = settings[notifyType]
|
||||
app.debug.Printf(lm.SetAdminNotify, notifyType, settings[notifyType], address)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
app.storage.SetInvitesKey(code, invite)
|
||||
@@ -480,7 +426,6 @@ func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||
var req deleteInviteDTO
|
||||
gc.BindJSON(&req)
|
||||
app.debug.Printf("%s: Deletion requested", req.Code)
|
||||
inv, ok := app.storage.GetInvitesKey(req.Code)
|
||||
if ok {
|
||||
app.storage.DeleteInvitesKey(req.Code)
|
||||
@@ -495,10 +440,10 @@ func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||
Time: time.Now(),
|
||||
}, gc, false)
|
||||
|
||||
app.info.Printf("%s: Invite deleted", req.Code)
|
||||
app.info.Printf(lm.DeleteInvite, req.Code)
|
||||
respondBool(200, true, gc)
|
||||
return
|
||||
}
|
||||
app.err.Printf("%s: Deletion failed: Invalid code", req.Code)
|
||||
app.err.Printf(lm.FailedDeleteInvite, req.Code, "invalid code")
|
||||
respond(400, "Code doesn't exist", gc)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/jellyseerr"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
// @Summary Get a list of Jellyseerr users.
|
||||
@@ -15,14 +18,12 @@ import (
|
||||
// @Security Bearer
|
||||
// @tags Jellyseerr
|
||||
func (app *appContext) JellyseerrUsers(gc *gin.Context) {
|
||||
app.debug.Println("Jellyseerr users requested")
|
||||
users, err := app.js.GetUsers()
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to get users from Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyseerr, err)
|
||||
respond(500, "Couldn't get users", gc)
|
||||
return
|
||||
}
|
||||
app.debug.Printf("Jellyseerr users retrieved: %d", len(users))
|
||||
userlist := make([]ombiUser, len(users))
|
||||
i := 0
|
||||
for _, u := range users {
|
||||
@@ -60,14 +61,14 @@ func (app *appContext) SetJellyseerrProfile(gc *gin.Context) {
|
||||
}
|
||||
u, err := app.js.UserByID(jellyseerrID)
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't get user from Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyseerr, err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
profile.Jellyseerr.User = u.UserTemplate
|
||||
n, err := app.js.GetNotificationPreferencesByID(jellyseerrID)
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't get user's notification prefs from Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedGetJellyseerrNotificationPrefs, gc.Param("id"), err)
|
||||
respond(500, "Couldn't get user notification prefs", gc)
|
||||
return
|
||||
}
|
||||
@@ -98,3 +99,67 @@ func (app *appContext) DeleteJellyseerrProfile(gc *gin.Context) {
|
||||
app.storage.SetProfileKey(profileName, profile)
|
||||
respondBool(204, true, gc)
|
||||
}
|
||||
|
||||
type JellyseerrWrapper struct {
|
||||
*jellyseerr.Jellyseerr
|
||||
}
|
||||
|
||||
func (js *JellyseerrWrapper) ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool) {
|
||||
// Gets existing user (not possible) or imports the given user.
|
||||
_, err = js.MustGetUser(jellyfinID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ok = true
|
||||
err = js.ApplyTemplateToUser(jellyfinID, profile.Jellyseerr.User)
|
||||
if err != nil {
|
||||
err = fmt.Errorf(lm.FailedApplyTemplate, "user", lm.Jellyseerr, jellyfinID, err)
|
||||
return
|
||||
}
|
||||
err = js.ApplyNotificationsTemplateToUser(jellyfinID, profile.Jellyseerr.Notifications)
|
||||
if err != nil {
|
||||
err = fmt.Errorf(lm.FailedApplyTemplate, "notifications", lm.Jellyseerr, jellyfinID, err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (js *JellyseerrWrapper) AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error) {
|
||||
_, err = js.MustGetUser(jellyfinID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
contactMethods := map[jellyseerr.NotificationsField]any{}
|
||||
if emailEnabled {
|
||||
err = js.ModifyMainUserSettings(jellyfinID, jellyseerr.MainUserSettings{Email: req.Email})
|
||||
if err != nil {
|
||||
// FIXME: This is a little ugly, considering all other errors are unformatted
|
||||
err = fmt.Errorf(lm.FailedSetEmailAddress, lm.Jellyseerr, jellyfinID, err)
|
||||
return
|
||||
} else {
|
||||
contactMethods[jellyseerr.FieldEmailEnabled] = req.EmailContact
|
||||
}
|
||||
}
|
||||
if discordEnabled && discord != nil {
|
||||
contactMethods[jellyseerr.FieldDiscord] = discord.ID
|
||||
contactMethods[jellyseerr.FieldDiscordEnabled] = req.DiscordContact
|
||||
}
|
||||
if telegramEnabled && discord != nil {
|
||||
contactMethods[jellyseerr.FieldTelegram] = telegram.ChatID
|
||||
contactMethods[jellyseerr.FieldTelegramEnabled] = req.TelegramContact
|
||||
}
|
||||
if len(contactMethods) > 0 {
|
||||
err = js.ModifyNotifications(jellyfinID, contactMethods)
|
||||
if err != nil {
|
||||
// app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (js *JellyseerrWrapper) Name() string { return lm.Jellyseerr }
|
||||
|
||||
func (js *JellyseerrWrapper) Enabled(app *appContext, profile *Profile) bool {
|
||||
return profile != nil && profile.Jellyseerr.Enabled && app.config.Section("jellyseerr").Key("enabled").MustBool(false)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/jellyseerr"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
@@ -134,7 +135,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
||||
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
|
||||
customMessage, ok := app.storage.GetCustomContentKey(id)
|
||||
if !ok && id != "Announcement" {
|
||||
app.err.Printf("Failed to get custom message with ID \"%s\"", id)
|
||||
app.err.Printf(lm.FailedGetCustomMessage, id)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -328,7 +329,7 @@ func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
||||
jellyseerr.FieldTelegram: tgUser.ChatID,
|
||||
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
|
||||
linkExistingOmbiDiscordTelegram(app)
|
||||
@@ -361,11 +362,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
||||
tgUser.Contact = req.Telegram
|
||||
app.storage.SetTelegramKey(req.ID, tgUser)
|
||||
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(lm.SetContactPrefForService, lm.Telegram, tgUser.Username, req.Telegram)
|
||||
jsPrefs[jellyseerr.FieldTelegramEnabled] = req.Telegram
|
||||
}
|
||||
}
|
||||
@@ -374,11 +371,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
||||
dcUser.Contact = req.Discord
|
||||
app.storage.SetDiscordKey(req.ID, dcUser)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Discord {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||
app.debug.Printf(lm.SetContactPrefForService, lm.Discord, dcUser.Username, req.Discord)
|
||||
jsPrefs[jellyseerr.FieldDiscordEnabled] = req.Discord
|
||||
}
|
||||
}
|
||||
@@ -387,11 +380,7 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
||||
mxUser.Contact = req.Matrix
|
||||
app.storage.SetMatrixKey(req.ID, mxUser)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Matrix {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
|
||||
app.debug.Printf(lm.SetContactPrefForService, lm.Matrix, mxUser.UserID, req.Matrix)
|
||||
}
|
||||
}
|
||||
if email, ok := app.storage.GetEmailsKey(req.ID); ok {
|
||||
@@ -399,18 +388,14 @@ func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Conte
|
||||
email.Contact = req.Email
|
||||
app.storage.SetEmailsKey(req.ID, email)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Email {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
|
||||
app.debug.Printf(lm.SetContactPrefForService, lm.Email, email.Addr, req.Email)
|
||||
jsPrefs[jellyseerr.FieldEmailEnabled] = req.Email
|
||||
}
|
||||
}
|
||||
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
||||
err := app.js.ModifyNotifications(req.ID, jsPrefs)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to sync contact prefs with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
@@ -429,7 +414,7 @@ func (app *appContext) TelegramVerified(gc *gin.Context) {
|
||||
respondBool(200, ok, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code.
|
||||
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code. NOTE: "/invite" might have been changed in Settings > URL Paths.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Success 401 {object} boolResponse
|
||||
@@ -446,14 +431,14 @@ func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
token, ok := app.telegram.TokenVerified(pin)
|
||||
if ok && app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
|
||||
app.discord.DeleteVerifiedUser(pin)
|
||||
app.discord.DeleteVerifiedToken(pin)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, ok, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
|
||||
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code. NOTE: "/invite" might have been changed in Settings > URL Paths.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
@@ -469,7 +454,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
||||
}
|
||||
pin := gc.Param("pin")
|
||||
user, ok := app.discord.UserVerified(pin)
|
||||
if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.ID) {
|
||||
if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.MethodID().(string)) {
|
||||
delete(app.discord.verifiedTokens, pin)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
@@ -477,7 +462,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
||||
respondBool(200, ok, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns a 10-minute, one-use Discord server invite
|
||||
// @Summary Returns a 10-minute, one-use Discord server invite. NOTE: "/invite" might have been changed in Settings > URL Paths.
|
||||
// @Produce json
|
||||
// @Success 200 {object} DiscordInviteDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
@@ -487,7 +472,7 @@ func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
||||
// @Router /invite/{invCode}/discord/invite [get]
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||
if app.discord.inviteChannelName == "" {
|
||||
if app.discord.InviteChannel.Name == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -504,7 +489,7 @@ func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
|
||||
}
|
||||
|
||||
// @Summary Generate and send a new PIN to a specified Matrix user.
|
||||
// @Summary Generate and send a new PIN to a specified Matrix user. NOTE: "/invite" might have been changed in Settings > URL Paths.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} stringResponse
|
||||
@@ -543,7 +528,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Check whether a matrix PIN is valid, and mark the token as verified if so. Requires invite code.
|
||||
// @Summary Check whether a matrix PIN is valid, and mark the token as verified if so. Requires invite code. NOTE: "/invite" might have been changed in Settings > URL Paths.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
@@ -555,7 +540,7 @@ func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||
app.debug.Println("Matrix: Invite code was invalid")
|
||||
app.debug.Printf(lm.InvalidInviteCode, code)
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -563,12 +548,12 @@ func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
user, ok := app.matrix.tokens[pin]
|
||||
if !ok {
|
||||
app.debug.Println("Matrix: PIN not found")
|
||||
app.debug.Printf(lm.InvalidPIN, pin)
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
if user.User.UserID != userID {
|
||||
app.debug.Println("Matrix: User ID of PIN didn't match")
|
||||
app.debug.Printf(lm.UnauthorizedPIN, pin)
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -596,18 +581,18 @@ func (app *appContext) MatrixLogin(gc *gin.Context) {
|
||||
}
|
||||
token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password)
|
||||
if err != nil {
|
||||
app.err.Printf("Matrix: Failed to generate token: %v", err)
|
||||
app.err.Printf(lm.FailedGenerateToken, err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
tempConfig, _ := ini.ShadowLoad(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)
|
||||
app.err.Printf(lm.FailedWriting, app.configPath, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -629,20 +614,18 @@ func (app *appContext) MatrixConnect(gc *gin.Context) {
|
||||
if app.storage.GetMatrix() == nil {
|
||||
app.storage.deprecatedMatrix = matrixStore{}
|
||||
}
|
||||
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
|
||||
roomID, err := app.matrix.CreateRoom(req.UserID)
|
||||
if err != nil {
|
||||
app.err.Printf("Matrix: Failed to create room: %v", err)
|
||||
app.err.Printf(lm.FailedCreateRoom, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.storage.SetMatrixKey(req.JellyfinID, MatrixUser{
|
||||
UserID: req.UserID,
|
||||
RoomID: string(roomID),
|
||||
Lang: "en-us",
|
||||
Contact: true,
|
||||
Encrypted: encrypted,
|
||||
UserID: req.UserID,
|
||||
RoomID: string(roomID),
|
||||
Lang: "en-us",
|
||||
Contact: true,
|
||||
})
|
||||
app.matrix.isEncrypted[roomID] = encrypted
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
@@ -701,7 +684,7 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
|
||||
jellyseerr.FieldDiscord: req.DiscordID,
|
||||
jellyseerr.FieldDiscordEnabled: true,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
@@ -739,7 +722,7 @@ func (app *appContext) UnlinkDiscord(gc *gin.Context) {
|
||||
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldDiscordEnabled: false,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
@@ -775,7 +758,7 @@ func (app *appContext) UnlinkTelegram(gc *gin.Context) {
|
||||
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldTelegramEnabled: false,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
|
||||
121
api-ombi.go
@@ -3,43 +3,53 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/jfa-go/ombi"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
)
|
||||
|
||||
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
|
||||
ombiUsers, code, err := app.ombi.GetUsers()
|
||||
if err != nil || code != 200 {
|
||||
return nil, code, err
|
||||
}
|
||||
jfUser, code, err := app.jf.UserByID(jfID, false)
|
||||
if err != nil || code != 200 {
|
||||
return nil, code, err
|
||||
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, error) {
|
||||
jfUser, err := app.jf.UserByID(jfID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
username := jfUser.Name
|
||||
email := ""
|
||||
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
||||
email = e.Addr
|
||||
}
|
||||
user, err := app.ombi.getUser(username, email)
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (ombi *OmbiWrapper) getUser(username string, email string) (map[string]interface{}, error) {
|
||||
ombiUsers, err := ombi.GetUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ombiUser := range ombiUsers {
|
||||
ombiAddr := ""
|
||||
if a, ok := ombiUser["emailAddress"]; ok && a != nil {
|
||||
ombiAddr = a.(string)
|
||||
}
|
||||
if ombiUser["userName"].(string) == username || (ombiAddr == email && email != "") {
|
||||
return ombiUser, code, err
|
||||
return ombiUser, err
|
||||
}
|
||||
}
|
||||
return nil, 400, fmt.Errorf("couldn't find user")
|
||||
// Gets a generic "not found" type error
|
||||
return nil, common.GenericErr(404, err)
|
||||
}
|
||||
|
||||
// Returns a user with the given name who has been imported from Jellyfin/Emby by Ombi
|
||||
func (app *appContext) getOmbiImportedUser(name string) (map[string]interface{}, int, error) {
|
||||
func (ombi *OmbiWrapper) getImportedUser(name string) (map[string]interface{}, error) {
|
||||
// Ombi User Types: 3/4 = Emby, 5 = Jellyfin
|
||||
ombiUsers, code, err := app.ombi.GetUsers()
|
||||
if err != nil || code != 200 {
|
||||
return nil, code, err
|
||||
ombiUsers, err := ombi.GetUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ombiUser := range ombiUsers {
|
||||
if ombiUser["userName"].(string) == name {
|
||||
@@ -52,10 +62,11 @@ func (app *appContext) getOmbiImportedUser(name string) (map[string]interface{},
|
||||
} else if uType != 3 && uType != 4 { // Emby
|
||||
continue
|
||||
}
|
||||
return ombiUser, code, err
|
||||
return ombiUser, err
|
||||
}
|
||||
}
|
||||
return nil, 400, fmt.Errorf("couldn't find user")
|
||||
// Gets a generic "not found" type error
|
||||
return nil, common.GenericErr(404, err)
|
||||
}
|
||||
|
||||
// @Summary Get a list of Ombi users.
|
||||
@@ -66,10 +77,9 @@ func (app *appContext) getOmbiImportedUser(name string) (map[string]interface{},
|
||||
// @Security Bearer
|
||||
// @tags Ombi
|
||||
func (app *appContext) OmbiUsers(gc *gin.Context) {
|
||||
app.debug.Println("Ombi users requested")
|
||||
users, status, err := app.ombi.GetUsers()
|
||||
if err != nil || status != 200 {
|
||||
app.err.Printf("Failed to get users from Ombi (%d): %v", status, err)
|
||||
users, err := app.ombi.GetUsers()
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
|
||||
respond(500, "Couldn't get users", gc)
|
||||
return
|
||||
}
|
||||
@@ -103,9 +113,9 @@ func (app *appContext) SetOmbiProfile(gc *gin.Context) {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
template, code, err := app.ombi.TemplateByID(req.ID)
|
||||
if err != nil || code != 200 || len(template) == 0 {
|
||||
app.err.Printf("Couldn't get user from Ombi (%d): %v", code, err)
|
||||
template, err := app.ombi.TemplateByID(req.ID)
|
||||
if err != nil || len(template) == 0 {
|
||||
app.err.Printf(lm.FailedGetUsers, lm.Ombi, err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
@@ -136,7 +146,11 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
|
||||
respondBool(204, true, gc)
|
||||
}
|
||||
|
||||
func (app *appContext) applyOmbiProfile(user map[string]interface{}, profile map[string]interface{}) (status int, err error) {
|
||||
type OmbiWrapper struct {
|
||||
*ombi.Ombi
|
||||
}
|
||||
|
||||
func (ombi *OmbiWrapper) applyProfile(user map[string]interface{}, profile map[string]interface{}) (err error) {
|
||||
for k, v := range profile {
|
||||
switch v.(type) {
|
||||
case map[string]interface{}, []interface{}:
|
||||
@@ -147,6 +161,63 @@ func (app *appContext) applyOmbiProfile(user map[string]interface{}, profile map
|
||||
}
|
||||
}
|
||||
}
|
||||
status, err = app.ombi.ModifyUser(user)
|
||||
err = ombi.ModifyUser(user)
|
||||
return
|
||||
}
|
||||
|
||||
func (ombi *OmbiWrapper) ImportUser(jellyfinID string, req newUserDTO, profile Profile) (err error, ok bool) {
|
||||
errors, err := ombi.NewUser(req.Username, req.Password, req.Email, profile.Ombi)
|
||||
var ombiUser map[string]interface{}
|
||||
if err != nil {
|
||||
// Check if on the off chance, Ombi's user importer has already added the account.
|
||||
ombiUser, err = ombi.getImportedUser(req.Username)
|
||||
if err == nil {
|
||||
// app.info.Println(lm.Ombi + " " + lm.UserExists)
|
||||
profile.Ombi["password"] = req.Password
|
||||
err = ombi.applyProfile(ombiUser, profile.Ombi)
|
||||
if err != nil {
|
||||
err = fmt.Errorf(lm.FailedApplyProfile, lm.Ombi, req.Username, err)
|
||||
}
|
||||
} else {
|
||||
if len(errors) != 0 {
|
||||
err = fmt.Errorf("%v, %s", err, strings.Join(errors, ", "))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func (ombi *OmbiWrapper) AddContactMethods(jellyfinID string, req newUserDTO, discord *DiscordUser, telegram *TelegramUser) (err error) {
|
||||
var ombiUser map[string]interface{}
|
||||
ombiUser, err = ombi.getUser(req.Username, req.Email)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if discordEnabled || telegramEnabled {
|
||||
dID := ""
|
||||
tUser := ""
|
||||
if discord != nil {
|
||||
dID = discord.ID
|
||||
}
|
||||
if telegram != nil {
|
||||
tUser = telegram.Username
|
||||
}
|
||||
var resp string
|
||||
resp, err = ombi.SetNotificationPrefs(ombiUser, dID, tUser)
|
||||
if err != nil {
|
||||
if resp != "" {
|
||||
err = fmt.Errorf("%v, %s", err, resp)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ombi *OmbiWrapper) Name() string { return lm.Ombi }
|
||||
|
||||
func (ombi *OmbiWrapper) Enabled(app *appContext, profile *Profile) bool {
|
||||
return profile != nil && profile.Ombi != nil && len(profile.Ombi) != 0 && app.config.Section("ombi").Key("enabled").MustBool(false)
|
||||
}
|
||||
|
||||
@@ -1,20 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
// @Summary Get a list of profiles
|
||||
// @Summary Get the names of all available profile.
|
||||
// @Produce json
|
||||
// @Success 200 {object} getProfileNamesDTO
|
||||
// @Router /profiles/names [get]
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
func (app *appContext) GetProfileNames(gc *gin.Context) {
|
||||
fullProfileList := app.storage.GetProfiles()
|
||||
profiles := make([]string, len(fullProfileList))
|
||||
if len(profiles) != 0 {
|
||||
defaultProfile := app.storage.GetDefaultProfile()
|
||||
profiles[0] = defaultProfile.Name
|
||||
i := 1
|
||||
if len(fullProfileList) > 1 {
|
||||
app.storage.db.ForEach(badgerhold.Where("Name").Ne(profiles[0]), func(p *Profile) error {
|
||||
profiles[i] = p.Name
|
||||
i++
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
resp := getProfileNamesDTO{
|
||||
Profiles: profiles,
|
||||
}
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
// @Summary Get all available profiles, indexed by their names.
|
||||
// @Produce json
|
||||
// @Success 200 {object} getProfilesDTO
|
||||
// @Router /profiles [get]
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
func (app *appContext) GetProfiles(gc *gin.Context) {
|
||||
app.debug.Println("Profiles requested")
|
||||
out := getProfilesDTO{
|
||||
DefaultProfile: app.storage.GetDefaultProfile().Name,
|
||||
Profiles: map[string]profileDTO{},
|
||||
@@ -52,10 +80,11 @@ func (app *appContext) GetProfiles(gc *gin.Context) {
|
||||
func (app *appContext) SetDefaultProfile(gc *gin.Context) {
|
||||
req := profileChangeDTO{}
|
||||
gc.BindJSON(&req)
|
||||
app.info.Printf("Setting default profile to \"%s\"", req.Name)
|
||||
app.info.Printf(lm.SetDefaultProfile, req.Name)
|
||||
if _, ok := app.storage.GetProfileKey(req.Name); !ok {
|
||||
app.err.Printf("Profile not found: \"%s\"", req.Name)
|
||||
respond(500, "Profile not found", gc)
|
||||
msg := fmt.Sprintf(lm.FailedGetProfile, req.Name)
|
||||
app.err.Println(msg)
|
||||
respond(500, msg, gc)
|
||||
return
|
||||
}
|
||||
app.storage.db.ForEach(&badgerhold.Query{}, func(profile *Profile) error {
|
||||
@@ -79,13 +108,12 @@ func (app *appContext) SetDefaultProfile(gc *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
func (app *appContext) CreateProfile(gc *gin.Context) {
|
||||
app.info.Println("Profile creation requested")
|
||||
var req newProfileDTO
|
||||
gc.BindJSON(&req)
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
user, status, err := app.jf.UserByID(req.ID, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get user from Jellyfin (%d): %v", status, err)
|
||||
user, err := app.jf.UserByID(req.ID, false)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
@@ -94,12 +122,12 @@ func (app *appContext) CreateProfile(gc *gin.Context) {
|
||||
Policy: user.Policy,
|
||||
Homescreen: req.Homescreen,
|
||||
}
|
||||
app.debug.Printf("Creating profile from user \"%s\"", user.Name)
|
||||
app.debug.Printf(lm.CreateProfileFromUser, user.Name)
|
||||
if req.Homescreen {
|
||||
profile.Configuration = user.Configuration
|
||||
profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get DisplayPrefs (%d): %v", status, err)
|
||||
profile.Displayprefs, err = app.jf.GetDisplayPreferences(req.ID)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetJellyfinDisplayPrefs, req.ID, err)
|
||||
respond(500, "Couldn't get displayprefs", gc)
|
||||
return
|
||||
}
|
||||
@@ -145,13 +173,13 @@ func (app *appContext) EnableReferralForProfile(gc *gin.Context) {
|
||||
inv, ok := app.storage.GetInvitesKey(invCode)
|
||||
if !ok {
|
||||
respond(400, "Invalid invite code", gc)
|
||||
app.err.Printf("\"%s\": Failed to enable referrals: invite not found", profileName)
|
||||
app.err.Printf(lm.InvalidInviteCode, invCode)
|
||||
return
|
||||
}
|
||||
profile, ok := app.storage.GetProfileKey(profileName)
|
||||
if !ok {
|
||||
respond(400, "Invalid profile", gc)
|
||||
app.err.Printf("\"%s\": Failed to enable referrals: profile not found", profileName)
|
||||
app.err.Printf(lm.FailedGetProfile, profileName)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
106
api-userpage.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/hrfee/jfa-go/jellyseerr"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
@@ -27,9 +29,9 @@ func (app *appContext) MyDetails(gc *gin.Context) {
|
||||
Id: gc.GetString("jfId"),
|
||||
}
|
||||
|
||||
user, status, err := app.jf.UserByID(resp.Id, false)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get Jellyfin user (%d): %+v\n", status, err)
|
||||
user, err := app.jf.UserByID(resp.Id, false)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||
respond(500, "Failed to get user", gc)
|
||||
return
|
||||
}
|
||||
@@ -133,8 +135,9 @@ func (app *appContext) SetMyContactMethods(gc *gin.Context) {
|
||||
func (app *appContext) LogoutUser(gc *gin.Context) {
|
||||
cookie, err := gc.Cookie("user-refresh")
|
||||
if err != nil {
|
||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||
respond(500, "Couldn't fetch cookies", gc)
|
||||
msg := fmt.Sprintf(lm.FailedGetCookies, "user-refresh", err)
|
||||
app.debug.Println(msg)
|
||||
respond(500, msg, gc)
|
||||
return
|
||||
}
|
||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||
@@ -161,9 +164,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
||||
var target ConfirmationTarget
|
||||
var id string
|
||||
fail := func() {
|
||||
gcHTML(gc, 404, "404.html", gin.H{
|
||||
"cssClass": app.cssClass,
|
||||
"cssVersion": cssVersion,
|
||||
app.gcHTML(gc, 404, "404.html", OtherPage, gin.H{
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
})
|
||||
}
|
||||
@@ -174,21 +175,21 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
||||
}
|
||||
token, err := jwt.Parse(key, checkToken)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to parse key: %s", err)
|
||||
app.err.Printf(lm.FailedParseJWT, err)
|
||||
fail()
|
||||
// respond(500, "unknownError", gc)
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
app.err.Printf("Failed to parse key: %s", err)
|
||||
app.err.Println(lm.FailedCastJWT)
|
||||
fail()
|
||||
// respond(500, "unknownError", gc)
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
||||
app.err.Printf("Invalid key")
|
||||
app.err.Println(lm.InvalidJWT)
|
||||
fail()
|
||||
// respond(400, "invalidKey", gc)
|
||||
return
|
||||
@@ -198,7 +199,7 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
||||
|
||||
// Perform an Action
|
||||
if target == NoOp {
|
||||
gc.Redirect(http.StatusSeeOther, "/my/account")
|
||||
gc.Redirect(http.StatusSeeOther, PAGES.MyAccount)
|
||||
return
|
||||
} else if target == UserEmailChange {
|
||||
app.modifyEmail(id, claims["email"].(string))
|
||||
@@ -212,8 +213,8 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
||||
Time: time.Now(),
|
||||
}, gc, true)
|
||||
|
||||
app.info.Println("Email list modified")
|
||||
gc.Redirect(http.StatusSeeOther, "/my/account")
|
||||
app.info.Printf(lm.UserEmailAdjusted, gc.GetString("jfId"))
|
||||
gc.Redirect(http.StatusSeeOther, PAGES.MyAccount)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -231,7 +232,6 @@ func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
||||
func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
||||
var req ModifyMyEmailDTO
|
||||
gc.BindJSON(&req)
|
||||
app.debug.Println("Email modification requested")
|
||||
if !strings.ContainsRune(req.Email, '@') {
|
||||
respond(400, "Invalid Email Address", gc)
|
||||
return
|
||||
@@ -251,26 +251,26 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
||||
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to generate confirmation token: %v", err)
|
||||
app.err.Printf(lm.FailedSignJWT, err)
|
||||
respond(500, "errorUnknown", gc)
|
||||
return
|
||||
}
|
||||
|
||||
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
||||
user, status, err := app.jf.UserByID(id, false)
|
||||
user, err := app.jf.UserByID(id, false)
|
||||
name := ""
|
||||
if status == 200 && err == nil {
|
||||
if err == nil {
|
||||
name = user.Name
|
||||
}
|
||||
app.debug.Printf("%s: Email confirmation required", id)
|
||||
app.debug.Printf(lm.EmailConfirmationRequired, id)
|
||||
respond(401, "confirmEmail", gc)
|
||||
msg, err := app.email.constructConfirmation("", name, key, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct confirmation email: %v", name, err)
|
||||
app.err.Printf(lm.FailedConstructConfirmationEmail, id, err)
|
||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||
app.err.Printf("%s: Failed to send user confirmation email: %v", name, err)
|
||||
app.err.Printf(lm.FailedSendConfirmationEmail, id, req.Email, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent user confirmation email to \"%s\"", name, req.Email)
|
||||
app.err.Printf(lm.SentConfirmationEmail, id, req.Email)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -290,7 +290,7 @@ func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @tags User Page
|
||||
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
|
||||
if app.discord.inviteChannelName == "" {
|
||||
if app.discord.InviteChannel.Name == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -338,7 +338,7 @@ func (app *appContext) GetMyPIN(gc *gin.Context) {
|
||||
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
dcUser, ok := app.discord.AssignedUserVerified(pin, gc.GetString("jfId"))
|
||||
app.discord.DeleteVerifiedUser(pin)
|
||||
app.discord.DeleteVerifiedToken(pin)
|
||||
if !ok {
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
@@ -358,7 +358,7 @@ func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
||||
jellyseerr.FieldDiscord: dcUser.ID,
|
||||
jellyseerr.FieldDiscordEnabled: dcUser.Contact,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
@@ -413,7 +413,7 @@ func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
|
||||
jellyseerr.FieldTelegram: tgUser.ChatID,
|
||||
jellyseerr.FieldTelegramEnabled: tgUser.Contact,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
@@ -477,12 +477,12 @@ func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
user, ok := app.matrix.tokens[pin]
|
||||
if !ok {
|
||||
app.debug.Println("Matrix: PIN not found")
|
||||
app.debug.Printf(lm.InvalidPIN, pin)
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
if user.User.UserID != userID {
|
||||
app.debug.Println("Matrix: User ID of PIN didn't match")
|
||||
app.debug.Printf(lm.UnauthorizedPIN, pin)
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -523,7 +523,7 @@ func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
|
||||
jellyseerr.FieldDiscord: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldDiscordEnabled: false,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
@@ -551,7 +551,7 @@ func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
|
||||
jellyseerr.FieldTelegram: jellyseerr.BogusIdentifier,
|
||||
jellyseerr.FieldTelegramEnabled: false,
|
||||
}); err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
|
||||
app.storage.SetActivityKey(shortuuid.New(), Activity{
|
||||
@@ -606,7 +606,6 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
||||
contactMethodAllowed := app.config.Section("user_page").Key("allow_pwr_contact_method").MustBool(true)
|
||||
address := gc.Param("address")
|
||||
if address == "" {
|
||||
app.debug.Println("Ignoring empty request for PWR")
|
||||
cancel.Stop()
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
@@ -616,7 +615,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
||||
|
||||
jfUser, ok := app.ReverseUserSearch(address, usernameAllowed, emailAllowed, contactMethodAllowed)
|
||||
if !ok {
|
||||
app.debug.Printf("Ignoring PWR request: User not found")
|
||||
app.debug.Printf(lm.FailedGetUsers, lm.Jellyfin, "no results")
|
||||
|
||||
for range timerWait {
|
||||
respondBool(204, true, gc)
|
||||
@@ -626,7 +625,7 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
||||
}
|
||||
pwr, err = app.GenInternalReset(jfUser.ID)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to get user from Jellyfin: %v", err)
|
||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||
for range timerWait {
|
||||
respondBool(204, true, gc)
|
||||
return
|
||||
@@ -647,16 +646,16 @@ func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
||||
}, app, false,
|
||||
)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct password reset message for \"%s\": %v", pwr.Username, err)
|
||||
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
|
||||
for range timerWait {
|
||||
respondBool(204, true, gc)
|
||||
return
|
||||
}
|
||||
return
|
||||
} else if err := app.sendByID(msg, jfUser.ID); err != nil {
|
||||
app.err.Printf("Failed to send password reset message to \"%s\": %v", address, err)
|
||||
app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, "?", err)
|
||||
} else {
|
||||
app.info.Printf("Sent password reset message to \"%s\"", address)
|
||||
app.info.Printf(lm.SentPWRMessage, pwr.Username, "?")
|
||||
}
|
||||
for range timerWait {
|
||||
respondBool(204, true, gc)
|
||||
@@ -683,25 +682,24 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
||||
validation := app.validator.validate(req.New)
|
||||
for _, val := range validation {
|
||||
if !val {
|
||||
app.debug.Printf("%s: Change password failed: Invalid password", gc.GetString("jfId"))
|
||||
gc.JSON(400, validation)
|
||||
return
|
||||
}
|
||||
}
|
||||
user, status, err := app.jf.UserByID(gc.GetString("jfId"), false)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to change password: couldn't find user (%d): %+v", status, err)
|
||||
user, err := app.jf.UserByID(gc.GetString("jfId"), false)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetUser, gc.GetString("jfId"), lm.Jellyfin, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
// Authenticate as user to confirm old password.
|
||||
user, status, err = app.authJf.Authenticate(user.Name, req.Old)
|
||||
if status != 200 || err != nil {
|
||||
user, err = app.authJf.Authenticate(user.Name, req.Old)
|
||||
if err != nil {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
status, err = app.jf.SetPassword(gc.GetString("jfId"), req.Old, req.New)
|
||||
if (status != 200 && status != 204) || err != nil {
|
||||
err = app.jf.SetPassword(gc.GetString("jfId"), req.Old, req.New)
|
||||
if err != nil {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -716,18 +714,18 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
||||
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
func() {
|
||||
ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId"))
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", user.Name, status, err)
|
||||
ombiUser, err := app.getOmbiUser(gc.GetString("jfId"))
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetUser, user.Name, lm.Ombi, err)
|
||||
return
|
||||
}
|
||||
ombiUser["password"] = req.New
|
||||
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)
|
||||
err = app.ombi.ModifyUser(ombiUser)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedChangePassword, lm.Ombi, ombiUser["userName"], err)
|
||||
return
|
||||
}
|
||||
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
||||
app.debug.Printf(lm.ChangePassword, lm.Ombi, ombiUser["userName"])
|
||||
}()
|
||||
}
|
||||
cookie, err := gc.Cookie("user-refresh")
|
||||
@@ -735,7 +733,7 @@ func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
|
||||
} else {
|
||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||
app.debug.Printf(lm.FailedGetCookies, "user-refresh", err)
|
||||
}
|
||||
respondBool(204, true, gc)
|
||||
}
|
||||
@@ -761,7 +759,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
|
||||
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
|
||||
err = app.storage.db.Get(user.ReferralTemplateKey, &inv)
|
||||
if !ok || err != nil || user.ReferralTemplateKey == "" {
|
||||
app.debug.Printf("Ignoring referral request, couldn't find template.")
|
||||
app.debug.Printf(lm.FailedGetReferralTemplate, user.ReferralTemplateKey, err)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -782,6 +780,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
|
||||
// If UseReferralExpiry is enabled, we delete it and return nothing.
|
||||
app.storage.DeleteInvitesKey(inv.Code)
|
||||
if inv.UseReferralExpiry {
|
||||
app.debug.Printf(lm.DeleteOldReferral, inv.Code)
|
||||
user, ok := app.storage.GetEmailsKey(gc.GetString("jfId"))
|
||||
if ok {
|
||||
user.ReferralTemplateKey = ""
|
||||
@@ -791,6 +790,7 @@ func (app *appContext) GetMyReferral(gc *gin.Context) {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
app.debug.Printf(lm.RenewOldReferral, inv.Code)
|
||||
inv.Code = GenerateInviteCode()
|
||||
inv.Created = time.Now()
|
||||
inv.ValidTill = inv.Created.Add(REFERRAL_EXPIRY_DAYS * 24 * time.Hour)
|
||||
|
||||
1152
api-users.go
252
api.go
@@ -1,10 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/itchyny/timefmt-go"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
@@ -122,14 +125,14 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
if !valid || req.PIN == "" {
|
||||
app.info.Printf("%s: Password reset failed: Invalid password", req.PIN)
|
||||
app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", lm.InvalidPassword)
|
||||
gc.JSON(400, validation)
|
||||
return
|
||||
}
|
||||
isInternal := false
|
||||
|
||||
if captcha && !app.verifyCaptcha(req.PIN, req.PIN, req.CaptchaText, true) {
|
||||
app.info.Printf("%s: PWR Failed: Captcha Incorrect", req.PIN)
|
||||
app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", lm.IncorrectCaptcha)
|
||||
respond(400, "errorCaptcha", gc)
|
||||
return
|
||||
}
|
||||
@@ -138,7 +141,7 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
if reset, ok := app.internalPWRs[req.PIN]; ok {
|
||||
isInternal = true
|
||||
if time.Now().After(reset.Expiry) {
|
||||
app.info.Printf("Password reset failed: PIN \"%s\" has expired", reset.PIN)
|
||||
app.info.Printf(lm.FailedChangePassword, lm.Jellyfin, "?", fmt.Sprintf(lm.ExpiredPIN, reset.PIN))
|
||||
respondBool(401, false, gc)
|
||||
delete(app.internalPWRs, req.PIN)
|
||||
return
|
||||
@@ -146,18 +149,18 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
userID = reset.ID
|
||||
username = reset.Username
|
||||
|
||||
status, err := app.jf.ResetPasswordAdmin(userID)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Password Reset failed (%d): %v", status, err)
|
||||
respondBool(status, false, gc)
|
||||
err := app.jf.ResetPasswordAdmin(userID)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
delete(app.internalPWRs, req.PIN)
|
||||
} else {
|
||||
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)
|
||||
resp, err := app.jf.ResetPassword(req.PIN)
|
||||
if err != nil || !resp.Success {
|
||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, userID, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
if req.Password == "" || len(resp.UsersReset) == 0 {
|
||||
@@ -168,15 +171,14 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
}
|
||||
|
||||
var user mediabrowser.User
|
||||
var status int
|
||||
var err error
|
||||
if isInternal {
|
||||
user, status, err = app.jf.UserByID(userID, false)
|
||||
user, err = app.jf.UserByID(userID, false)
|
||||
} else {
|
||||
user, status, err = app.jf.UserByName(username, false)
|
||||
user, err = app.jf.UserByName(username, false)
|
||||
}
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get user \"%s\" (%d): %v", username, status, err)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -193,199 +195,126 @@ func (app *appContext) ResetSetPassword(gc *gin.Context) {
|
||||
if isInternal {
|
||||
prevPassword = ""
|
||||
}
|
||||
status, err = app.jf.SetPassword(user.ID, prevPassword, req.Password)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to change password for \"%s\" (%d): %v", username, status, err)
|
||||
err = app.jf.SetPassword(user.ID, prevPassword, req.Password)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedChangePassword, lm.Jellyfin, user.ID, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
// Silently fail for changing ombi passwords
|
||||
// This makes no sense so has been commented out.
|
||||
// It probably did at some point in the past.
|
||||
/* Silently fail for changing ombi passwords
|
||||
if (status != 200 && status != 204) || err != nil {
|
||||
app.err.Printf("Failed to get user \"%s\" from jellyfin/emby (%d): %v", username, status, err)
|
||||
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, 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", username, status, err)
|
||||
} */
|
||||
ombiUser, err := app.getOmbiUser(user.ID)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetUser, user.ID, lm.Ombi, 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)
|
||||
err = app.ombi.ModifyUser(ombiUser)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedChangePassword, lm.Ombi, user.ID, err)
|
||||
respondBool(200, true, gc)
|
||||
return
|
||||
}
|
||||
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
||||
app.debug.Printf(lm.ChangePassword, lm.Ombi, user.ID)
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Get jfa-go configuration.
|
||||
// @Produce json
|
||||
// @Success 200 {object} settings "Uses the same format as config-base.json"
|
||||
// @Success 200 {object} common.Config "Uses the same format as config-base.json"
|
||||
// @Router /config [get]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
app.info.Println("Config requested")
|
||||
resp := app.configBase
|
||||
// Load language options
|
||||
formOptions := app.storage.lang.User.getOptions()
|
||||
fl := resp.Sections["ui"].Settings["language-form"]
|
||||
fl.Options = formOptions
|
||||
fl.Value = app.config.Section("ui").Key("language-form").MustString("en-us")
|
||||
pwrOptions := app.storage.lang.PasswordReset.getOptions()
|
||||
pl := resp.Sections["password_resets"].Settings["language"]
|
||||
pl.Options = pwrOptions
|
||||
pl.Value = app.config.Section("password_resets").Key("language").MustString("en-us")
|
||||
adminOptions := app.storage.lang.Admin.getOptions()
|
||||
al := resp.Sections["ui"].Settings["language-admin"]
|
||||
al.Options = adminOptions
|
||||
al.Value = app.config.Section("ui").Key("language-admin").MustString("en-us")
|
||||
emailOptions := app.storage.lang.Email.getOptions()
|
||||
el := resp.Sections["email"].Settings["language"]
|
||||
el.Options = emailOptions
|
||||
el.Value = app.config.Section("email").Key("language").MustString("en-us")
|
||||
telegramOptions := app.storage.lang.Email.getOptions()
|
||||
tl := resp.Sections["telegram"].Settings["language"]
|
||||
tl.Options = telegramOptions
|
||||
tl.Value = app.config.Section("telegram").Key("language").MustString("en-us")
|
||||
if updater == "" {
|
||||
delete(resp.Sections, "updates")
|
||||
for i, v := range resp.Order {
|
||||
if v == "updates" {
|
||||
resp.Order = append(resp.Order[:i], resp.Order[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
if !MatrixE2EE() {
|
||||
delete(resp.Sections["matrix"].Settings, "encryption")
|
||||
for i, v := range resp.Sections["matrix"].Order {
|
||||
if v == "encryption" {
|
||||
sect := resp.Sections["matrix"]
|
||||
sect.Order = append(sect.Order[:i], sect.Order[i+1:]...)
|
||||
resp.Sections["matrix"] = sect
|
||||
}
|
||||
}
|
||||
}
|
||||
for sectName, section := range resp.Sections {
|
||||
for settingName, setting := range section.Settings {
|
||||
val := app.config.Section(sectName).Key(settingName)
|
||||
s := resp.Sections[sectName].Settings[settingName]
|
||||
switch setting.Type {
|
||||
case "text", "email", "select", "password", "note":
|
||||
s.Value = val.MustString("")
|
||||
case "number":
|
||||
s.Value = val.MustInt(0)
|
||||
case "bool":
|
||||
s.Value = val.MustBool(false)
|
||||
}
|
||||
resp.Sections[sectName].Settings[settingName] = s
|
||||
}
|
||||
}
|
||||
if discordEnabled {
|
||||
r, err := app.discord.ListRoles()
|
||||
if err == nil {
|
||||
roles := make([][2]string, len(r)+1)
|
||||
roles[0] = [2]string{"", "None"}
|
||||
for i, role := range r {
|
||||
roles[i+1] = role
|
||||
}
|
||||
s := resp.Sections["discord"].Settings["apply_role"]
|
||||
s.Options = roles
|
||||
resp.Sections["discord"].Settings["apply_role"] = s
|
||||
}
|
||||
app.PatchConfigDiscordRoles()
|
||||
}
|
||||
|
||||
resp.Sections["ui"].Settings["language-form"] = fl
|
||||
resp.Sections["ui"].Settings["language-admin"] = al
|
||||
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
|
||||
|
||||
// if setting := resp.Sections["invite_emails"].Settings["url_base"]; setting.Value == "" {
|
||||
// setting.Value = strings.TrimSuffix(resp.Sections["password_resets"].Settings["url_base"].Value.(string), "/invite")
|
||||
// resp.Sections["invite_emails"].Settings["url_base"] = setting
|
||||
// }
|
||||
// if setting := resp.Sections["password_resets"].Settings["url_base"]; setting.Value == "" {
|
||||
// setting.Value = strings.TrimSuffix(resp.Sections["invite_emails"].Settings["url_base"].Value.(string), "/invite")
|
||||
// resp.Sections["password_resets"].Settings["url_base"] = setting
|
||||
// }
|
||||
|
||||
gc.JSON(200, resp)
|
||||
gc.JSON(200, app.patchedConfig)
|
||||
}
|
||||
|
||||
// @Summary Modify app config.
|
||||
// @Produce json
|
||||
// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings."
|
||||
// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings (lists split with | delimiter)."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /config [post]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) ModifyConfig(gc *gin.Context) {
|
||||
app.info.Println("Config modification requested")
|
||||
var req configDTO
|
||||
gc.BindJSON(&req)
|
||||
// Load a new config, as we set various default values in app.config that shouldn't be stored.
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
for section, settings := range req {
|
||||
if section != "restart-program" {
|
||||
_, err := tempConfig.GetSection(section)
|
||||
tempConfig, _ := ini.ShadowLoad(app.configPath)
|
||||
for _, section := range app.configBase.Sections {
|
||||
ns, ok := req[section.Section]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
newSection := ns.(map[string]any)
|
||||
iniSection, err := tempConfig.GetSection(section.Section)
|
||||
if err != nil {
|
||||
iniSection, err = tempConfig.NewSection(section.Section)
|
||||
if err != nil {
|
||||
tempConfig.NewSection(section)
|
||||
app.err.Printf(lm.FailedModifyConfig, app.configPath, err)
|
||||
respond(500, err.Error(), gc)
|
||||
return
|
||||
}
|
||||
for setting, value := range settings.(map[string]interface{}) {
|
||||
if section == "email" && setting == "method" && value == "disabled" {
|
||||
value = ""
|
||||
}
|
||||
if (section == "discord" || section == "matrix") && setting == "language" {
|
||||
tempConfig.Section("telegram").Key("language").SetValue(value.(string))
|
||||
} else if value.(string) != app.config.Section(section).Key(setting).MustString("") {
|
||||
tempConfig.Section(section).Key(setting).SetValue(value.(string))
|
||||
}
|
||||
for _, setting := range section.Settings {
|
||||
newValue, ok := newSection[setting.Setting]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Patch disabled to actually be an empty string
|
||||
if section.Section == "email" && setting.Setting == "method" && newValue == "disabled" {
|
||||
newValue = ""
|
||||
}
|
||||
// Copy language preference for chatbots to root one in "telegram"
|
||||
if (section.Section == "discord" || section.Section == "matrix") && setting.Setting == "language" {
|
||||
iniSection.Key("language").SetValue(newValue.(string))
|
||||
} else if setting.Type == common.ListType {
|
||||
splitValues := strings.Split(newValue.(string), "|")
|
||||
// Delete the key first to get rid of any shadow values
|
||||
iniSection.DeleteKey(setting.Setting)
|
||||
for i, v := range splitValues {
|
||||
if i == 0 {
|
||||
iniSection.Key(setting.Setting).SetValue(v)
|
||||
} else {
|
||||
iniSection.Key(setting.Setting).AddShadow(v)
|
||||
}
|
||||
}
|
||||
|
||||
} else if newValue.(string) != iniSection.Key(setting.Setting).MustString("") {
|
||||
iniSection.Key(setting.Setting).SetValue(newValue.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
app.err.Printf(lm.FailedWriting, app.configPath, err)
|
||||
respond(500, err.Error(), gc)
|
||||
return
|
||||
}
|
||||
app.debug.Println("Config saved")
|
||||
app.info.Printf(lm.ModifyConfig, app.configPath)
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
if req["restart-program"] != nil && req["restart-program"].(bool) {
|
||||
app.info.Println("Restarting...")
|
||||
if TRAY {
|
||||
TRAYRESTART <- true
|
||||
} else {
|
||||
RESTART <- true
|
||||
}
|
||||
// Safety Sleep (Ensure shutdown tasks get done)
|
||||
time.Sleep(time.Second)
|
||||
app.Restart()
|
||||
}
|
||||
app.loadConfig()
|
||||
// Patch new settings for next GetConfig
|
||||
app.PatchConfigBase()
|
||||
// Reinitialize password validator on config change, as opposed to every applicable request like in python.
|
||||
if _, ok := req["password_validation"]; ok {
|
||||
app.debug.Println("Reinitializing validator")
|
||||
validatorConf := ValidatorConf{
|
||||
"length": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
||||
"uppercase": app.config.Section("password_validation").Key("upper").MustInt(0),
|
||||
@@ -425,12 +354,13 @@ func (app *appContext) CheckUpdate(gc *gin.Context) {
|
||||
// @tags Configuration
|
||||
func (app *appContext) ApplyUpdate(gc *gin.Context) {
|
||||
if !app.update.CanUpdate {
|
||||
respond(400, "Update is manual", gc)
|
||||
app.info.Printf(lm.FailedApplyUpdate, lm.UpdateManual)
|
||||
respond(400, lm.UpdateManual, gc)
|
||||
return
|
||||
}
|
||||
err := app.update.update()
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to apply update: %v", err)
|
||||
app.err.Printf(lm.FailedApplyUpdate, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
@@ -452,8 +382,9 @@ func (app *appContext) ApplyUpdate(gc *gin.Context) {
|
||||
func (app *appContext) Logout(gc *gin.Context) {
|
||||
cookie, err := gc.Cookie("refresh")
|
||||
if err != nil {
|
||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||
respond(500, "Couldn't fetch cookies", gc)
|
||||
msg := fmt.Sprintf(lm.FailedGetCookies, "refresh", err)
|
||||
app.debug.Println(msg)
|
||||
respond(500, msg, gc)
|
||||
return
|
||||
}
|
||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||
@@ -526,11 +457,7 @@ func (app *appContext) ServeLang(gc *gin.Context) {
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) restart(gc *gin.Context) {
|
||||
app.info.Println("Restarting...")
|
||||
err := app.Restart()
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't restart, try restarting manually: %v", err)
|
||||
}
|
||||
app.Restart()
|
||||
}
|
||||
|
||||
// @Summary Returns the last 100 lines of the log.
|
||||
@@ -544,6 +471,7 @@ func (app *appContext) GetLog(gc *gin.Context) {
|
||||
|
||||
// no need to syscall.exec anymore!
|
||||
func (app *appContext) Restart() error {
|
||||
app.info.Println(lm.Restarting)
|
||||
if TRAY {
|
||||
TRAYRESTART <- true
|
||||
} else {
|
||||
|
||||
83
auth.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
)
|
||||
@@ -41,6 +43,8 @@ func (app *appContext) webAuth() gin.HandlerFunc {
|
||||
return app.authenticate
|
||||
}
|
||||
|
||||
func (app *appContext) authLog(v any) { app.debug.PrintfCustomLevel(4, lm.FailedAuthRequest, v) }
|
||||
|
||||
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
|
||||
func CreateToken(userId, jfId string, admin bool) (string, string, error) {
|
||||
var token, refresh string
|
||||
@@ -72,32 +76,26 @@ func (app *appContext) decodeValidateAuthHeader(gc *gin.Context) (claims jwt.Map
|
||||
ok = false
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
if header[0] != "Bearer" {
|
||||
app.debug.Println("Invalid authorization header")
|
||||
app.authLog(lm.InvalidAuthHeader)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
token, err := jwt.Parse(string(header[1]), checkToken)
|
||||
if err != nil {
|
||||
app.debug.Printf("Auth denied: %s", err)
|
||||
app.authLog(fmt.Sprintf(lm.FailedParseJWT, err))
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
claims, ok = token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
app.debug.Println("Invalid JWT")
|
||||
app.authLog(lm.FailedCastJWT)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
expiryUnix := int64(claims["exp"].(float64))
|
||||
if err != nil {
|
||||
app.debug.Printf("Auth denied: %s", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(expiryUnix, 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
|
||||
app.debug.Printf("Auth denied: Invalid token")
|
||||
app.authLog(lm.InvalidJWT)
|
||||
// app.debug.Printf("Expiry: %+v, OK: %t, Valid: %t, ClaimType: %s\n", expiry, ok, token.Valid, claims["type"].(string))
|
||||
respond(401, "Unauthorized", gc)
|
||||
ok = false
|
||||
@@ -115,7 +113,7 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
||||
}
|
||||
isAdminToken := claims["admin"].(bool)
|
||||
if !isAdminToken {
|
||||
app.debug.Printf("Auth denied: Token was not for admin access")
|
||||
app.authLog(lm.NonAdminToken)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@@ -130,14 +128,13 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
app.debug.Printf("Couldn't find user ID \"%s\"", userID)
|
||||
app.authLog(fmt.Sprintf(lm.NonAdminUser, userID))
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
gc.Set("jfId", jfID)
|
||||
gc.Set("userId", userID)
|
||||
gc.Set("userMode", false)
|
||||
app.debug.Println("Auth succeeded")
|
||||
gc.Next()
|
||||
}
|
||||
|
||||
@@ -160,7 +157,7 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context, userpage bool)
|
||||
password = creds[1]
|
||||
ok = false
|
||||
if username == "" || password == "" {
|
||||
app.logIpDebug(gc, userpage, "Auth denied: blank username/password")
|
||||
app.logIpDebug(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.EmptyUserOrPass))
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@@ -170,19 +167,18 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context, userpage bool)
|
||||
|
||||
func (app *appContext) validateJellyfinCredentials(username, password string, gc *gin.Context, userpage bool) (user mediabrowser.User, ok bool) {
|
||||
ok = false
|
||||
user, status, err := app.authJf.Authenticate(username, password)
|
||||
if status != 200 || err != nil {
|
||||
if status == 401 || status == 400 {
|
||||
app.logIpInfo(gc, userpage, "Auth denied: Invalid username/password (Jellyfin)")
|
||||
user, err := app.authJf.Authenticate(username, password)
|
||||
if err != nil {
|
||||
if errors.As(err, &mediabrowser.ErrUnauthorized{}) {
|
||||
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
if status == 403 {
|
||||
app.logIpInfo(gc, userpage, "Auth denied: Jellyfin account disabled")
|
||||
} else if errors.As(err, &mediabrowser.ErrForbidden{}) {
|
||||
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.UserDisabled))
|
||||
respond(403, "yourAccountWasDisabled", gc)
|
||||
return
|
||||
}
|
||||
app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
|
||||
app.authLog(fmt.Sprintf(lm.FailedAuthJellyfin, app.jf.Server, 0, err))
|
||||
respond(500, "Jellyfin error", gc)
|
||||
return
|
||||
}
|
||||
@@ -199,7 +195,7 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
|
||||
// @tags Auth
|
||||
// @Security getTokenAuth
|
||||
func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
app.logIpInfo(gc, false, "Token requested (login attempt)")
|
||||
app.logIpInfo(gc, false, fmt.Sprintf(lm.RequestingToken, lm.TokenLoginAttempt))
|
||||
username, password, ok := app.decodeValidateLoginHeader(gc, false)
|
||||
if !ok {
|
||||
return
|
||||
@@ -209,13 +205,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
for _, user := range app.adminUsers {
|
||||
if user.Username == username && user.Password == password {
|
||||
match = true
|
||||
app.debug.Println("Found existing user")
|
||||
userID = user.UserID
|
||||
break
|
||||
}
|
||||
}
|
||||
if !app.jellyfinLogin && !match {
|
||||
app.logIpInfo(gc, false, "Auth denied: Invalid username/password")
|
||||
app.logIpInfo(gc, false, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@@ -233,7 +228,7 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
}
|
||||
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
|
||||
if !accountsAdmin {
|
||||
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", username)
|
||||
app.authLog(fmt.Sprintf(lm.NonAdminUser, username))
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@@ -243,16 +238,19 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
newUser := User{
|
||||
UserID: userID,
|
||||
}
|
||||
app.debug.Printf("Token generated for user \"%s\"", username)
|
||||
app.debug.Printf(lm.GenerateToken, username)
|
||||
app.adminUsers = append(app.adminUsers, newUser)
|
||||
}
|
||||
token, refresh, err := CreateToken(userID, jfID, true)
|
||||
if err != nil {
|
||||
app.err.Printf("getToken failed: Couldn't generate token (%s)", err)
|
||||
app.err.Printf(lm.FailedGenerateToken, err)
|
||||
respond(500, "Couldn't generate token", gc)
|
||||
return
|
||||
}
|
||||
host := gc.Request.URL.Hostname()
|
||||
// host := gc.Request.URL.Hostname()
|
||||
host := app.ExternalDomain
|
||||
|
||||
// Before you think this is broken: the first "true" arg is for "secure", i.e. only HTTPS!
|
||||
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true)
|
||||
gc.JSON(200, getTokenDTO{token})
|
||||
}
|
||||
@@ -261,35 +259,29 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
|
||||
ok = false
|
||||
cookie, err := gc.Cookie(cookieName)
|
||||
if err != nil || cookie == "" {
|
||||
app.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err)
|
||||
app.authLog(fmt.Sprintf(lm.FailedGetCookies, cookieName, err))
|
||||
respond(400, "Couldn't get token", gc)
|
||||
return
|
||||
}
|
||||
for _, token := range app.invalidTokens {
|
||||
if cookie == token {
|
||||
app.debug.Println("getTokenRefresh: Invalid token")
|
||||
respond(401, "Invalid token", gc)
|
||||
app.authLog(lm.LocallyInvalidatedJWT)
|
||||
respond(401, lm.InvalidJWT, gc)
|
||||
return
|
||||
}
|
||||
}
|
||||
token, err := jwt.Parse(cookie, checkToken)
|
||||
if err != nil {
|
||||
app.debug.Println("getTokenRefresh: Invalid token")
|
||||
respond(400, "Invalid token", gc)
|
||||
app.authLog(fmt.Sprintf(lm.FailedParseJWT, err))
|
||||
respond(400, lm.InvalidJWT, gc)
|
||||
return
|
||||
}
|
||||
claims, ok = token.Claims.(jwt.MapClaims)
|
||||
expiryUnix := int64(claims["exp"].(float64))
|
||||
if err != nil {
|
||||
app.debug.Printf("getTokenRefresh: Invalid token expiry: %s", err)
|
||||
respond(401, "Invalid token", gc)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(expiryUnix, 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
||||
app.debug.Printf("getTokenRefresh: Invalid token: %+v", err)
|
||||
respond(401, "Invalid token", gc)
|
||||
app.authLog(lm.InvalidJWT)
|
||||
respond(401, lm.InvalidJWT, gc)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
@@ -304,7 +296,7 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
|
||||
// @Router /token/refresh [get]
|
||||
// @tags Auth
|
||||
func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||
app.logIpInfo(gc, false, "Token requested (refresh token)")
|
||||
app.logIpInfo(gc, false, fmt.Sprintf(lm.RequestingToken, lm.TokenRefresh))
|
||||
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
|
||||
if !ok {
|
||||
return
|
||||
@@ -313,11 +305,12 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||
jfID := claims["jfid"].(string)
|
||||
jwt, refresh, err := CreateToken(userID, jfID, true)
|
||||
if err != nil {
|
||||
app.err.Printf("getTokenRefresh failed: Couldn't generate token (%s)", err)
|
||||
app.err.Printf(lm.FailedGenerateToken, err)
|
||||
respond(500, "Couldn't generate token", gc)
|
||||
return
|
||||
}
|
||||
host := gc.Request.URL.Hostname()
|
||||
// host := gc.Request.URL.Hostname()
|
||||
host := app.ExternalDomain
|
||||
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", host, true, true)
|
||||
gc.JSON(200, getTokenDTO{jwt})
|
||||
}
|
||||
|
||||
187
backups.go
@@ -5,40 +5,134 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
const (
|
||||
BACKUP_PREFIX = "jfa-go-db-"
|
||||
BACKUP_PREFIX = "jfa-go-db"
|
||||
BACKUP_COMMIT_PREFIX = "-c-"
|
||||
BACKUP_DATE_PREFIX = "-d-"
|
||||
BACKUP_UPLOAD_PREFIX = "upload-"
|
||||
BACKUP_DATEFMT = "2006-01-02T15-04-05"
|
||||
BACKUP_SUFFIX = ".bak"
|
||||
)
|
||||
|
||||
type Backup struct {
|
||||
Date time.Time
|
||||
Commit string
|
||||
Upload bool
|
||||
}
|
||||
|
||||
func (b Backup) IsZero() bool { return b.Date.IsZero() && b.Commit == "" && b.Upload == false }
|
||||
|
||||
func (b Backup) Equals(a Backup) bool {
|
||||
return a.Date.Equal(b.Date) && a.Commit == b.Commit && a.Upload == b.Upload
|
||||
}
|
||||
|
||||
// Pre 21/03/25 format: "{BACKUP_PREFIX}{date in BACKUP_DATEFMT}{BACKUP_SUFFIX}" = "jfa-go-db-2006-01-02T15-04-05.bak"
|
||||
// Post 21/03/25 format: "{BACKUP_PREFIX}-c-{commit}-d-{date in BACKUP_DATEFMT}{BACKUP_SUFFIX}" = "jfa-go-db-c-0b92060-d-2006-01-02T15-04-05.bak"
|
||||
|
||||
func (b Backup) String() string {
|
||||
t := b.Date
|
||||
if t.IsZero() {
|
||||
t = time.Now()
|
||||
}
|
||||
out := BACKUP_PREFIX
|
||||
if b.Upload {
|
||||
out = BACKUP_UPLOAD_PREFIX + out
|
||||
}
|
||||
if b.Commit != "" {
|
||||
out += BACKUP_COMMIT_PREFIX + b.Commit
|
||||
}
|
||||
out += BACKUP_DATE_PREFIX + t.Local().Format(BACKUP_DATEFMT) + BACKUP_SUFFIX
|
||||
return out
|
||||
}
|
||||
|
||||
func (b *Backup) FromString(f string) error {
|
||||
of := f
|
||||
if strings.HasPrefix(f, BACKUP_UPLOAD_PREFIX) {
|
||||
b.Upload = true
|
||||
f = f[len(BACKUP_UPLOAD_PREFIX):]
|
||||
}
|
||||
if !strings.HasPrefix(f, BACKUP_PREFIX) {
|
||||
return fmt.Errorf("file doesn't have correct prefix (\"%s\")", BACKUP_PREFIX)
|
||||
}
|
||||
f = f[len(BACKUP_PREFIX):]
|
||||
if !strings.HasSuffix(f, BACKUP_SUFFIX) {
|
||||
return fmt.Errorf("file doesn't have correct suffix (\"%s\")", BACKUP_SUFFIX)
|
||||
}
|
||||
for range 2 {
|
||||
if strings.HasPrefix(f, BACKUP_COMMIT_PREFIX) {
|
||||
f = f[len(BACKUP_COMMIT_PREFIX):]
|
||||
commitEnd := strings.Index(f, BACKUP_DATE_PREFIX)
|
||||
if commitEnd == -1 {
|
||||
commitEnd = strings.Index(f, BACKUP_SUFFIX)
|
||||
}
|
||||
if commitEnd == -1 {
|
||||
return fmt.Errorf("end of commit (\"%s\" or \"%s\") not found in \"%s\"", BACKUP_DATE_PREFIX, BACKUP_PREFIX, f)
|
||||
}
|
||||
b.Commit = f[:commitEnd]
|
||||
f = f[commitEnd:]
|
||||
} else if strings.HasPrefix(f, BACKUP_DATE_PREFIX) {
|
||||
f = f[len(BACKUP_DATE_PREFIX):]
|
||||
dateEnd := strings.Index(f, BACKUP_COMMIT_PREFIX)
|
||||
if dateEnd == -1 {
|
||||
dateEnd = strings.Index(f, BACKUP_SUFFIX)
|
||||
}
|
||||
if dateEnd == -1 {
|
||||
return fmt.Errorf("end of date (\"%s\" or \"%s\") not found in \"%s\"", BACKUP_COMMIT_PREFIX, BACKUP_PREFIX, f)
|
||||
}
|
||||
t, err := time.Parse(BACKUP_DATEFMT, f[:dateEnd])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Date = t
|
||||
f = f[dateEnd:]
|
||||
}
|
||||
}
|
||||
if b.Date.IsZero() {
|
||||
return b.FromOldString(of)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Backup) FromOldString(f string) error {
|
||||
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(f, BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX+"-"), BACKUP_SUFFIX))
|
||||
if err != nil {
|
||||
return fmt.Errorf(lm.FailedParseTime, err)
|
||||
}
|
||||
b.Date = t
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
type BackupList struct {
|
||||
files []os.DirEntry
|
||||
dates []time.Time
|
||||
info []Backup
|
||||
count int
|
||||
}
|
||||
|
||||
func (bl BackupList) Len() int { return len(bl.files) }
|
||||
func (bl BackupList) Swap(i, j int) {
|
||||
bl.files[i], bl.files[j] = bl.files[j], bl.files[i]
|
||||
bl.dates[i], bl.dates[j] = bl.dates[j], bl.dates[i]
|
||||
bl.info[i], bl.info[j] = bl.info[j], bl.info[i]
|
||||
}
|
||||
|
||||
func (bl BackupList) Less(i, j int) bool {
|
||||
// Push non-backup files to the end of the array,
|
||||
// Since they didn't have a date parsed.
|
||||
if bl.dates[i].IsZero() {
|
||||
if bl.info[i].Date.IsZero() {
|
||||
return false
|
||||
}
|
||||
if bl.dates[j].IsZero() {
|
||||
if bl.info[j].Date.IsZero() {
|
||||
return true
|
||||
}
|
||||
// Sort by oldest first
|
||||
return bl.dates[j].After(bl.dates[i])
|
||||
return bl.info[j].Date.After(bl.info[i].Date)
|
||||
}
|
||||
|
||||
// Get human-readable file size from f.Size() result.
|
||||
@@ -60,28 +154,29 @@ func (app *appContext) getBackups() *BackupList {
|
||||
path := app.config.Section("backups").Key("path").String()
|
||||
err := os.MkdirAll(path, 0755)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to create backup directory \"%s\": %v\n", path, err)
|
||||
app.err.Printf(lm.FailedCreateDir, path, err)
|
||||
return nil
|
||||
}
|
||||
items, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to read backup directory \"%s\": %v\n", path, err)
|
||||
app.err.Printf(lm.FailedReading, path, err)
|
||||
return nil
|
||||
}
|
||||
backups := &BackupList{}
|
||||
backups.files = items
|
||||
backups.dates = make([]time.Time, len(items))
|
||||
backups.info = make([]Backup, len(items))
|
||||
backups.count = 0
|
||||
for i, item := range items {
|
||||
// Even though Backup{} can parse and check validity, still check if the file ends in .bak, we don't need to print an error if a file isn't a .bak.
|
||||
if item.IsDir() || !(strings.HasSuffix(item.Name(), BACKUP_SUFFIX)) {
|
||||
continue
|
||||
}
|
||||
t, err := time.Parse(BACKUP_DATEFMT, strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(item.Name(), BACKUP_UPLOAD_PREFIX), BACKUP_PREFIX), BACKUP_SUFFIX))
|
||||
if err != nil {
|
||||
app.debug.Printf("Failed to parse backup filename \"%s\": %v\n", item.Name(), err)
|
||||
b := Backup{}
|
||||
if err := b.FromString(item.Name()); err != nil {
|
||||
app.debug.Printf(lm.FailedParseBackup, item.Name(), err)
|
||||
continue
|
||||
}
|
||||
backups.dates[i] = t
|
||||
backups.info[i] = b
|
||||
backups.count++
|
||||
}
|
||||
return backups
|
||||
@@ -89,48 +184,82 @@ func (app *appContext) getBackups() *BackupList {
|
||||
|
||||
func (app *appContext) makeBackup() (fileDetails CreateBackupDTO) {
|
||||
toKeep := app.config.Section("backups").Key("keep_n_backups").MustInt(20)
|
||||
fname := BACKUP_PREFIX + time.Now().Local().Format(BACKUP_DATEFMT) + BACKUP_SUFFIX
|
||||
keepPreviousVersions := app.config.Section("backups").Key("keep_previous_version_backup").MustBool(true)
|
||||
|
||||
b := Backup{Commit: commit}
|
||||
fname := b.String()
|
||||
path := app.config.Section("backups").Key("path").String()
|
||||
backups := app.getBackups()
|
||||
if backups == nil {
|
||||
return
|
||||
}
|
||||
toDelete := backups.count + 1 - toKeep
|
||||
if toDelete > 0 || keepPreviousVersions {
|
||||
sort.Sort(backups)
|
||||
}
|
||||
backupsByCommit := map[string]int{}
|
||||
if keepPreviousVersions {
|
||||
// Count backups by commit
|
||||
for _, b := range backups.info {
|
||||
if b.IsZero() {
|
||||
continue
|
||||
}
|
||||
// If b.Commit is empty, the backup is pre-versions-in-backup-names.
|
||||
// Still use the empty string as a key, considering these as a single version.
|
||||
count, ok := backupsByCommit[b.Commit]
|
||||
if !ok {
|
||||
count = 0
|
||||
}
|
||||
count += 1
|
||||
backupsByCommit[b.Commit] = count
|
||||
}
|
||||
fmt.Printf("remaining:%+v\n", backupsByCommit)
|
||||
}
|
||||
// fmt.Printf("toDelete: %d, backCount: %d, keep: %d, length: %d\n", toDelete, backups.count, toKeep, len(backups.files))
|
||||
if toDelete > 0 && toDelete <= backups.count {
|
||||
sort.Sort(backups)
|
||||
for _, item := range backups.files[:toDelete] {
|
||||
for i := range toDelete {
|
||||
backupsRemaining, ok := backupsByCommit[backups.info[i].Commit]
|
||||
app.debug.Println("item", backups.files[i], "remaining", backupsRemaining)
|
||||
if keepPreviousVersions && ok && backupsRemaining <= 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
item := backups.files[i]
|
||||
fullpath := filepath.Join(path, item.Name())
|
||||
app.debug.Printf("Deleting old backup \"%s\"\n", item.Name())
|
||||
err := os.Remove(fullpath)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to delete old backup \"%s\": %v\n", fullpath, err)
|
||||
app.err.Printf(lm.FailedDeleteOldBackup, fullpath, err)
|
||||
return
|
||||
}
|
||||
app.debug.Printf(lm.DeleteOldBackup, fullpath)
|
||||
if keepPreviousVersions && ok {
|
||||
backupsRemaining -= 1
|
||||
backupsByCommit[backups.info[i].Commit] = backupsRemaining
|
||||
}
|
||||
}
|
||||
}
|
||||
fullpath := filepath.Join(path, fname)
|
||||
f, err := os.Create(fullpath)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to open backup file \"%s\": %v\n", fullpath, err)
|
||||
app.err.Printf(lm.FailedOpen, fullpath, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = app.storage.db.Badger().Backup(f, 0)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to create backup: %v\n", err)
|
||||
app.err.Printf(lm.FailedCreateBackup, err)
|
||||
return
|
||||
}
|
||||
|
||||
fstat, err := f.Stat()
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to get info on new backup: %v\n", err)
|
||||
app.err.Printf(lm.FailedStat, fullpath, err)
|
||||
return
|
||||
}
|
||||
fileDetails.Size = fileSize(fstat.Size())
|
||||
fileDetails.Name = fname
|
||||
fileDetails.Path = fullpath
|
||||
// fmt.Printf("Created backup %+v\n", fileDetails)
|
||||
app.debug.Printf(lm.CreateBackup, fileDetails)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -138,26 +267,26 @@ func (app *appContext) loadPendingBackup() {
|
||||
if LOADBAK == "" {
|
||||
return
|
||||
}
|
||||
oldPath := filepath.Join(app.dataPath, "db-"+string(time.Now().Unix())+"-pre-"+filepath.Base(LOADBAK))
|
||||
app.info.Printf("Moving existing database to \"%s\"\n", oldPath)
|
||||
oldPath := filepath.Join(app.dataPath, "db-"+strconv.FormatInt(time.Now().Unix(), 10)+"-pre-"+filepath.Base(LOADBAK))
|
||||
err := os.Rename(app.storage.db_path, oldPath)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to move existing database: %v\n", err)
|
||||
app.err.Fatalf(lm.FailedMoveOldDB, oldPath, err)
|
||||
}
|
||||
app.info.Printf(lm.MoveOldDB, oldPath)
|
||||
|
||||
app.ConnectDB()
|
||||
defer app.storage.db.Close()
|
||||
|
||||
f, err := os.Open(LOADBAK)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to open backup file \"%s\": %v\n", LOADBAK, err)
|
||||
app.err.Fatalf(lm.FailedOpen, LOADBAK, err)
|
||||
}
|
||||
err = app.storage.db.Badger().Load(f, 256)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to restore backup file \"%s\": %v\n", LOADBAK, err)
|
||||
app.err.Fatalf(lm.FailedRestoreDB, LOADBAK, err)
|
||||
}
|
||||
app.info.Printf("Restored backup \"%s\".", LOADBAK)
|
||||
app.info.Printf(lm.RestoreDB, LOADBAK)
|
||||
LOADBAK = ""
|
||||
}
|
||||
|
||||
@@ -165,9 +294,9 @@ func newBackupDaemon(app *appContext) *GenericDaemon {
|
||||
interval := time.Duration(app.config.Section("backups").Key("every_n_minutes").MustInt(1440)) * time.Minute
|
||||
d := NewGenericDaemon(interval, app,
|
||||
func(app *appContext) {
|
||||
app.debug.Println("Backups: Creating backup")
|
||||
app.makeBackup()
|
||||
},
|
||||
)
|
||||
d.Name("Backup")
|
||||
return d
|
||||
}
|
||||
|
||||
57
backups_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testBackupParse(f string, a Backup, t *testing.T) {
|
||||
b := Backup{}
|
||||
err := b.FromString(f)
|
||||
if err != nil {
|
||||
t.Fatalf("error: %+v", err)
|
||||
}
|
||||
if !b.Equals(a) {
|
||||
t.Fatalf("not equal: %+v != %+v", b, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupParserOld(t *testing.T) {
|
||||
Q1 := BACKUP_PREFIX + "2023-12-21T21-08-00" + BACKUP_SUFFIX
|
||||
A1 := Backup{}
|
||||
A1.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00")
|
||||
testBackupParse(Q1, A1, t)
|
||||
}
|
||||
func TestBackupParserOldUpload(t *testing.T) {
|
||||
Q2 := BACKUP_UPLOAD_PREFIX + BACKUP_PREFIX + "2023-12-21T21-08-00" + BACKUP_SUFFIX
|
||||
A2 := Backup{
|
||||
Upload: true,
|
||||
}
|
||||
A2.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00")
|
||||
testBackupParse(Q2, A2, t)
|
||||
}
|
||||
func TestBackupParserUploadDate(t *testing.T) {
|
||||
Q3 := BACKUP_UPLOAD_PREFIX + BACKUP_PREFIX + BACKUP_DATE_PREFIX + "2023-12-21T21-08-00" + BACKUP_SUFFIX
|
||||
A3 := Backup{
|
||||
Upload: true,
|
||||
}
|
||||
A3.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00")
|
||||
testBackupParse(Q3, A3, t)
|
||||
}
|
||||
func TestBackupParserUploadCommitDate(t *testing.T) {
|
||||
Q4 := BACKUP_UPLOAD_PREFIX + BACKUP_PREFIX + BACKUP_COMMIT_PREFIX + "testcommit" + BACKUP_DATE_PREFIX + "2023-12-21T21-08-00" + BACKUP_SUFFIX
|
||||
A4 := Backup{
|
||||
Commit: "testcommit",
|
||||
Upload: true,
|
||||
}
|
||||
A4.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00")
|
||||
testBackupParse(Q4, A4, t)
|
||||
}
|
||||
func TestBackupParserDateCommit(t *testing.T) {
|
||||
Q5 := BACKUP_PREFIX + BACKUP_DATE_PREFIX + "2023-12-21T21-08-00" + BACKUP_COMMIT_PREFIX + "testcommit" + BACKUP_SUFFIX
|
||||
A5 := Backup{
|
||||
Commit: "testcommit",
|
||||
}
|
||||
A5.Date, _ = time.Parse(BACKUP_DATEFMT, "2023-12-21T21-08-00")
|
||||
testBackupParse(Q5, A5, t)
|
||||
}
|
||||
136
common/common.go
@@ -1,8 +1,18 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
// TimeoutHandler recovers from an http timeout or panic.
|
||||
@@ -12,7 +22,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(lm.FailedAuth, name, addr, 0, lm.TimedOut)
|
||||
if noFail {
|
||||
log.Print(out)
|
||||
} else {
|
||||
@@ -21,3 +31,127 @@ func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// most 404 errors are from UserNotFound, so this generic error doesn't really need any detail.
|
||||
type ErrNotFound error
|
||||
|
||||
type ErrUnauthorized struct{}
|
||||
|
||||
func (err ErrUnauthorized) Error() string {
|
||||
return lm.Unauthorized
|
||||
}
|
||||
|
||||
type ErrForbidden struct{}
|
||||
|
||||
func (err ErrForbidden) Error() string {
|
||||
return lm.Forbidden
|
||||
}
|
||||
|
||||
var (
|
||||
NotFound ErrNotFound = errors.New(lm.NotFound)
|
||||
)
|
||||
|
||||
type ErrUnknown struct {
|
||||
code int
|
||||
}
|
||||
|
||||
func (err ErrUnknown) Error() string {
|
||||
msg := fmt.Sprintf(lm.FailedGenericWithCode, err.code)
|
||||
return msg
|
||||
}
|
||||
|
||||
// GenericErr returns an error appropriate to the given HTTP status (or actual error, if given).
|
||||
func GenericErr(status int, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch status {
|
||||
case 200, 204, 201:
|
||||
return nil
|
||||
case 401, 400:
|
||||
return ErrUnauthorized{}
|
||||
case 404:
|
||||
return NotFound
|
||||
case 403:
|
||||
return ErrForbidden{}
|
||||
default:
|
||||
return ErrUnknown{code: status}
|
||||
}
|
||||
}
|
||||
|
||||
func GenericErrFromResponse(resp *http.Response, err error) error {
|
||||
if resp == nil {
|
||||
return ErrUnknown{code: -2}
|
||||
}
|
||||
return GenericErr(resp.StatusCode, err)
|
||||
}
|
||||
|
||||
type ConfigurableTransport interface {
|
||||
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
||||
SetTransport(t *http.Transport)
|
||||
}
|
||||
|
||||
// Stripped down-ish version of rough http request function used in most of the API clients.
|
||||
func Req(httpClient *http.Client, timeoutHandler TimeoutHandler, mode string, uri string, data any, queryParams url.Values, headers map[string]string, response bool) (string, int, error) {
|
||||
var params []byte
|
||||
if data != nil {
|
||||
params, _ = json.Marshal(data)
|
||||
}
|
||||
if qp := queryParams.Encode(); qp != "" {
|
||||
uri += "?" + qp
|
||||
}
|
||||
var req *http.Request
|
||||
if data != nil {
|
||||
req, _ = http.NewRequest(mode, uri, bytes.NewBuffer(params))
|
||||
} else {
|
||||
req, _ = http.NewRequest(mode, uri, nil)
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
for name, value := range headers {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if resp == nil {
|
||||
return "", 0, err
|
||||
}
|
||||
err = GenericErr(resp.StatusCode, err)
|
||||
if timeoutHandler != nil {
|
||||
defer timeoutHandler()
|
||||
}
|
||||
var responseText string
|
||||
defer resp.Body.Close()
|
||||
if response || err != nil {
|
||||
responseText, err = decodeResp(resp)
|
||||
if err != nil {
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
var msg any
|
||||
err = json.Unmarshal([]byte(responseText), &msg)
|
||||
if err != nil {
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
if msg != nil {
|
||||
err = fmt.Errorf("got %d: %+v", resp.StatusCode, msg)
|
||||
}
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
|
||||
func decodeResp(resp *http.Response) (string, error) {
|
||||
var out io.Reader
|
||||
switch resp.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
out, _ = gzip.NewReader(resp.Body)
|
||||
default:
|
||||
out = resp.Body
|
||||
}
|
||||
buf := new(strings.Builder)
|
||||
_, err := io.Copy(buf, out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
62
common/config.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package common
|
||||
|
||||
type SectionMeta struct {
|
||||
Name string `json:"name" yaml:"name" example:"My Section"` // friendly name of the section
|
||||
Description string `json:"description" yaml:"description"`
|
||||
Advanced bool `json:"advanced,omitempty" yaml:"advanced,omitempty"`
|
||||
Disabled bool `json:"disabled,omitempty" yaml:"disabled,omitempty"`
|
||||
DependsTrue string `json:"depends_true,omitempty" yaml:"depends_true,omitempty"`
|
||||
DependsFalse string `json:"depends_false,omitempty" yaml:"depends_false,omitempty"`
|
||||
WikiLink string `json:"wiki_link,omitempty" yaml:"wiki_link,omitempty"`
|
||||
}
|
||||
|
||||
type Option [2]string
|
||||
|
||||
type SettingType string
|
||||
|
||||
var (
|
||||
BoolType SettingType = "bool"
|
||||
SelectType SettingType = "select"
|
||||
TextType SettingType = "text"
|
||||
PasswordType SettingType = "password"
|
||||
NumberType SettingType = "number"
|
||||
NoteType SettingType = "note"
|
||||
EmailType SettingType = "email"
|
||||
ListType SettingType = "list"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
Setting string `json:"setting" yaml:"setting" example:"my_setting"`
|
||||
Name string `json:"name" yaml:"name" example:"My Setting"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
Required bool `json:"required" yaml:"required"`
|
||||
RequiresRestart bool `json:"requires_restart" yaml:"requires_restart"`
|
||||
Advanced bool `json:"advanced,omitempty" yaml:"advanced,omitempty"`
|
||||
Type SettingType `json:"type" yaml:"type"` // Type (string, number, bool, etc.)
|
||||
Value any `json:"value" yaml:"value"`
|
||||
Options []Option `json:"options,omitempty" yaml:"options,omitempty"`
|
||||
DependsTrue string `json:"depends_true,omitempty" yaml:"depends_true,omitempty"` // If specified, this field is enabled when the specified bool setting is enabled.
|
||||
DependsFalse string `json:"depends_false,omitempty" yaml:"depends_false,omitempty"` // If specified, opposite behaviour of DependsTrue.
|
||||
Style string `json:"style,omitempty" yaml:"style,omitempty"`
|
||||
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
|
||||
WikiLink string `json:"wiki_link,omitempty" yaml:"wiki_link,omitempty"`
|
||||
}
|
||||
|
||||
type Section struct {
|
||||
Section string `json:"section" yaml:"section" example:"my_section"`
|
||||
Meta SectionMeta `json:"meta" yaml:"meta"`
|
||||
Settings []Setting `json:"settings" yaml:"settings"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Sections []Section `json:"sections" yaml:"sections"`
|
||||
}
|
||||
|
||||
func (c *Config) removeSection(section string) {
|
||||
for i, v := range c.Sections {
|
||||
if v.Section == section {
|
||||
c.Sections = append(c.Sections[:i], c.Sections[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
module github.com/hrfee/jfa-go/common
|
||||
|
||||
go 1.15
|
||||
replace github.com/hrfee/jfa-go/logmessages => ../logmessages
|
||||
|
||||
go 1.18
|
||||
|
||||
require github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a
|
||||
|
||||
177
config.go
@@ -3,12 +3,16 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
"github.com/hrfee/jfa-go/easyproxy"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
@@ -18,6 +22,9 @@ var telegramEnabled = false
|
||||
var discordEnabled = false
|
||||
var matrixEnabled = false
|
||||
|
||||
// URL subpaths. Ignore the "Current" field.
|
||||
var PAGES = PagePaths{}
|
||||
|
||||
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
||||
val := app.config.Section(sect).Key(key).MustString("")
|
||||
if strings.HasPrefix(val, "jfa-go:") {
|
||||
@@ -31,15 +38,44 @@ func (app *appContext) MustSetValue(section, key, val string) {
|
||||
app.config.Section(section).Key(key).SetValue(app.config.Section(section).Key(key).MustString(val))
|
||||
}
|
||||
|
||||
func (app *appContext) MustSetURLPath(section, key, val string) {
|
||||
if !strings.HasPrefix(val, "/") && val != "" {
|
||||
val = "/" + val
|
||||
}
|
||||
app.MustSetValue(section, key, val)
|
||||
}
|
||||
|
||||
func FormatSubpath(path string) string {
|
||||
if path == "/" {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSuffix(path, "/")
|
||||
}
|
||||
|
||||
func (app *appContext) loadConfig() error {
|
||||
var err error
|
||||
app.config, err = ini.Load(app.configPath)
|
||||
app.config, err = ini.ShadowLoad(app.configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// URLs
|
||||
app.MustSetURLPath("ui", "url_base", "")
|
||||
app.MustSetURLPath("url_paths", "admin", "")
|
||||
app.MustSetURLPath("url_paths", "user_page", "/my/account")
|
||||
app.MustSetURLPath("url_paths", "form", "/invite")
|
||||
PAGES.Base = FormatSubpath(app.config.Section("ui").Key("url_base").String())
|
||||
PAGES.Admin = FormatSubpath(app.config.Section("url_paths").Key("admin").String())
|
||||
PAGES.MyAccount = FormatSubpath(app.config.Section("url_paths").Key("user_page").String())
|
||||
PAGES.Form = FormatSubpath(app.config.Section("url_paths").Key("form").String())
|
||||
if !(app.config.Section("user_page").Key("enabled").MustBool(true)) {
|
||||
PAGES.MyAccount = "disabled"
|
||||
}
|
||||
if PAGES.Base == PAGES.Form || PAGES.Base == "/accounts" || PAGES.Base == "/settings" || PAGES.Base == "/activity" {
|
||||
app.err.Printf(lm.BadURLBase, PAGES.Base)
|
||||
}
|
||||
app.info.Printf(lm.SubpathBlockMessage, PAGES.Base, PAGES.Admin, PAGES.MyAccount, PAGES.Form)
|
||||
app.MustSetValue("jellyfin", "public_server", app.config.Section("jellyfin").Key("server").String())
|
||||
|
||||
app.MustSetValue("ui", "redirect_url", app.config.Section("jellyfin").Key("public_server").String())
|
||||
|
||||
for _, key := range app.config.Section("files").Keys() {
|
||||
@@ -53,7 +89,19 @@ func (app *appContext) loadConfig() error {
|
||||
for _, key := range []string{"matrix_sql"} {
|
||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".db"))))
|
||||
}
|
||||
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
|
||||
|
||||
app.ExternalURI = strings.TrimSuffix(strings.TrimSuffix(app.config.Section("ui").Key("jfa_url").MustString(""), "/invite"), "/")
|
||||
if !strings.HasSuffix(app.ExternalURI, PAGES.Base) {
|
||||
app.err.Println(lm.NoURLSuffix)
|
||||
}
|
||||
if app.ExternalURI == "" {
|
||||
app.err.Println(lm.NoExternalHost + lm.LoginWontSave)
|
||||
}
|
||||
u, err := url.Parse(app.ExternalURI)
|
||||
if err == nil {
|
||||
app.ExternalDomain = u.Hostname()
|
||||
}
|
||||
|
||||
app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false)))
|
||||
|
||||
app.MustSetValue("password_resets", "email_html", "jfa-go:"+"email.html")
|
||||
@@ -77,6 +125,7 @@ func (app *appContext) loadConfig() error {
|
||||
app.MustSetValue("smtp", "hello_hostname", "localhost")
|
||||
app.MustSetValue("smtp", "cert_validation", "true")
|
||||
app.MustSetValue("smtp", "auth_type", "4")
|
||||
app.MustSetValue("smtp", "port", "465")
|
||||
|
||||
app.MustSetValue("activity_log", "keep_n_records", "1000")
|
||||
app.MustSetValue("activity_log", "delete_after_days", "90")
|
||||
@@ -108,6 +157,8 @@ func (app *appContext) loadConfig() error {
|
||||
app.MustSetValue("user_expiry", "adjustment_email_html", "jfa-go:"+"expiry-adjusted.html")
|
||||
app.MustSetValue("user_expiry", "adjustment_email_text", "jfa-go:"+"expiry-adjusted.txt")
|
||||
|
||||
app.MustSetValue("email", "collect", "true")
|
||||
|
||||
app.MustSetValue("matrix", "topic", "Jellyfin notifications")
|
||||
app.MustSetValue("matrix", "show_on_reg", "true")
|
||||
|
||||
@@ -118,6 +169,7 @@ func (app *appContext) loadConfig() error {
|
||||
app.MustSetValue("backups", "every_n_minutes", "1440")
|
||||
app.MustSetValue("backups", "path", filepath.Join(app.dataPath, "backups"))
|
||||
app.MustSetValue("backups", "keep_n_backups", "20")
|
||||
app.MustSetValue("backups", "keep_previous_version_backup", "true")
|
||||
|
||||
app.config.Section("jellyfin").Key("version").SetValue(version)
|
||||
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||
@@ -126,11 +178,13 @@ func (app *appContext) loadConfig() error {
|
||||
LOGIP = app.config.Section("advanced").Key("log_ips").MustBool(false)
|
||||
LOGIPU = app.config.Section("advanced").Key("log_ips_users").MustBool(false)
|
||||
|
||||
// These two settings are pretty much the same
|
||||
url1 := app.config.Section("invite_emails").Key("url_base").String()
|
||||
url2 := app.config.Section("password_resets").Key("url_base").String()
|
||||
app.MustSetValue("password_resets", "url_base", strings.TrimSuffix(url1, "/invite"))
|
||||
app.MustSetValue("invite_emails", "url_base", url2)
|
||||
app.MustSetValue("advanced", "auth_retry_count", "6")
|
||||
app.MustSetValue("advanced", "auth_retry_gap", "10")
|
||||
|
||||
app.MustSetValue("ui", "port", "8056")
|
||||
app.MustSetValue("advanced", "tls_port", "8057")
|
||||
|
||||
app.MustSetValue("advanced", "value_log_size", "512")
|
||||
|
||||
pwrMethods := []string{"allow_pwr_username", "allow_pwr_email", "allow_pwr_contact_method"}
|
||||
allDisabled := true
|
||||
@@ -140,7 +194,7 @@ func (app *appContext) loadConfig() error {
|
||||
}
|
||||
}
|
||||
if allDisabled {
|
||||
fmt.Println("SETALLTRUE")
|
||||
app.info.Println(lm.EnableAllPWRMethods)
|
||||
for _, v := range pwrMethods {
|
||||
app.config.Section("user_page").Key(v).SetValue("true")
|
||||
}
|
||||
@@ -175,9 +229,15 @@ func (app *appContext) loadConfig() error {
|
||||
app.proxyConfig.Password = app.config.Section("advanced").Key("proxy_password").MustString("")
|
||||
app.proxyTransport, err = easyproxy.NewTransport(app.proxyConfig)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to initialize Proxy: %v\n", err)
|
||||
app.err.Printf(lm.FailedInitProxy, app.proxyConfig.Addr, err)
|
||||
// As explained in lm.FailedInitProxy, sleep here might grab the admin's attention,
|
||||
// Since we don't crash on this failing.
|
||||
time.Sleep(15 * time.Second)
|
||||
app.proxyEnabled = false
|
||||
} else {
|
||||
app.proxyEnabled = true
|
||||
app.info.Printf(lm.InitProxy, app.proxyConfig.Addr)
|
||||
}
|
||||
app.proxyEnabled = true
|
||||
}
|
||||
|
||||
app.MustSetValue("updates", "enabled", "true")
|
||||
@@ -229,3 +289,98 @@ func (app *appContext) loadConfig() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *appContext) PatchConfigBase() {
|
||||
conf := app.configBase
|
||||
// Load language options
|
||||
formOptions := app.storage.lang.User.getOptions()
|
||||
pwrOptions := app.storage.lang.PasswordReset.getOptions()
|
||||
adminOptions := app.storage.lang.Admin.getOptions()
|
||||
emailOptions := app.storage.lang.Email.getOptions()
|
||||
telegramOptions := app.storage.lang.Email.getOptions()
|
||||
|
||||
for i, section := range app.configBase.Sections {
|
||||
if section.Section == "updates" && updater == "" {
|
||||
section.Meta.Disabled = true
|
||||
}
|
||||
for j, setting := range section.Settings {
|
||||
if section.Section == "ui" {
|
||||
if setting.Setting == "language-form" {
|
||||
setting.Options = formOptions
|
||||
setting.Value = "en-us"
|
||||
} else if setting.Setting == "language-admin" {
|
||||
setting.Options = adminOptions
|
||||
setting.Value = "en-us"
|
||||
}
|
||||
} else if section.Section == "password_resets" {
|
||||
if setting.Setting == "language" {
|
||||
setting.Options = pwrOptions
|
||||
setting.Value = "en-us"
|
||||
}
|
||||
} else if section.Section == "email" {
|
||||
if setting.Setting == "language" {
|
||||
setting.Options = emailOptions
|
||||
setting.Value = "en-us"
|
||||
}
|
||||
} else if section.Section == "telegram" {
|
||||
if setting.Setting == "language" {
|
||||
setting.Options = telegramOptions
|
||||
setting.Value = "en-us"
|
||||
}
|
||||
} else if section.Section == "smtp" {
|
||||
if setting.Setting == "ssl_cert" && PLATFORM == "windows" {
|
||||
// Not accurate but the effect is hiding the option, which we want.
|
||||
setting.Deprecated = true
|
||||
}
|
||||
} else if section.Section == "matrix" {
|
||||
if setting.Setting == "encryption" && !MatrixE2EE() {
|
||||
// Not accurate but the effect is hiding the option, which we want.
|
||||
setting.Deprecated = true
|
||||
}
|
||||
}
|
||||
val := app.config.Section(section.Section).Key(setting.Setting)
|
||||
switch setting.Type {
|
||||
case "list":
|
||||
setting.Value = val.StringsWithShadows("|")
|
||||
case "text", "email", "select", "password", "note":
|
||||
setting.Value = val.MustString("")
|
||||
case "number":
|
||||
setting.Value = val.MustInt(0)
|
||||
case "bool":
|
||||
setting.Value = val.MustBool(false)
|
||||
}
|
||||
section.Settings[j] = setting
|
||||
}
|
||||
conf.Sections[i] = section
|
||||
}
|
||||
app.patchedConfig = conf
|
||||
}
|
||||
|
||||
func (app *appContext) PatchConfigDiscordRoles() {
|
||||
if !discordEnabled {
|
||||
return
|
||||
}
|
||||
r, err := app.discord.ListRoles()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
roles := make([]common.Option, len(r)+1)
|
||||
roles[0] = common.Option{"", "None"}
|
||||
for i, role := range r {
|
||||
roles[i+1] = role
|
||||
}
|
||||
|
||||
for i, section := range app.patchedConfig.Sections {
|
||||
if section.Section != "discord" {
|
||||
continue
|
||||
}
|
||||
for j, setting := range section.Settings {
|
||||
if setting.Setting != "apply_role" {
|
||||
continue
|
||||
}
|
||||
setting.Options = roles
|
||||
section.Settings[j] = setting
|
||||
}
|
||||
app.patchedConfig.Sections[i] = section
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
### fixconfig
|
||||
|
||||
Python's `json` library retains the order of data in a JSON file, which meant settings sent to the web page would be in the right order. Go's `encoding/json` and maps do not retain order, so `enumerate/enumerate_config.py` opens the json file, and for each section, adds an "order" array which tells the web page in which order to display settings.
|
||||
Specify the input and output files with `-i` and `-o` respectively.
|
||||
1
config/README.txt
Normal file
@@ -0,0 +1 @@
|
||||
The two python scripts here, `config-json-to-new-yaml.py` and `gen-rough-schema.py` were used to convert the old format, which was stored in a JSON file, to the new format in YAML. The latter script is used to get the possible values for settings and sections, so they could be properly defined in common/config.go.
|
||||
1636
config/config-base.yaml
Normal file
35
config/config-json-to-new-yaml.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from ruamel.yaml import YAML
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
yaml = YAML()
|
||||
|
||||
# c = yaml.load(Path(sys.argv[len(sys.argv)-1]))
|
||||
with open(sys.argv[len(sys.argv)-1], 'r') as f:
|
||||
c = json.load(f)
|
||||
|
||||
c.pop("order")
|
||||
|
||||
c1 = c.copy()
|
||||
c1["sections"] = []
|
||||
for section in c["sections"]:
|
||||
codeSection = { "section": section }
|
||||
s = codeSection | c["sections"][section]
|
||||
s.pop("order")
|
||||
c1["sections"].append(s)
|
||||
|
||||
c2 = c.copy()
|
||||
c2["sections"] = []
|
||||
|
||||
for section in c1["sections"]:
|
||||
sArray = []
|
||||
for setting in section["settings"]:
|
||||
codeSetting = { "setting": setting }
|
||||
s = codeSetting | section["settings"][setting]
|
||||
sArray.append(s)
|
||||
|
||||
section["settings"] = sArray
|
||||
c2["sections"].append(section)
|
||||
|
||||
|
||||
yaml.dump(c2, sys.stdout)
|
||||
40
config/gen-rough-schema.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
sectionSchema = {}
|
||||
metaSchema = {}
|
||||
settingSchema = {}
|
||||
typeValues = {}
|
||||
|
||||
# c = yaml.load(Path(sys.argv[len(sys.argv)-1]))
|
||||
with open(sys.argv[len(sys.argv)-1], 'r') as f:
|
||||
c = json.load(f)
|
||||
|
||||
for section in c["sections"]:
|
||||
for key in c["sections"][section]:
|
||||
sectionSchema[key] = True
|
||||
|
||||
for key in c["sections"][section]["meta"]:
|
||||
metaSchema[key] = c["sections"][section]["meta"][key]
|
||||
|
||||
for setting in c["sections"][section]["settings"]:
|
||||
for field in c["sections"][section]["settings"][setting]:
|
||||
settingSchema[field] = c["sections"][section]["settings"][setting][field]
|
||||
typeValues[c["sections"][section]["settings"][setting]["type"]] = True
|
||||
|
||||
print("Section Content:")
|
||||
for v in sectionSchema:
|
||||
print(v)
|
||||
print("---")
|
||||
print("Meta Schema")
|
||||
for v in metaSchema:
|
||||
print(v, "=", type(metaSchema[v]))
|
||||
print("---")
|
||||
print("Setting Schema")
|
||||
for v in settingSchema:
|
||||
print(v, "=", type(settingSchema[v]))
|
||||
print("---")
|
||||
print("Possible Types")
|
||||
for v in typeValues:
|
||||
print(v)
|
||||
|
||||
27
css/base.css
@@ -18,6 +18,8 @@
|
||||
|
||||
--bg-light: #fff;
|
||||
--bg-dark: #101010;
|
||||
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.light {
|
||||
@@ -26,6 +28,7 @@
|
||||
|
||||
.dark {
|
||||
--settings-section-button-filter: 80%;
|
||||
color-scheme: dark !important;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
@@ -62,18 +65,7 @@ html:not(.dark) .card.\@low:not(.\~neutral):not(.\~positive):not(.\~urge):not(.\
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
margin: 5% 20% 5% 20%;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.page-container {
|
||||
margin: 2%;
|
||||
margin-top: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
@media screen and (max-width: 1024px) {
|
||||
:root {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -467,3 +459,14 @@ input[type="checkbox" i], [class^="ri-"], [class*=" ri-"], .ri-refresh-line:befo
|
||||
margin-bottom: -0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
section.section:not(.\~neutral) {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.switch {
|
||||
@apply flex flex-row gap-1 items-center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
353
discord.go
@@ -2,29 +2,31 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dg "github.com/bwmarrin/discordgo"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
type DiscordDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
bot *dg.Session
|
||||
username string
|
||||
tokens map[string]VerifToken // Map of pins to tokens.
|
||||
verifiedTokens map[string]DiscordUser // Map of token pins 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.
|
||||
roleID string
|
||||
app *appContext
|
||||
commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string)
|
||||
commandIDs []string
|
||||
commandDescriptions []*dg.ApplicationCommand
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
bot *dg.Session
|
||||
username string
|
||||
tokens map[string]VerifToken // Map of pins to tokens.
|
||||
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
|
||||
Channel, InviteChannel struct{ ID, Name 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.
|
||||
roleID string
|
||||
app *appContext
|
||||
commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string)
|
||||
commandIDs []string
|
||||
commandDescriptions []*dg.ApplicationCommand
|
||||
}
|
||||
|
||||
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
||||
@@ -59,6 +61,11 @@ func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
||||
return dd, nil
|
||||
}
|
||||
|
||||
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
||||
func (d *DiscordDaemon) SetTransport(t *http.Transport) {
|
||||
d.bot.Client.Transport = t
|
||||
}
|
||||
|
||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||
func (d *DiscordDaemon) NewAuthToken() string {
|
||||
pin := genAuthToken()
|
||||
@@ -92,13 +99,11 @@ func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) run() {
|
||||
d.bot.AddHandler(d.messageHandler)
|
||||
|
||||
d.bot.AddHandler(d.commandHandler)
|
||||
|
||||
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)
|
||||
d.app.err.Printf(lm.FailedStartDaemon, lm.Discord, err)
|
||||
return
|
||||
}
|
||||
// Wait for everything to populate, it's slow sometimes.
|
||||
@@ -116,17 +121,17 @@ func (d *DiscordDaemon) run() {
|
||||
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.app.err.Printf(lm.FailedGetDiscordGuild, err)
|
||||
}
|
||||
d.serverChannelName = guild.Name
|
||||
d.serverName = guild.Name
|
||||
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
|
||||
d.channelName = channel
|
||||
d.Channel.Name = 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
|
||||
d.InviteChannel.Name = invChannel
|
||||
}
|
||||
}
|
||||
err = d.bot.UpdateGameStatus(0, "/"+d.app.config.Section("discord").Key("start_command").MustString("start"))
|
||||
@@ -145,7 +150,7 @@ func (d *DiscordDaemon) ListRoles() (roles [][2]string, err error) {
|
||||
var r []*dg.Role
|
||||
r, err = d.bot.GuildRoles(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get roles: %v", err)
|
||||
d.app.err.Printf(lm.FailedGetDiscordRoles, err)
|
||||
return
|
||||
}
|
||||
for _, role := range r {
|
||||
@@ -168,44 +173,62 @@ func (d *DiscordDaemon) ApplyRole(userID string) error {
|
||||
return d.bot.GuildMemberRoleAdd(d.guildID, userID, d.roleID)
|
||||
}
|
||||
|
||||
// RemoveRole removes the member role to the given user if set.
|
||||
func (d *DiscordDaemon) RemoveRole(userID string) error {
|
||||
if d.roleID == "" {
|
||||
return nil
|
||||
}
|
||||
return d.bot.GuildMemberRoleRemove(d.guildID, userID, d.roleID)
|
||||
}
|
||||
|
||||
// SetRoleDisabled removes the role if "disabled", and applies if "!disabled".
|
||||
func (d *DiscordDaemon) SetRoleDisabled(userID string, disabled bool) (err error) {
|
||||
if disabled {
|
||||
err = d.RemoveRole(userID)
|
||||
} else {
|
||||
err = d.ApplyRole(userID)
|
||||
}
|
||||
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.")
|
||||
if d.InviteChannel.Name == "" {
|
||||
d.app.err.Printf(lm.FailedCreateDiscordInviteChannel, lm.InviteChannelEmpty)
|
||||
return
|
||||
}
|
||||
if d.inviteChannelID == "" {
|
||||
if d.InviteChannel.ID == "" {
|
||||
channels, err := d.bot.GuildChannels(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Couldn't get channel list: %v", err)
|
||||
d.app.err.Printf(lm.FailedGetDiscordChannels, 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)
|
||||
// d.app.err.Printf(lm.FailedGetDiscordChannel, ch.ID, err)
|
||||
// return
|
||||
// }
|
||||
if channel.Name == d.inviteChannelName {
|
||||
d.inviteChannelID = channel.ID
|
||||
if channel.Name == d.InviteChannel.Name {
|
||||
d.InviteChannel.ID = channel.ID
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
d.app.err.Printf("Discord: Couldn't find invite channel \"%s\"", d.inviteChannelName)
|
||||
d.app.err.Printf(lm.FailedGetDiscordChannel, d.InviteChannel.Name, lm.NotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
// channel, err := d.bot.Channel(d.inviteChannelID)
|
||||
// if err != nil {
|
||||
// d.app.err.Printf("Discord: Couldn't get invite channel: %v", err)
|
||||
// d.app.err.Printf(lm.FailedGetDiscordChannel, d.inviteChannelID, err)
|
||||
// return
|
||||
// }
|
||||
inv, err = d.bot.ChannelInviteCreate(d.inviteChannelID, dg.Invite{
|
||||
inv, err = d.bot.ChannelInviteCreate(d.InviteChannel.ID, dg.Invite{
|
||||
// Guild: d.bot.State.Guilds[len(d.bot.State.Guilds)-1],
|
||||
// Channel: channel,
|
||||
// Inviter: d.bot.State.User,
|
||||
@@ -214,13 +237,13 @@ func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconU
|
||||
Temporary: false,
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create invite: %v", err)
|
||||
d.app.err.Printf(lm.FailedGenerateDiscordInvite, 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)
|
||||
d.app.err.Printf(lm.FailedGetDiscordGuild, err)
|
||||
return
|
||||
}
|
||||
iconURL = guild.IconURL("256")
|
||||
@@ -255,7 +278,7 @@ func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
|
||||
1000,
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get members: %v", err)
|
||||
d.app.err.Printf(lm.FailedGetDiscordGuildMembers, err)
|
||||
return nil
|
||||
}
|
||||
hasDiscriminator := strings.Contains(username, "#")
|
||||
@@ -285,7 +308,7 @@ func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
|
||||
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)
|
||||
d.app.err.Printf(lm.FailedGetUser, ID, lm.Discord, err)
|
||||
return
|
||||
}
|
||||
user.ID = ID
|
||||
@@ -294,7 +317,7 @@ func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
|
||||
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)
|
||||
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, ID, err)
|
||||
return
|
||||
}
|
||||
user.ChannelID = channel.ID
|
||||
@@ -381,7 +404,7 @@ func (d *DiscordDaemon) registerCommands() {
|
||||
d.commandDescriptions[1].Options[0].Choices = make([]*dg.ApplicationCommandOptionChoice, len(d.app.storage.lang.Telegram))
|
||||
i := 0
|
||||
for code := range d.app.storage.lang.Telegram {
|
||||
d.app.debug.Printf("Discord: registering lang choice \"%s\":\"%s\"\n", d.app.storage.lang.Telegram[code].Meta.Name, code)
|
||||
d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Lang, d.app.storage.lang.Telegram[code].Meta.Name+":"+code)
|
||||
d.commandDescriptions[1].Options[0].Choices[i] = &dg.ApplicationCommandOptionChoice{
|
||||
Name: d.app.storage.lang.Telegram[code].Meta.Name,
|
||||
Value: code,
|
||||
@@ -392,7 +415,7 @@ func (d *DiscordDaemon) registerCommands() {
|
||||
profiles := d.app.storage.GetProfiles()
|
||||
d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(profiles))
|
||||
for i, profile := range profiles {
|
||||
d.app.debug.Printf("Discord: registering profile choice \"%s\"", profile.Name)
|
||||
d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Profile, profile.Name)
|
||||
d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{
|
||||
Name: profile.Name,
|
||||
Value: profile.Name,
|
||||
@@ -409,9 +432,9 @@ func (d *DiscordDaemon) registerCommands() {
|
||||
for i, cmd := range d.commandDescriptions {
|
||||
command, err := d.bot.ApplicationCommandCreate(d.bot.State.User.ID, d.guildID, cmd)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Cannot create command \"%s\": %v", cmd.Name, err)
|
||||
d.app.err.Printf(lm.FailedRegisterDiscordCommand, cmd.Name, err)
|
||||
} else {
|
||||
d.app.debug.Printf("Discord: registered command \"%s\"", cmd.Name)
|
||||
d.app.debug.Printf(lm.RegisterDiscordCommand, cmd.Name)
|
||||
d.commandIDs[i] = command.ID
|
||||
}
|
||||
}
|
||||
@@ -420,12 +443,12 @@ func (d *DiscordDaemon) registerCommands() {
|
||||
func (d *DiscordDaemon) deregisterCommands() {
|
||||
existingCommands, err := d.bot.ApplicationCommands(d.bot.State.User.ID, d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get commands: %v", err)
|
||||
d.app.err.Printf(lm.FailedGetDiscordCommands, err)
|
||||
return
|
||||
}
|
||||
for _, cmd := range existingCommands {
|
||||
if err := d.bot.ApplicationCommandDelete(d.bot.State.User.ID, d.guildID, cmd.ID); err != nil {
|
||||
d.app.err.Printf("Discord: Failed to deregister command: %v", err)
|
||||
d.app.err.Printf(lm.FailedDeregDiscordCommand, cmd.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -436,7 +459,7 @@ func (d *DiscordDaemon) UpdateCommands() {
|
||||
profiles := d.app.storage.GetProfiles()
|
||||
d.commandDescriptions[3].Options[3].Choices = make([]*dg.ApplicationCommandOptionChoice, len(profiles))
|
||||
for i, profile := range profiles {
|
||||
d.app.debug.Printf("Discord: registering profile choice \"%s\"", profile.Name)
|
||||
d.app.debug.Printf(lm.RegisterDiscordChoice, lm.Profile, profile.Name)
|
||||
d.commandDescriptions[3].Options[3].Choices[i] = &dg.ApplicationCommandOptionChoice{
|
||||
Name: profile.Name,
|
||||
Value: profile.Name,
|
||||
@@ -444,7 +467,7 @@ func (d *DiscordDaemon) UpdateCommands() {
|
||||
}
|
||||
cmd, err := d.bot.ApplicationCommandEdit(d.bot.State.User.ID, d.guildID, d.commandIDs[3], d.commandDescriptions[3])
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to update profile list: %v\n", err)
|
||||
d.app.err.Printf(lm.FailedRegisterDiscordChoices, lm.Profile, err)
|
||||
} else {
|
||||
d.commandIDs[3] = cmd.ID
|
||||
}
|
||||
@@ -452,19 +475,20 @@ func (d *DiscordDaemon) UpdateCommands() {
|
||||
|
||||
func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
|
||||
if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok {
|
||||
if i.GuildID != "" && d.channelName != "" {
|
||||
if d.channelID == "" {
|
||||
if i.GuildID != "" && d.Channel.Name != "" {
|
||||
if d.Channel.ID == "" {
|
||||
channel, err := s.Channel(i.ChannelID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
|
||||
d.channelName = ""
|
||||
d.app.err.Printf(lm.FailedGetDiscordChannel, i.ChannelID, err)
|
||||
d.app.err.Println(lm.MonitorAllDiscordChannels)
|
||||
d.Channel.Name = ""
|
||||
}
|
||||
if channel.Name == d.channelName {
|
||||
d.channelID = channel.ID
|
||||
if channel.Name == d.Channel.Name {
|
||||
d.Channel.ID = channel.ID
|
||||
}
|
||||
}
|
||||
if d.channelID != i.ChannelID {
|
||||
d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
|
||||
if d.Channel.ID != i.ChannelID {
|
||||
d.app.debug.Printf(lm.IgnoreOutOfChannelMessage, lm.Discord)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -486,7 +510,7 @@ func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
|
||||
func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
||||
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, i.Interaction.Member.User.ID, err)
|
||||
return
|
||||
}
|
||||
user := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username)
|
||||
@@ -503,7 +527,7 @@ func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang st
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send reply: %v", err)
|
||||
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -521,7 +545,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
|
||||
}
|
||||
delete(d.tokens, pin)
|
||||
return
|
||||
@@ -535,7 +559,7 @@ func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang stri
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
|
||||
}
|
||||
dcUser := d.users[i.Interaction.Member.User.ID]
|
||||
dcUser.JellyfinID = user.JellyfinID
|
||||
@@ -566,7 +590,7 @@ func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang str
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send reply: %v", err)
|
||||
d.app.err.Printf(lm.FailedReply, lm.Discord, i.Interaction.Member.User.ID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -575,7 +599,7 @@ func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang str
|
||||
func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
||||
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||
d.app.err.Printf(lm.FailedCreateDiscordDMChannel, i.Interaction.Member.User.ID, err)
|
||||
return
|
||||
}
|
||||
requester := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username)
|
||||
@@ -590,12 +614,9 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
||||
//}
|
||||
// Check whether requestor is linked to the admin account
|
||||
requesterEmail, ok := d.app.storage.GetEmailsKey(requester.JellyfinID)
|
||||
if !ok {
|
||||
d.app.err.Printf("Failed to verify admin")
|
||||
}
|
||||
if !requesterEmail.Admin {
|
||||
d.app.err.Printf("User is not admin")
|
||||
//add response message
|
||||
if !(ok && requesterEmail.Admin) {
|
||||
d.app.err.Printf(lm.FailedGenerateInvite, fmt.Sprintf(lm.NonAdminUser, requester.JellyfinID))
|
||||
// FIXME: add response message
|
||||
return
|
||||
}
|
||||
|
||||
@@ -629,7 +650,7 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
||||
ValidTill: validTill,
|
||||
UserLabel: userLabel,
|
||||
Profile: "Default",
|
||||
Label: fmt.Sprintf("Discord: %s", RenderDiscordUsername(recipient)),
|
||||
Label: fmt.Sprintf("%s: %s", lm.Discord, RenderDiscordUsername(recipient)),
|
||||
}
|
||||
if profileName != "" {
|
||||
if _, ok := d.app.storage.GetProfileKey(profileName); ok {
|
||||
@@ -638,13 +659,12 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
||||
}
|
||||
|
||||
if recipient != nil && d.app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||
d.app.debug.Printf("%s: Sending invite message", invite.Code)
|
||||
invname, err := d.bot.GuildMember(d.guildID, recipient.ID)
|
||||
invite.SendTo = invname.User.Username
|
||||
msg, err := d.app.email.constructInvite(invite.Code, invite, d.app, false)
|
||||
if err != nil {
|
||||
invite.SendTo = fmt.Sprintf("Failed to send to %s", RenderDiscordUsername(recipient))
|
||||
d.app.err.Printf("%s: Failed to construct invite message: %v", invite.Code, err)
|
||||
invite.SendTo = fmt.Sprintf(lm.FailedConstructInviteMessage, invite.Code, err)
|
||||
d.app.err.Println(invite.SendTo)
|
||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Data: &dg.InteractionResponseData{
|
||||
@@ -653,14 +673,14 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
|
||||
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err)
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
err = d.app.discord.SendDM(msg, recipient.ID)
|
||||
if err != nil {
|
||||
invite.SendTo = fmt.Sprintf("Failed to send to %s", RenderDiscordUsername(recipient))
|
||||
d.app.err.Printf("%s: %s: %v", invite.Code, invite.SendTo, err)
|
||||
invite.SendTo = fmt.Sprintf(lm.FailedSendInviteMessage, invite.Code, RenderDiscordUsername(recipient), err)
|
||||
d.app.err.Println(invite.SendTo)
|
||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Data: &dg.InteractionResponseData{
|
||||
@@ -669,10 +689,10 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
|
||||
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err)
|
||||
}
|
||||
} else {
|
||||
d.app.info.Printf("%s: Sent invite email to \"%s\"", invite.Code, RenderDiscordUsername(recipient))
|
||||
d.app.info.Printf(lm.SentInviteMessage, invite.Code, RenderDiscordUsername(recipient))
|
||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Data: &dg.InteractionResponseData{
|
||||
@@ -681,7 +701,7 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", RenderDiscordUsername(requester), err)
|
||||
d.app.err.Printf(lm.FailedReply, lm.Discord, requester.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -690,140 +710,6 @@ func (d *DiscordDaemon) cmdInvite(s *dg.Session, i *dg.InteractionCreate, lang s
|
||||
d.app.storage.SetInvitesKey(invite.Code, invite)
|
||||
}
|
||||
|
||||
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.msgStart(s, m, lang)
|
||||
case "!lang":
|
||||
d.msgLang(s, m, sects, lang)
|
||||
default:
|
||||
d.msgPIN(s, m, sects, lang)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) msgStart(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
|
||||
|
||||
_, err = d.bot.ChannelMessageSendReply(m.ChannelID, d.app.storage.lang.Telegram[lang].Strings.get("discordDMs"), m.Reference())
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send reply to \"%s\": %v", m.Author.Username, err)
|
||||
return
|
||||
}
|
||||
|
||||
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) msgLang(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 _, u := range d.app.storage.GetDiscord() {
|
||||
if u.ID == m.Author.ID {
|
||||
u.Lang = sects[1]
|
||||
d.app.storage.SetDiscordKey(u.JellyfinID, u)
|
||||
user = u
|
||||
break
|
||||
}
|
||||
}
|
||||
d.users[m.Author.ID] = user
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) msgPIN(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
|
||||
}
|
||||
user, ok := d.tokens[sects[0]]
|
||||
if !ok || time.Now().After(user.Expiry) {
|
||||
_, 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)
|
||||
}
|
||||
delete(d.tokens, sects[0])
|
||||
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)
|
||||
}
|
||||
dcUser := d.users[m.Author.ID]
|
||||
dcUser.JellyfinID = user.JellyfinID
|
||||
d.verifiedTokens[sects[0]] = dcUser
|
||||
delete(d.tokens, sects[0])
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error {
|
||||
channels := make([]string, len(userID))
|
||||
for i, id := range userID {
|
||||
@@ -877,10 +763,10 @@ func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
|
||||
}
|
||||
|
||||
// UserVerified returns whether or not a token with the given PIN has been verified, and the user itself.
|
||||
func (d *DiscordDaemon) UserVerified(pin string) (user DiscordUser, ok bool) {
|
||||
user, ok = d.verifiedTokens[pin]
|
||||
func (d *DiscordDaemon) UserVerified(pin string) (ContactMethodUser, bool) {
|
||||
u, ok := d.verifiedTokens[pin]
|
||||
// delete(d.verifiedTokens, pin)
|
||||
return
|
||||
return &u, ok
|
||||
}
|
||||
|
||||
// AssignedUserVerified returns whether or not a user with the given PIN has been verified, and the token itself.
|
||||
@@ -900,7 +786,44 @@ func (d *DiscordDaemon) UserExists(id string) bool {
|
||||
return err != nil || c > 0
|
||||
}
|
||||
|
||||
// DeleteVerifiedUser removes the token with the given PIN.
|
||||
func (d *DiscordDaemon) DeleteVerifiedUser(pin string) {
|
||||
delete(d.verifiedTokens, pin)
|
||||
// Exists returns whether or not the given user exists.
|
||||
func (d *DiscordDaemon) Exists(user ContactMethodUser) bool {
|
||||
return d.UserExists(user.MethodID().(string))
|
||||
}
|
||||
|
||||
// DeleteVerifiedToken removes the token with the given PIN.
|
||||
func (d *DiscordDaemon) DeleteVerifiedToken(PIN string) {
|
||||
delete(d.verifiedTokens, PIN)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) PIN(req newUserDTO) string { return req.DiscordPIN }
|
||||
|
||||
func (d *DiscordDaemon) Name() string { return lm.Discord }
|
||||
|
||||
func (d *DiscordDaemon) Required() bool {
|
||||
return d.app.config.Section("discord").Key("required").MustBool(false)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) UniqueRequired() bool {
|
||||
return d.app.config.Section("discord").Key("require_unique").MustBool(false)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) PostVerificationTasks(PIN string, u ContactMethodUser) error {
|
||||
err := d.ApplyRole(u.MethodID().(string))
|
||||
if err != nil {
|
||||
return fmt.Errorf(lm.FailedSetDiscordMemberRole, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DiscordUser) Name() string { return RenderDiscordUsername(*d) }
|
||||
func (d *DiscordUser) SetMethodID(id any) { d.ID = id.(string) }
|
||||
func (d *DiscordUser) MethodID() any { return d.ID }
|
||||
func (d *DiscordUser) SetJellyfin(id string) { d.JellyfinID = id }
|
||||
func (d *DiscordUser) Jellyfin() string { return d.JellyfinID }
|
||||
func (d *DiscordUser) SetAllowContactFromDTO(req newUserDTO) { d.Contact = req.DiscordContact }
|
||||
func (d *DiscordUser) SetAllowContact(contact bool) { d.Contact = contact }
|
||||
func (d *DiscordUser) AllowContact() bool { return d.Contact }
|
||||
func (d *DiscordUser) Store(st *Storage) {
|
||||
st.SetDiscordKey(d.Jellyfin(), *d)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module github.com/hrfee/jfa-go/easyproxy
|
||||
|
||||
go 1.20
|
||||
go 1.18
|
||||
|
||||
require golang.org/x/net v0.15.0
|
||||
require golang.org/x/net v0.36.0
|
||||
|
||||
require github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b // indirect
|
||||
require github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b h1:xZ59n7Frzh8CwyfAapUZLSg+gXH5m63YEaFCMpDHhpI=
|
||||
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b/go.mod h1:uDd4sYVYsqcxAB8j+Q7uhL6IJCs/r1kxib1HV4bgOMg=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||
|
||||
51
email.go
@@ -10,6 +10,7 @@ import (
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/hrfee/jfa-go/easyproxy"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/itchyny/timefmt-go"
|
||||
"github.com/mailgun/mailgun-go/v4"
|
||||
@@ -95,10 +97,10 @@ func NewEmailer(app *appContext) *Emailer {
|
||||
authType := sMail.AuthType(app.config.Section("smtp").Key("auth_type").MustInt(4))
|
||||
err := emailer.NewSMTP(app.config.Section("smtp").Key("server").String(), app.config.Section("smtp").Key("port").MustInt(465), username, password, sslTLS, app.config.Section("smtp").Key("ssl_cert").MustString(""), app.config.Section("smtp").Key("hello_hostname").String(), app.config.Section("smtp").Key("cert_validation").MustBool(true), authType, proxyConf)
|
||||
if err != nil {
|
||||
app.err.Printf("Error while initiating SMTP mailer: %v", err)
|
||||
app.err.Printf(lm.FailedInitSMTP, err)
|
||||
}
|
||||
} else if method == "mailgun" {
|
||||
emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String())
|
||||
emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String(), app.proxyTransport)
|
||||
} else if method == "dummy" {
|
||||
emailer.sender = &DummyClient{}
|
||||
}
|
||||
@@ -200,10 +202,14 @@ type Mailgun struct {
|
||||
}
|
||||
|
||||
// NewMailgun returns a Mailgun emailClient.
|
||||
func (emailer *Emailer) NewMailgun(url, key string) {
|
||||
func (emailer *Emailer) NewMailgun(url, key string, transport *http.Transport) {
|
||||
sender := &Mailgun{
|
||||
client: mailgun.NewMailgun(strings.Split(emailer.fromAddr, "@")[1], key),
|
||||
}
|
||||
if transport != nil {
|
||||
cli := sender.client.Client()
|
||||
cli.Transport = transport
|
||||
}
|
||||
// Mailgun client takes the base url, so we need to trim off the end (e.g 'v3/messages')
|
||||
if strings.Contains(url, "messages") {
|
||||
url = url[0:strings.LastIndex(url, "/")]
|
||||
@@ -319,17 +325,11 @@ func (emailer *Emailer) confirmationValues(code, username, key string, app *appC
|
||||
}
|
||||
} else {
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
||||
inviteLink := app.ExternalURI
|
||||
if code == "" { // Personal email change
|
||||
if strings.HasSuffix(inviteLink, "/invite") {
|
||||
inviteLink = strings.TrimSuffix(inviteLink, "/invite")
|
||||
}
|
||||
inviteLink = fmt.Sprintf("%s/my/confirm/%s", inviteLink, url.PathEscape(key))
|
||||
} else { // Invite email confirmation
|
||||
if !strings.HasSuffix(inviteLink, "/invite") {
|
||||
inviteLink += "/invite"
|
||||
}
|
||||
inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, url.PathEscape(key))
|
||||
inviteLink = fmt.Sprintf("%s%s/%s?key=%s", inviteLink, PAGES.Form, code, url.PathEscape(key))
|
||||
}
|
||||
template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username})
|
||||
template["confirmationURL"] = inviteLink
|
||||
@@ -393,11 +393,7 @@ func (emailer *Emailer) inviteValues(code string, invite Invite, app *appContext
|
||||
expiry := invite.ValidTill
|
||||
d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
inviteLink := app.config.Section("invite_emails").Key("url_base").String()
|
||||
if !strings.HasSuffix(inviteLink, "/invite") {
|
||||
inviteLink += "/invite"
|
||||
}
|
||||
inviteLink = fmt.Sprintf("%s/%s", inviteLink, code)
|
||||
inviteLink := fmt.Sprintf("%s%s/%s", app.ExternalURI, PAGES.Form, code)
|
||||
template := map[string]interface{}{
|
||||
"hello": emailer.lang.InviteEmail.get("hello"),
|
||||
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
|
||||
@@ -580,7 +576,7 @@ func (emailer *Emailer) resetValues(pwr PasswordReset, app *appContext, noSub bo
|
||||
// Only used in html email.
|
||||
template["pin_code"] = pwr.Pin
|
||||
} else {
|
||||
app.info.Println("Couldn't generate PWR link: %v", err)
|
||||
app.info.Printf(lm.FailedGeneratePWRLink, err)
|
||||
template["pin"] = pwr.Pin
|
||||
}
|
||||
} else {
|
||||
@@ -967,11 +963,10 @@ func (app *appContext) getAddressOrName(jfID string) string {
|
||||
// returns "" if none found. returns only the first match, might be an issue if there are users with the same contact method usernames.
|
||||
func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEmail, matchContactMethod bool) (user mediabrowser.User, ok bool) {
|
||||
ok = false
|
||||
var status int
|
||||
var err error = nil
|
||||
if matchUsername {
|
||||
user, status, err = app.jf.UserByName(address, false)
|
||||
if status == 200 && err == nil {
|
||||
user, err = app.jf.UserByName(address, false)
|
||||
if err == nil {
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
@@ -982,8 +977,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
|
||||
err = app.storage.db.Find(&emailAddresses, badgerhold.Where("Addr").Eq(address))
|
||||
if err == nil && len(emailAddresses) > 0 {
|
||||
for _, emailUser := range emailAddresses {
|
||||
user, status, err = app.jf.UserByID(emailUser.JellyfinID, false)
|
||||
if status == 200 && err == nil {
|
||||
user, err = app.jf.UserByID(emailUser.JellyfinID, false)
|
||||
if err == nil {
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
@@ -996,8 +991,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
|
||||
if matchContactMethod {
|
||||
for _, dcUser := range app.storage.GetDiscord() {
|
||||
if RenderDiscordUsername(dcUser) == strings.ToLower(address) {
|
||||
user, status, err = app.jf.UserByID(dcUser.JellyfinID, false)
|
||||
if status == 200 && err == nil {
|
||||
user, err = app.jf.UserByID(dcUser.JellyfinID, false)
|
||||
if err == nil {
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
@@ -1008,8 +1003,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
|
||||
err = app.storage.db.Find(&telegramUsers, badgerhold.Where("Username").Eq(tgUsername))
|
||||
if err == nil && len(telegramUsers) > 0 {
|
||||
for _, telegramUser := range telegramUsers {
|
||||
user, status, err = app.jf.UserByID(telegramUser.JellyfinID, false)
|
||||
if status == 200 && err == nil {
|
||||
user, err = app.jf.UserByID(telegramUser.JellyfinID, false)
|
||||
if err == nil {
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
@@ -1019,8 +1014,8 @@ func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEma
|
||||
err = app.storage.db.Find(&matrixUsers, badgerhold.Where("UserID").Eq(address))
|
||||
if err == nil && len(matrixUsers) > 0 {
|
||||
for _, matrixUser := range matrixUsers {
|
||||
user, status, err = app.jf.UserByID(matrixUser.JellyfinID, false)
|
||||
if status == 200 && err == nil {
|
||||
user, err = app.jf.UserByID(matrixUser.JellyfinID, false)
|
||||
if err == nil {
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build external
|
||||
// +build external
|
||||
|
||||
package main
|
||||
@@ -12,6 +13,8 @@ import (
|
||||
|
||||
const binaryType = "external"
|
||||
|
||||
func BuildTagsExternal() { buildTags = append(buildTags, "external") }
|
||||
|
||||
var localFS dirFS
|
||||
var langFS dirFS
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package main
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
||||
|
||||
@@ -36,7 +40,7 @@ func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app
|
||||
func (d *GenericDaemon) Name(name string) { d.name = name }
|
||||
|
||||
func (d *GenericDaemon) run() {
|
||||
d.app.info.Printf("%s started", d.name)
|
||||
d.app.info.Printf(lm.StartDaemon, d.name)
|
||||
for {
|
||||
select {
|
||||
case <-d.ShutdownChannel:
|
||||
146
go.mod
@@ -1,6 +1,8 @@
|
||||
module github.com/hrfee/jfa-go
|
||||
|
||||
go 1.20
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.0
|
||||
|
||||
replace github.com/hrfee/jfa-go/docs => ./docs
|
||||
|
||||
@@ -10,6 +12,8 @@ replace github.com/hrfee/jfa-go/ombi => ./ombi
|
||||
|
||||
replace github.com/hrfee/jfa-go/logger => ./logger
|
||||
|
||||
replace github.com/hrfee/jfa-go/logmessages => ./logmessages
|
||||
|
||||
replace github.com/hrfee/jfa-go/linecache => ./linecache
|
||||
|
||||
replace github.com/hrfee/jfa-go/api => ./api
|
||||
@@ -19,118 +23,122 @@ replace github.com/hrfee/jfa-go/easyproxy => ./easyproxy
|
||||
replace github.com/hrfee/jfa-go/jellyseerr => ./jellyseerr
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/discordgo v0.27.1
|
||||
github.com/dgraph-io/badger/v3 v3.2103.5
|
||||
github.com/bwmarrin/discordgo v0.28.1
|
||||
github.com/dgraph-io/badger/v4 v4.3.1
|
||||
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/gin-contrib/pprof v1.4.0
|
||||
github.com/gin-contrib/static v0.0.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/gin-contrib/pprof v1.5.0
|
||||
github.com/gin-contrib/static v1.1.2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769
|
||||
github.com/hrfee/jfa-go/docs v0.0.0-20230626224816-f72960635dc3
|
||||
github.com/hrfee/jfa-go/easyproxy v0.0.0-00010101000000-000000000000
|
||||
github.com/hrfee/jfa-go/linecache v0.0.0-20230626224816-f72960635dc3
|
||||
github.com/hrfee/jfa-go/logger v0.0.0-20230626224816-f72960635dc3
|
||||
github.com/hrfee/jfa-go/ombi v0.0.0-20230626224816-f72960635dc3
|
||||
github.com/hrfee/mediabrowser v0.3.13
|
||||
github.com/itchyny/timefmt-go v0.1.5
|
||||
github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20241105225412-da4470bc4fbc
|
||||
github.com/hrfee/jfa-go/docs v0.0.0-20241105225412-da4470bc4fbc
|
||||
github.com/hrfee/jfa-go/easyproxy v0.0.0-20241105225412-da4470bc4fbc
|
||||
github.com/hrfee/jfa-go/jellyseerr v0.0.0-20241105225412-da4470bc4fbc
|
||||
github.com/hrfee/jfa-go/linecache v0.0.0-20241105225412-da4470bc4fbc
|
||||
github.com/hrfee/jfa-go/logger v0.0.0-20241105225412-da4470bc4fbc
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20241105225412-da4470bc4fbc
|
||||
github.com/hrfee/jfa-go/ombi v0.0.0-20241105225412-da4470bc4fbc
|
||||
github.com/hrfee/mediabrowser v0.3.24
|
||||
github.com/itchyny/timefmt-go v0.1.6
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||
github.com/mailgun/mailgun-go/v4 v4.9.1
|
||||
github.com/mailgun/mailgun-go/v4 v4.18.1
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/robert-nix/ansihtml v1.0.1
|
||||
github.com/steambap/captcha v1.4.1
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/timshannon/badgerhold/v4 v4.0.2
|
||||
github.com/timshannon/badgerhold/v4 v4.0.3
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
maunium.net/go/mautrix v0.15.3
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
maunium.net/go/mautrix v0.21.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.9.2 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.4 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/dgraph-io/ristretto v1.0.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
|
||||
github.com/getlantern/errors v1.0.3 // indirect
|
||||
github.com/getlantern/errors v1.0.4 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20230519221840-1283e026181c // indirect
|
||||
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-chi/chi/v5 v5.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/spec v0.20.9 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.22.1 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/go-test/deep v1.1.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/glog v1.1.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/flatbuffers v23.5.26+incompatible // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hrfee/jfa-go/jellyseerr v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/flatbuffers v24.3.25+incompatible // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b // indirect
|
||||
github.com/mailgun/errors v0.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20241025130422-66cb2e6d7274 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rs/zerolog v1.29.1 // indirect
|
||||
github.com/swaggo/swag v1.16.1 // indirect
|
||||
github.com/rs/zerolog v1.33.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/swaggo/swag v1.16.4 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
go.mau.fi/util v0.8.1 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/otel v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.opentelemetry.io/otel v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.31.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.31.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
|
||||
golang.org/x/image v0.8.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/tools v0.10.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/arch v0.11.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||
golang.org/x/image v0.21.0 // indirect
|
||||
golang.org/x/net v0.36.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
)
|
||||
|
||||
361
go.sum
@@ -1,32 +1,35 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
|
||||
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM=
|
||||
github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
|
||||
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
|
||||
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
@@ -34,18 +37,18 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.1/go.mod h1:dULbq6ehJ5K0cGW/1TQ9iSfUk0gbSiToDWmWmTsJ53E=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
|
||||
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
|
||||
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
|
||||
github.com/dgraph-io/badger/v4 v4.1.0/go.mod h1:P50u28d39ibBRmIJuQC/NSdBOg46HnHw7al2SW5QRHg=
|
||||
github.com/dgraph-io/badger/v4 v4.3.1 h1:7r5wKqmoRpGgSxqa0S/nGdpOpvvzuREGPLSua73C8tw=
|
||||
github.com/dgraph-io/badger/v4 v4.3.1/go.mod h1:oObz97DImXpd6O/Dt8BqdKLLTDmEmarAimo72VV5whQ=
|
||||
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
|
||||
github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
@@ -55,27 +58,21 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA=
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/errors v1.0.3 h1:Ne4Ycj7NI1BtSyAfVeAT/DNoxz7/S2BUc3L2Ht1YSHE=
|
||||
github.com/getlantern/errors v1.0.3/go.mod h1:m8C7H1qmouvsGpwQqk/6NUpIVMpfzUPn608aBZDYV04=
|
||||
github.com/getlantern/errors v1.0.4 h1:i2iR1M9GKj4WuingpNqJ+XQEw6i6dnAgKAmLj6ZB3X0=
|
||||
github.com/getlantern/errors v1.0.4/go.mod h1:/Foq8jtSDGP8GOXzAjeslsC4Ar/3kB+UiQH+WyV4pzY=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 h1:NlQedYmPI3pRAXJb+hLVVDGqfvvXGRPV8vp7XOjKAZ0=
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU=
|
||||
@@ -87,71 +84,62 @@ github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2y
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/ops v0.0.0-20230519221840-1283e026181c h1:qcPAzA1ZDnwx618jAgQmxo6UvJkw2SkM1L4ofncmEhI=
|
||||
github.com/getlantern/ops v0.0.0-20230519221840-1283e026181c/go.mod h1:g2ueCncOwWenlAr56Fh90FwsACkelqqtFUDLAHg1mng=
|
||||
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 h1:3BwvWj0JZzFEvNNiMhCu4bf60nqcIuQpTYb00Ezm1ag=
|
||||
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534/go.mod h1:ZsLfOY6gKQOTyEcPYNA9ws5/XHZQFroxqCOhHjGcs9Y=
|
||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
|
||||
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/pprof v1.5.0 h1:E/Oy7g+kNw94KfdCy3bZxQFtyDnAX2V7axRS7sNYVrU=
|
||||
github.com/gin-contrib/pprof v1.5.0/go.mod h1:GqFL6LerKoCQ/RSWnkYczkTJ+tOAUVN/8sbnEtaqOKs=
|
||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
|
||||
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
|
||||
github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0NglqmlZ4=
|
||||
github.com/gin-contrib/static v1.1.2/go.mod h1:Fw90ozjHCmZBWbgrsqrDvO28YbhKEKzKp8GixhR4yLw=
|
||||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
|
||||
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
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.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
|
||||
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
|
||||
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||
@@ -159,9 +147,8 @@ github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaEL
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
@@ -170,8 +157,6 @@ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw=
|
||||
github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -181,7 +166,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
@@ -190,19 +174,17 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a h1:AWZzzFrqyjYlRloN6edwTLTUbKxf5flLXNuTBDm3Ews=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 h1:5lyLWsV+qCkoYqsKUDuycESh9DEIPVKN6iCFeL7ag50=
|
||||
github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg=
|
||||
github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v23.5.9+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
|
||||
github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -212,53 +194,48 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hrfee/mediabrowser v0.3.13 h1:NgQNbq+JWwsP68BdWXL/rwbpfE/oO5LJ5KVkE+aNbX8=
|
||||
github.com/hrfee/mediabrowser v0.3.13/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||
github.com/hrfee/mediabrowser v0.3.24 h1:cT5+X3bZeaSBQFevMYkFIw6JJ8nW7Myvb+11a2/THMA=
|
||||
github.com/hrfee/mediabrowser v0.3.24/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
|
||||
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
|
||||
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
@@ -266,44 +243,43 @@ github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b h1:xZ59n7Frzh8CwyfAapUZLSg+gXH5m63YEaFCMpDHhpI=
|
||||
github.com/magisterquis/connectproxy v0.0.0-20200725203833-3582e84f0c9b/go.mod h1:uDd4sYVYsqcxAB8j+Q7uhL6IJCs/r1kxib1HV4bgOMg=
|
||||
github.com/mailgun/mailgun-go/v4 v4.9.1 h1:D/jhJXYod4RqRsNOOSrjrtAcMEnz8mPYJmeA5cueHKY=
|
||||
github.com/mailgun/mailgun-go/v4 v4.9.1/go.mod h1:FJlF9rI5cQT+mrwujtJjPMbIVy3Ebor9bKTVsJ0QU40=
|
||||
github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8=
|
||||
github.com/mailgun/errors v0.4.0/go.mod h1:xGBaaKdEdQT0/FhwvoXv4oBaqqmVZz9P1XEnvD/onc0=
|
||||
github.com/mailgun/mailgun-go/v4 v4.18.1 h1:ShNH/wzj7albTF/6le011FF+DGMd3azcSKL4iO9AgeI=
|
||||
github.com/mailgun/mailgun-go/v4 v4.18.1/go.mod h1:+d4FCswFAukgYc1XtKK2IxOYaVxjVm8AN2z/5TBiT8M=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
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/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/petermattis/goid v0.0.0-20241025130422-66cb2e6d7274 h1:qli3BGQK0tYDkSEvZ/FzZTi9ZrOX86Q6CIhKLGc489A=
|
||||
github.com/petermattis/goid v0.0.0-20241025130422-66cb2e6d7274/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -312,20 +288,18 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc=
|
||||
github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
|
||||
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
@@ -346,9 +320,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
@@ -357,13 +330,13 @@ github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+z
|
||||
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
||||
github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
|
||||
github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=
|
||||
github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=
|
||||
github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=
|
||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
@@ -371,25 +344,20 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/timshannon/badgerhold/v3 v3.0.0-20210909134927-2b6764d68c1e h1:zWSVsQaifg0cVH9VvR+cMguV7exK6U+SoW8YD1cZpR4=
|
||||
github.com/timshannon/badgerhold/v3 v3.0.0-20210909134927-2b6764d68c1e/go.mod h1:/Seq5xGNo8jLhSbDX3jdbeZrp4yFIpQ6/7n4TjziEWs=
|
||||
github.com/timshannon/badgerhold/v4 v4.0.2 h1:83OLY/NFnEaMnHEPd84bYtkLipVkjTsMbzQRYbk47g4=
|
||||
github.com/timshannon/badgerhold/v4 v4.0.2/go.mod h1:rh6RyXLQFsvrvcKondPQQFZnNovpRzu+gS0FlLxYuHY=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
|
||||
github.com/timshannon/badgerhold/v4 v4.0.3 h1:W6pd2qckoXw2cl8eH0ZCV/9CXNaXvaM26tzFi5Tj+v8=
|
||||
github.com/timshannon/badgerhold/v4 v4.0.3/go.mod h1:IkZIr0kcZLMdD7YJfW/G6epb6ZXHD/h0XR2BTk/VZg8=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 h1:flbMkdl6HxQkLs6DDhH1UkcnFpNBOu70391STjMS0O4=
|
||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
|
||||
@@ -401,48 +369,46 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mau.fi/util v0.8.1 h1:Ga43cz6esQBYqcjZ/onRoVnYWoUwjWbsxVeJg2jOTSo=
|
||||
go.mau.fi/util v0.8.1/go.mod h1:T1u/rD2rzidVrBLyaUdPpZiJdP/rsyi+aTzn0D+Q6wc=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
|
||||
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
|
||||
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
|
||||
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
|
||||
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
|
||||
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
|
||||
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
|
||||
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
|
||||
go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
|
||||
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
||||
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
||||
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
|
||||
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
|
||||
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
|
||||
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg=
|
||||
golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM=
|
||||
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
|
||||
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
@@ -452,7 +418,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
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=
|
||||
@@ -470,12 +437,12 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||
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=
|
||||
@@ -487,6 +454,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -501,36 +470,34 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
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=
|
||||
@@ -545,8 +512,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
|
||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
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=
|
||||
@@ -574,18 +541,15 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
@@ -593,17 +557,12 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
|
||||
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
|
||||
maunium.net/go/mautrix v0.15.3 h1:C9BHSUM0gYbuZmAtopuLjIcH5XHLb/ZjTEz7nN+0jN0=
|
||||
maunium.net/go/mautrix v0.15.3/go.mod h1:zLrQqdxJlLkurRCozTc9CL6FySkgZlO/kpCYxBILSLE=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
maunium.net/go/mautrix v0.21.1 h1:Z+e448jtlY977iC1kokNJTH5kg2WmDpcQCqn+v9oZOA=
|
||||
maunium.net/go/mautrix v0.21.1/go.mod h1:7F/S6XAdyc/6DW+Q7xyFXRSPb6IjfqMb1OMepQ8C8OE=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
||||
@@ -3,7 +3,8 @@ package main
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/dgraph-io/badger/v3"
|
||||
"github.com/dgraph-io/badger/v4"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
@@ -12,10 +13,10 @@ import (
|
||||
// meant to be called with other such housekeeping functions, so assumes
|
||||
// the user cache is fresh.
|
||||
func (app *appContext) clearEmails() {
|
||||
app.debug.Println("Housekeeping: removing unused email addresses")
|
||||
app.debug.Println(lm.HousekeepingEmail)
|
||||
emails := app.storage.GetEmails()
|
||||
for _, email := range emails {
|
||||
_, _, err := app.jf.UserByID(email.JellyfinID, false)
|
||||
_, err := app.jf.UserByID(email.JellyfinID, false)
|
||||
// Make sure the user doesn't exist, and no other error has occured
|
||||
switch err.(type) {
|
||||
case mediabrowser.ErrUserNotFound:
|
||||
@@ -28,15 +29,20 @@ func (app *appContext) clearEmails() {
|
||||
|
||||
// clearDiscord does the same as clearEmails, but for Discord Users.
|
||||
func (app *appContext) clearDiscord() {
|
||||
app.debug.Println("Housekeeping: removing unused Discord IDs")
|
||||
app.debug.Println(lm.HousekeepingDiscord)
|
||||
discordUsers := app.storage.GetDiscord()
|
||||
for _, discordUser := range discordUsers {
|
||||
_, _, err := app.jf.UserByID(discordUser.JellyfinID, false)
|
||||
user, err := app.jf.UserByID(discordUser.JellyfinID, false)
|
||||
// Make sure the user doesn't exist, and no other error has occured
|
||||
switch err.(type) {
|
||||
case mediabrowser.ErrUserNotFound:
|
||||
// Remove role in case their account was deleted oustide of jfa-go
|
||||
app.discord.RemoveRole(discordUser.MethodID().(string))
|
||||
app.storage.DeleteDiscordKey(discordUser.JellyfinID)
|
||||
default:
|
||||
if user.Policy.IsDisabled {
|
||||
app.discord.RemoveRole(discordUser.MethodID().(string))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -44,10 +50,10 @@ func (app *appContext) clearDiscord() {
|
||||
|
||||
// clearMatrix does the same as clearEmails, but for Matrix Users.
|
||||
func (app *appContext) clearMatrix() {
|
||||
app.debug.Println("Housekeeping: removing unused Matrix IDs")
|
||||
app.debug.Println(lm.HousekeepingMatrix)
|
||||
matrixUsers := app.storage.GetMatrix()
|
||||
for _, matrixUser := range matrixUsers {
|
||||
_, _, err := app.jf.UserByID(matrixUser.JellyfinID, false)
|
||||
_, err := app.jf.UserByID(matrixUser.JellyfinID, false)
|
||||
// Make sure the user doesn't exist, and no other error has occured
|
||||
switch err.(type) {
|
||||
case mediabrowser.ErrUserNotFound:
|
||||
@@ -60,10 +66,10 @@ func (app *appContext) clearMatrix() {
|
||||
|
||||
// clearTelegram does the same as clearEmails, but for Telegram Users.
|
||||
func (app *appContext) clearTelegram() {
|
||||
app.debug.Println("Housekeeping: removing unused Telegram IDs")
|
||||
app.debug.Println(lm.HousekeepingTelegram)
|
||||
telegramUsers := app.storage.GetTelegram()
|
||||
for _, telegramUser := range telegramUsers {
|
||||
_, _, err := app.jf.UserByID(telegramUser.JellyfinID, false)
|
||||
_, err := app.jf.UserByID(telegramUser.JellyfinID, false)
|
||||
// Make sure the user doesn't exist, and no other error has occured
|
||||
switch err.(type) {
|
||||
case mediabrowser.ErrUserNotFound:
|
||||
@@ -75,7 +81,7 @@ func (app *appContext) clearTelegram() {
|
||||
}
|
||||
|
||||
func (app *appContext) clearPWRCaptchas() {
|
||||
app.debug.Println("Housekeeping: Clearing old PWR Captchas")
|
||||
app.debug.Println(lm.HousekeepingCaptcha)
|
||||
captchas := map[string]Captcha{}
|
||||
for k, capt := range app.pwrCaptchas {
|
||||
if capt.Generated.Add(CAPTCHA_VALIDITY * time.Second).After(time.Now()) {
|
||||
@@ -86,7 +92,7 @@ func (app *appContext) clearPWRCaptchas() {
|
||||
}
|
||||
|
||||
func (app *appContext) clearActivities() {
|
||||
app.debug.Println("Housekeeping: Cleaning up Activity log...")
|
||||
app.debug.Println(lm.HousekeepingActivity)
|
||||
keepCount := app.config.Section("activity_log").Key("keep_n_records").MustInt(1000)
|
||||
maxAgeDays := app.config.Section("activity_log").Key("delete_after_days").MustInt(90)
|
||||
minAge := time.Now().AddDate(0, 0, -maxAgeDays)
|
||||
@@ -103,7 +109,7 @@ func (app *appContext) clearActivities() {
|
||||
}
|
||||
}
|
||||
if err == badger.ErrTxnTooBig {
|
||||
app.debug.Printf("Activities: Delete txn was too big, doing it manually.")
|
||||
app.debug.Printf(lm.ActivityLogTxnTooBig)
|
||||
list := []Activity{}
|
||||
if errorSource == 0 {
|
||||
app.storage.db.Find(&list, badgerhold.Where("Time").Lt(minAge))
|
||||
@@ -119,18 +125,18 @@ func (app *appContext) clearActivities() {
|
||||
func newHousekeepingDaemon(interval time.Duration, app *appContext) *GenericDaemon {
|
||||
d := NewGenericDaemon(interval, app,
|
||||
func(app *appContext) {
|
||||
app.debug.Println("Housekeeping: Checking for expired invites")
|
||||
app.debug.Println(lm.HousekeepingInvites)
|
||||
app.checkInvites()
|
||||
},
|
||||
func(app *appContext) { app.clearActivities() },
|
||||
)
|
||||
|
||||
d.Name("Housekeeping daemon")
|
||||
d.Name("Housekeeping")
|
||||
|
||||
clearEmail := app.config.Section("email").Key("require_unique").MustBool(false)
|
||||
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
|
||||
clearTelegram := app.config.Section("telegram").Key("require_unique").MustBool(false)
|
||||
clearMatrix := app.config.Section("matrix").Key("require_unique").MustBool(false)
|
||||
clearDiscord := discordEnabled && (app.config.Section("discord").Key("require_unique").MustBool(false) || app.config.Section("discord").Key("disable_enable_role").MustBool(false))
|
||||
clearTelegram := telegramEnabled && (app.config.Section("telegram").Key("require_unique").MustBool(false))
|
||||
clearMatrix := matrixEnabled && (app.config.Section("matrix").Key("require_unique").MustBool(false))
|
||||
clearPWR := app.config.Section("captcha").Key("enabled").MustBool(false) && !app.config.Section("captcha").Key("recaptcha").MustBool(false)
|
||||
|
||||
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
|
||||
@@ -1,12 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>404 - jfa-go</title>
|
||||
{{ template "header.html" . }}
|
||||
</head>
|
||||
<body class="section">
|
||||
<div class="page-container">
|
||||
<div class="page-container m-2 lg:my-20 lg:mx-64">
|
||||
<div class="card">
|
||||
<h1 class="heading">Page not found.</h1>
|
||||
<p class="content">
|
||||
|
||||
169
html/admin.html
@@ -1,16 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
||||
<script>
|
||||
window.URLBase = "{{ .urlBase }}";
|
||||
window.notificationsEnabled = {{ .notifications }};
|
||||
window.emailEnabled = {{ .emailEnabled }};
|
||||
window.telegramEnabled = {{ .telegramEnabled }};
|
||||
window.discordEnabled = {{ .discordEnabled }};
|
||||
window.matrixEnabled = {{ .matrixEnabled }};
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.jellyseerrEnabled = {{ .jellyseerrEnabled }};
|
||||
window.usernameEnabled = {{ .username }};
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
window.linkResetEnabled = {{ .linkResetEnabled }};
|
||||
@@ -18,7 +9,6 @@
|
||||
window.jellyfinLogin = {{ .jellyfinLogin }};
|
||||
window.jfAdminOnly = {{ .jfAdminOnly }};
|
||||
window.jfAllowAll = {{ .jfAllowAll }};
|
||||
window.referralsEnabled = {{ .referralsEnabled }};
|
||||
window.loginAppearance = "{{ .loginAppearance }}";
|
||||
</script>
|
||||
<title>Admin - jfa-go</title>
|
||||
@@ -27,16 +17,18 @@
|
||||
<body class="max-w-full overflow-x-hidden section">
|
||||
{{ template "login-modal.html" . }}
|
||||
<div id="modal-add-user" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3" id="form-add-user" href="">
|
||||
<form class="card relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 flex flex-col gap-2" id="form-add-user" href="">
|
||||
<span class="heading">{{ .strings.newUser }} <span class="modal-close">×</span></span>
|
||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="add-user-user">
|
||||
<input type="email" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.emailAddress }}">
|
||||
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="add-user-password">
|
||||
<label class="label supra">{{ .strings.profile }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="add-user-profile">
|
||||
</select>
|
||||
</div>
|
||||
<input type="text" class="field input ~neutral @high" placeholder="{{ .strings.username }}" id="add-user-user">
|
||||
<input type="email" class="field input ~neutral @high" placeholder="{{ .strings.emailAddress }}">
|
||||
<input type="password" class="field input ~neutral @high" placeholder="{{ .strings.password }}" id="add-user-password">
|
||||
<label class="label flex flex-col gap-2">
|
||||
<span class="supra">{{ .strings.profile }}</span>
|
||||
<div class="select ~neutral @low">
|
||||
<select id="add-user-profile">
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.create }}</span>
|
||||
@@ -44,13 +36,14 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-about" class="modal">
|
||||
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/3 content card">
|
||||
<img src="{{ .urlBase }}/banner.svg" class="banner header" alt="jfa-go banner">
|
||||
<div class="relative mx-auto my-[10%] w-11/12 sm:w-4/5 lg:w-1/2 content card">
|
||||
<img src="{{ .pages.Base }}/banner.svg" class="banner header" alt="jfa-go banner">
|
||||
<span class="heading"><span class="modal-close">×</span></span>
|
||||
<p>{{ .strings.version }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .version }}</span></p>
|
||||
<p>{{ .strings.commitNoun }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .commit }}</span></p>
|
||||
<p>{{ .strings.buildTime }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .buildTime }}</span></p>
|
||||
<p>{{ .strings.builtBy }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .builtBy }}</span></p>
|
||||
<p>{{ .strings.buildTags }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .buildTags }}</span></p>
|
||||
<div class="flex flex-row flex-wrap gap-2 my-2">
|
||||
<a class="button ~neutral lang-link" href="https://github.com/hrfee/jfa-go"><i class="ri-github-line mr-2"></i>github</a>
|
||||
<a class="button ~urge lang-link" href="https://wiki.jfa-go.com">wiki/docs</a>
|
||||
@@ -311,25 +304,27 @@
|
||||
<form class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card" id="form-editor" href="">
|
||||
<span class="heading"><span id="header-editor"></span> <span class="modal-close">×</span></span>
|
||||
<div class="row">
|
||||
<div class="col card ~neutral @low">
|
||||
<aside class="aside sm ~urge dark:~d_info mb-2 @low" id="aside-editor"></aside>
|
||||
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span>
|
||||
<div id="editor-variables" class="mt-4"></div>
|
||||
<span class="label supra" for="editor-conditionals" id="label-editor-conditionals">{{ .strings.conditionals }}</span>
|
||||
<div id="editor-conditionals"></div>
|
||||
<label class="label supra" for="textarea-editor">{{ .strings.message }}</label>
|
||||
<textarea id="textarea-editor" class="textarea full-width flex-auto ~neutral @low mt-4 font-mono"></textarea>
|
||||
<p class="support mt-4 mb-2">{{ .strings.markdownSupported }}</p>
|
||||
<div class="flex-row">
|
||||
<label class="full-width ml-2">
|
||||
<div class="col card ~neutral @low flex flex-col gap-2 justify-between">
|
||||
<div class="flex flex-col gap-2">
|
||||
<aside class="aside sm ~urge dark:~d_info @low" id="aside-editor"></aside>
|
||||
<label class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</label>
|
||||
<div id="editor-variables" class="flex flex-row gap-2 flex-wrap"></div>
|
||||
<span class="label supra" for="editor-conditionals" id="label-editor-conditionals">{{ .strings.conditionals }}</span>
|
||||
<div id="editor-conditionals"></div>
|
||||
<label class="label supra" for="textarea-editor">{{ .strings.message }}</label>
|
||||
<textarea id="textarea-editor" class="textarea full-width flex-auto ~neutral @low font-mono"></textarea>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="support">{{ .strings.markdownSupported }}</p>
|
||||
<label class="w-full">
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.submit }}</span>
|
||||
<span class="button ~urge @low w-full supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col card ~neutral @low">
|
||||
<div class="col card ~neutral @low flex flex-col gap-2">
|
||||
<span class="subheading supra">{{ .strings.preview }}</span>
|
||||
<div class="mt-8" id="editor-preview"></div>
|
||||
<div id="editor-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -367,6 +362,7 @@
|
||||
<tr>
|
||||
<th>{{ .strings.name }}</th>
|
||||
<th>{{ .strings.date }}</th>
|
||||
<th>{{ .strings.version }}</th>
|
||||
<th class="table-inline justify-center">{{ .strings.backupDownloadRestore }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -535,58 +531,38 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="notification-box"></div>
|
||||
<div class="top-4 left-4 absolute flex flex-row gap-2">
|
||||
<span class="dropdown z-[11]" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
<span class="ml-2 chev"></span>
|
||||
</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low">
|
||||
<label class="switch pb-4">
|
||||
<input type="radio" name="lang-time" id="lang-12h">
|
||||
<span>{{ .strings.time12h }}</span>
|
||||
</label>
|
||||
<label class="switch pb-4">
|
||||
<input type="radio" name="lang-time" id="lang-24h">
|
||||
<span>{{ .strings.time24h }}</span>
|
||||
</label>
|
||||
<div id="lang-list"></div>
|
||||
</div>
|
||||
<div class="page-container m-2 lg:my-20 lg:mx-64 flex flex-col gap-4">
|
||||
<div class="top-2 inset-x-2 lg:absolute flex flex-row justify-between">
|
||||
<div class="flex flex-row gap-2">
|
||||
{{ template "lang-select.html" . }}
|
||||
<span class="button ~warning h-min" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
||||
</div>
|
||||
</span>
|
||||
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
||||
</div>
|
||||
{{ if .userPageEnabled }}
|
||||
<div class="top-4 right-4 absolute">
|
||||
<a class="button ~info" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="page-container">
|
||||
<div class="mb-4">
|
||||
<header>
|
||||
<div class="flex flex-row overflow-x-scroll items-center">
|
||||
<span id="button-tab-invites" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.invites }}</span>
|
||||
<span id="button-tab-accounts" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.accounts }}</span>
|
||||
<span id="button-tab-activity" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.activity }}</span>
|
||||
<span id="button-tab-settings" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.settings }}</span>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div>
|
||||
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
|
||||
<div class="flex flex-row gap-2">
|
||||
<span class="button ~critical @low unfocused" id="logout-button">{{ .strings.logout }}</span>
|
||||
{{ if .userPageEnabled }}
|
||||
<div class="">
|
||||
<a class="button ~info" href="{{ .pages.Base }}{{ .pages.MyAccount }}"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-invites">
|
||||
<div class="card @low invites dark:~d_neutral mb-4 overflow-visible">
|
||||
<header>
|
||||
<div class="flex flex-row overflow-x-auto items-center gap-2">
|
||||
<span id="button-tab-invites" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.invites }}</span>
|
||||
<span id="button-tab-accounts" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.accounts }}</span>
|
||||
<span id="button-tab-activity" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.activity }}</span>
|
||||
<span id="button-tab-settings" class="text-3xl button portal ~neutral dark:~d_neutral @low px-5">{{ .strings.settings }}</span>
|
||||
</div>
|
||||
</header>
|
||||
<div id="tab-invites" class="flex flex-col gap-4">
|
||||
<div class="card @low dark:~d_neutral flex flex-col gap-2 overflow-visible invites">
|
||||
<span class="heading">{{ .strings.invites }}</span>
|
||||
<div id="invites" class="mt-2"></div>
|
||||
<div id="invites"></div>
|
||||
</div>
|
||||
<div class="card @low dark:~d_neutral">
|
||||
<div class="card @low dark:~d_neutral flex flex-col gap-2">
|
||||
<span class="heading">{{ .strings.create }}</span>
|
||||
<div class="flex flex-col md:flex-row gap-3 mt-2" id="create-inv">
|
||||
<div class="card ~neutral @low flex flex-col gap-2 grow">
|
||||
<div class="flex flex-col md:flex-row gap-3" id="create-inv">
|
||||
<div class="card ~neutral @low flex flex-col gap-2 flex-1">
|
||||
<div class="flex flex-row gap-2">
|
||||
<label class="w-1/2">
|
||||
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
|
||||
@@ -694,7 +670,7 @@
|
||||
<input type="text" id="create-user-label" class="input ~neutral @low">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card ~neutral @low flex flex-col justify-between gap-2 grow">
|
||||
<div class="card ~neutral @low flex flex-col justify-between gap-2 flex-1">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-4">
|
||||
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
|
||||
@@ -738,7 +714,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-accounts" class="unfocused">
|
||||
<div id="tab-accounts" class="flex flex-col gap-4 unfocused">
|
||||
<div class="card @low dark:~d_neutral accounts mb-4 overflow-visible">
|
||||
<div id="accounts-filter-dropdown" class="dropdown manual z-10 w-full">
|
||||
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||
@@ -799,7 +775,7 @@
|
||||
<span class="button ~critical @low center " id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
||||
</div>
|
||||
<div class="card @low accounts-header table-responsive mt-2">
|
||||
<table class="table text-base leading-4">
|
||||
<table class="table text-base leading-5">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" value="" id="accounts-select-all"></th>
|
||||
@@ -837,7 +813,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-activity" class="unfocused">
|
||||
<div id="tab-activity" class="flex flex-col gap-4 unfocused">
|
||||
<div class="card @low dark:~d_neutral activity mb-4 overflow-visible">
|
||||
<div id="activity-filter-dropdown" class="dropdown manual z-10 w-full" tabindex="0">
|
||||
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||
@@ -894,8 +870,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-settings" class="unfocused">
|
||||
<div class="card @low dark:~d_neutral settings overflow">
|
||||
<div id="tab-settings" class="flex flex-col gap-4 unfocused">
|
||||
<div class="card @low dark:~d_neutral settings overflow flex flex-col gap-2">
|
||||
<div class="flex flex-col md:flex-row align-middle gap-2">
|
||||
<div class="flex flex-row align-middle justify-between md:justify-normal">
|
||||
<span class="heading">{{ .strings.settings }}</span>
|
||||
@@ -912,16 +888,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-3">
|
||||
<div class="card @low dark:~d_neutral col" id="settings-sidebar">
|
||||
<div class="md:card @low dark:~d_neutral flex md:flex flex-col gap-2 flex-1" id="settings-sidebar">
|
||||
<div class="flex flex-row justify-between">
|
||||
<input type="search" class="field ~neutral @low input settings-section-button justify-between mb-2" id="settings-search" placeholder="{{ .strings.search }}">
|
||||
<button class="button ~neutral @low center -ml-10 rounded-s-none mb-2 settings-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></button>
|
||||
<input type="search" class="field ~neutral @low input settings-section-button justify-between" id="settings-search" placeholder="{{ .strings.search }}">
|
||||
<button class="button ~neutral @low center -ml-10 rounded-s-none settings-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></button>
|
||||
</div>
|
||||
<aside class="aside sm ~urge dark:~d_info @low" id="settings-message">Note: <span class="badge ~critical">*</span> indicates a required field, <span class="badge ~info dark:~d_warning">R</span> indicates changes require a restart.</aside>
|
||||
<div id="settings-loader" class="flex flex-row flex-wrap gap-2">
|
||||
<span class="button ~neutral @low justify-center grow" id="setting-about"><span class="flex">{{ .strings.aboutProgram }} <i class="ri-information-line ml-2"></i></span></span>
|
||||
<a class="button ~urge dark:~d_info @low justify-center grow" target="_blank" href="https://wiki.jfa-go.com"><span class="flex">{{ .strings.wiki }} <i class="ri-book-shelf-line ml-2"></i></a>
|
||||
<span class="button ~neutral @low justify-center grow" id="setting-profiles"><span class="flex">{{ .strings.userProfiles }} <i class="ri-user-line ml-2"></i></span></span>
|
||||
</div>
|
||||
<aside class="aside sm ~urge dark:~d_info mb-2 @low" id="settings-message">Note: <span class="badge ~critical">*</span> indicates a required field, <span class="badge ~info dark:~d_warning">R</span> indicates changes require a restart.</aside>
|
||||
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-about"><span class="flex">{{ .strings.aboutProgram }} <i class="ri-information-line ml-2"></i></span></span>
|
||||
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-profiles"><span class="flex">{{ .strings.userProfiles }} <i class="ri-user-line ml-2"></i></span></span>
|
||||
</div>
|
||||
<div class="card ~neutral @low col overflow" id="settings-panel">
|
||||
<div class="card ~neutral @low overflow flex-1" id="settings-panel">
|
||||
<div class="settings-section unfocused h-[100%]" id="settings-not-found">
|
||||
<div class="flex flex-col h-[100%] justify-center items-center">
|
||||
<span class="text-2xl font-medium italic mb-2">{{ .strings.noResultsFound }}</span>
|
||||
@@ -936,6 +915,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ .urlBase }}/js/admin.js" type="module"></script>
|
||||
<script src="{{ .pages.Base }}/js/admin.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link inline rel="stylesheet" type="text/css" href="bundle.css">
|
||||
<!--- This CSS is inlined so we should keep this here! -->
|
||||
<link inline rel="stylesheet" type="text/css" href="web/css/v3bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>Crash report</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container">
|
||||
<div class="page-container m-2 lg:my-20 lg:mx-64">
|
||||
<div class="card ~critical sectioned">
|
||||
<section class="section ~critical">
|
||||
<span class="heading">Crash report for jfa-go</span>
|
||||
@@ -40,6 +41,6 @@
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<script inline src="crash.js"></script>
|
||||
<script inline src="web/js/crash.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>{{ .strings.successHeader }} - jfa-go</title>
|
||||
</head>
|
||||
<body class="section">
|
||||
<div class="page-container">
|
||||
<div class="page-container m-2 lg:my-20 lg:mx-64">
|
||||
<div class="card ~neutral @low mb-4">
|
||||
<span class="heading mb-4">{{ .strings.successHeader }}</span>
|
||||
<p class="content my-4">{{ .successMessage }}</p>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
window.usernameEnabled = {{ .username }};
|
||||
window.validationStrings = JSON.parse({{ .validationStrings }});
|
||||
window.invalidPassword = "{{ .strings.reEnterPasswordInvalid }}";
|
||||
window.URLBase = "{{ .urlBase }}";
|
||||
window.code = "{{ .code }}";
|
||||
window.language = "{{ .langName }}";
|
||||
window.messages = JSON.parse({{ .notifications }});
|
||||
@@ -14,16 +13,13 @@
|
||||
window.userExpiryHours = {{ .userExpiryHours }};
|
||||
window.userExpiryMinutes = {{ .userExpiryMinutes }};
|
||||
window.userExpiryMessage = {{ .userExpiryMessage }};
|
||||
window.telegramEnabled = {{ .telegramEnabled }};
|
||||
window.telegramRequired = {{ .telegramRequired }};
|
||||
window.telegramPIN = "{{ .telegramPIN }}";
|
||||
window.emailRequired = {{ .emailRequired }};
|
||||
window.discordEnabled = {{ .discordEnabled }};
|
||||
window.discordRequired = {{ .discordRequired }};
|
||||
window.discordPIN = "{{ .discordPIN }}";
|
||||
window.discordInviteLink = {{ .discordInviteLink }};
|
||||
window.discordServerName = "{{ .discordServerName }}";
|
||||
window.matrixEnabled = {{ .matrixEnabled }};
|
||||
window.matrixRequired = {{ .matrixRequired }};
|
||||
window.matrixUserID = "{{ .matrixUser }}";
|
||||
window.captcha = {{ .captcha }};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
{{ if .passwordReset }}
|
||||
<title>{{ .strings.passwordReset }}</title>
|
||||
@@ -35,20 +34,11 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ template "account-linking.html" . }}
|
||||
<div class="top-4 left-4 absolute">
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
<span class="ml-2 chev"></span>
|
||||
</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low" id="lang-list">
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div id="notification-box"></div>
|
||||
<div class="page-container">
|
||||
<div class="page-container m-2 lg:my-20 lg:mx-64 flex flex-col gap-4">
|
||||
<div class="top-2 inset-x-2 lg:absolute flex flex-row justify-between">
|
||||
{{ template "lang-select.html" . }}
|
||||
</div>
|
||||
<div class="card dark:~d_neutral @low">
|
||||
<div class="flex flex-col md:flex-row gap-3 items-baseline mb-2">
|
||||
<span class="heading mr-5">
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
<link rel="stylesheet" type="text/css" href="{{ .pages.Base }}/css/{{ .cssVersion }}bundle.css">
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="Description" content="jfa-go, a better way to manage Jellyfin users.">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<meta name="robots" content="noindex">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ .urlBase }}/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ .urlBase }}/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ .urlBase }}/favicon-16x16.png">
|
||||
<link rel="manifest" href="{{ .urlBase }}/site.webmanifest">
|
||||
<link rel="mask-icon" href="{{ .urlBase }}/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ .pages.Base }}/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ .pages.Base }}/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ .pages.Base }}/favicon-16x16.png">
|
||||
<link rel="manifest" href="{{ .pages.Base }}/site.webmanifest">
|
||||
<link rel="mask-icon" href="{{ .pages.Base }}/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#603cba">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<script>
|
||||
window.pages = {
|
||||
"Base": "{{ .pages.Base }}",
|
||||
"Current": "{{ .pages.Current }}",
|
||||
"Admin": "{{ .pages.Admin }}",
|
||||
"MyAccount": "{{ .pages.MyAccount }}",
|
||||
"Form": "{{ .pages.Form }}"
|
||||
};
|
||||
window.emailEnabled = {{ .emailEnabled }};
|
||||
window.discordEnabled = {{ .discordEnabled }};
|
||||
window.telegramEnabled = {{ .telegramEnabled }};
|
||||
window.matrixEnabled = {{ .matrixEnabled }};
|
||||
window.notificationsEnabled = {{ .notifications }};
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.jellyseerrEnabled = {{ .jellyseerrEnabled }};
|
||||
window.referralsEnabled = {{ .referralsEnabled }};
|
||||
window.pwrEnabled = {{ .pwrEnabled }};
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>Invalid Code - jfa-go</title>
|
||||
</head>
|
||||
<body class="section">
|
||||
<div class="page-container">
|
||||
<div class="page-container m-2 lg:my-20 lg:mx-64">
|
||||
<div class="card">
|
||||
<h1 class="text-3xl font-semibold">Invalid invite code.</h1>
|
||||
<p class="content">The code above was either incorrect, or has expired.</p>
|
||||
|
||||
19
html/lang-select.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<span class="dropdown z-[11]" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
<span class="ml-2 chev"></span>
|
||||
</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low flex flex-col gap-2">
|
||||
<label class="switch">
|
||||
<input type="radio" name="lang-time" id="lang-12h">
|
||||
<span>{{ .strings.time12h }}</span>
|
||||
</label>
|
||||
<label class="switch">
|
||||
<input type="radio" name="lang-time" id="lang-24h">
|
||||
<span>{{ .strings.time24h }}</span>
|
||||
</label>
|
||||
<div id="lang-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
@@ -15,7 +15,7 @@
|
||||
{{ $hasTwoCards = 1 }}
|
||||
<div class="card mx-2 flex-initial w-full lg:w-[35%] mb-4 lg:mb-0 dark:~d_neutral @low content">
|
||||
<span class="heading row">{{ .strings.loginNotAdmin }}</span>
|
||||
<a class="button ~info h-12 w-full" href="{{ .urlBase }}/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
||||
<a class="button ~info h-12 w-full" href="{{ .pages.Base }}{{ .pages.MyAccount }}"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>{{ .strings.passwordReset }} - jfa-go</title>
|
||||
</head>
|
||||
@@ -11,7 +10,7 @@
|
||||
<span id="copy-notification" class="unfocused">{{ .strings.copied }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="page-container">
|
||||
<div class="page-container m-2 lg:my-20 lg:mx-64">
|
||||
<div class="card ~neutral @low mb-4">
|
||||
<span class="heading mb-4">
|
||||
{{ if .success }}
|
||||
@@ -40,6 +39,6 @@
|
||||
</div>
|
||||
<i class="content">{{ .contactMessage }}</i>
|
||||
</div>
|
||||
<script src="{{ .urlBase }}/js/pwr-pin.js" type="module"></script>
|
||||
<script src="{{ .pages.Base }}/js/pwr-pin.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
895
html/setup.html
@@ -1,30 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="light">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
||||
<script>
|
||||
window.URLBase = "{{ .urlBase }}";
|
||||
window.notificationsEnabled = {{ .notifications }};
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
window.pwrEnabled = {{ .pwrEnabled }};
|
||||
window.linkResetEnabled = {{ .linkResetEnabled }};
|
||||
window.language = "{{ .langName }}";
|
||||
window.telegramEnabled = {{ .telegramEnabled }};
|
||||
window.telegramRequired = {{ .telegramRequired }};
|
||||
window.telegramUsername = {{ .telegramUsername }};
|
||||
window.telegramURL = {{ .telegramURL }};
|
||||
window.emailEnabled = {{ .emailEnabled }};
|
||||
window.emailRequired = {{ .emailRequired }};
|
||||
window.discordEnabled = {{ .discordEnabled }};
|
||||
window.discordRequired = {{ .discordRequired }};
|
||||
window.discordServerName = "{{ .discordServerName }}";
|
||||
window.discordInviteLink = {{ .discordInviteLink }};
|
||||
window.discordSendPINMessage = "{{ .discordSendPINMessage }}";
|
||||
window.matrixEnabled = {{ .matrixEnabled }};
|
||||
window.matrixRequired = {{ .matrixRequired }};
|
||||
window.matrixUserID = "{{ .matrixUser }}";
|
||||
window.validationStrings = JSON.parse({{ .validationStrings }});
|
||||
window.referralsEnabled = {{ .referralsEnabled }};
|
||||
</script>
|
||||
{{ template "header.html" . }}
|
||||
<title>{{ .strings.myAccount }}</title>
|
||||
@@ -79,33 +70,15 @@
|
||||
{{ template "login-modal.html" . }}
|
||||
{{ template "account-linking.html" . }}
|
||||
<div id="notification-box"></div>
|
||||
<div class="top-4 left-4 absolute">
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
<span class="ml-2 chev"></span>
|
||||
</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low">
|
||||
<label class="switch pb-4">
|
||||
<input type="radio" name="lang-time" id="lang-12h">
|
||||
<span>{{ .strings.time12h }}</span>
|
||||
</label>
|
||||
<label class="switch pb-4">
|
||||
<input type="radio" name="lang-time" id="lang-24h">
|
||||
<span>{{ .strings.time24h }}</span>
|
||||
</label>
|
||||
<div id="lang-list"></div>
|
||||
</div>
|
||||
<div class="page-container m-2 lg:my-20 lg:mx-64 flex flex-col gap-4 unfocused">
|
||||
<div class="top-2 inset-x-2 lg:absolute flex flex-row justify-between">
|
||||
<div class="flex flex-row gap-2">
|
||||
{{ template "lang-select.html" . }}
|
||||
<span class="button ~warning h-min" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
||||
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
|
||||
</div>
|
||||
</span>
|
||||
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
||||
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
|
||||
</div>
|
||||
<div class="top-4 right-4 absolute">
|
||||
<a class="button ~info unfocused" href="/" id="admin-back-button"><i class="ri-arrow-left-fill mr-2"></i>{{ .strings.admin }}</a>
|
||||
</div>
|
||||
<div class="page-container unfocused">
|
||||
<a class="button ~info unfocused h-min" href="/" id="admin-back-button"><i class="ri-arrow-left-fill mr-2"></i>{{ .strings.admin }}</a>
|
||||
</div>
|
||||
<div class="card @low dark:~d_neutral mb-4" id="card-user">
|
||||
<span class="heading mb-2"></span>
|
||||
</div>
|
||||
@@ -173,7 +146,7 @@
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ .urlBase }}/js/user.js" type="module"></script>
|
||||
<script src="{{ .pages.Base }}/js/user.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 56 KiB |
0
images/jfa-go-icon.png
Executable file → Normal file
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
0
images/jfa-go-icon.svg
Executable file → Normal file
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 72 KiB |
@@ -1,3 +1,4 @@
|
||||
//go:build !external
|
||||
// +build !external
|
||||
|
||||
package main
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
|
||||
const binaryType = "internal"
|
||||
|
||||
func BuildTagsExternal() {}
|
||||
|
||||
//go:embed data data/html data/web data/web/css data/web/js
|
||||
var loFS embed.FS
|
||||
|
||||
|
||||
@@ -5,20 +5,21 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/jellyseerr"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||
user, imported, err := app.js.GetOrImportUser(jfID)
|
||||
if err != nil {
|
||||
app.debug.Printf("Failed to get or trigger import for Jellyseerr (user \"%s\"): %v", jfID, err)
|
||||
app.debug.Printf(lm.FailedImportUser, lm.Jellyseerr, jfID, err)
|
||||
return
|
||||
}
|
||||
if imported {
|
||||
app.debug.Printf("Jellyseerr: Triggered import for Jellyfin user \"%s\" (ID %d)", jfID, user.ID)
|
||||
app.debug.Printf(lm.ImportJellyseerrUser, jfID, user.ID)
|
||||
}
|
||||
notif, err := app.js.GetNotificationPreferencesByID(user.ID)
|
||||
if err != nil {
|
||||
app.debug.Printf("Failed to get notification prefs for Jellyseerr (user \"%s\"): %v", jfID, err)
|
||||
app.debug.Printf(lm.FailedGetJellyseerrNotificationPrefs, jfID, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -27,7 +28,7 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||
if ok && email.Addr != "" && user.Email != email.Addr {
|
||||
err = app.js.ModifyMainUserSettings(jfID, jellyseerr.MainUserSettings{Email: email.Addr})
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to set Jellyseerr email address: %v\n", err)
|
||||
app.err.Printf(lm.FailedSetEmailAddress, lm.Jellyseerr, jfID, err)
|
||||
} else {
|
||||
contactMethods[jellyseerr.FieldEmailEnabled] = email.Contact
|
||||
}
|
||||
@@ -51,15 +52,15 @@ func (app *appContext) SynchronizeJellyseerrUser(jfID string) {
|
||||
if len(contactMethods) != 0 {
|
||||
err := app.js.ModifyNotifications(jfID, contactMethods)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to sync contact methods with Jellyseerr: %v", err)
|
||||
app.err.Printf(lm.FailedSyncContactMethods, lm.Jellyseerr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) SynchronizeJellyseerrUsers() {
|
||||
users, status, err := app.jf.GetUsers(false)
|
||||
if err != nil || status != 200 {
|
||||
app.err.Printf("Failed to get users (%d): %s", status, err)
|
||||
users, err := app.jf.GetUsers(false)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||
return
|
||||
}
|
||||
// I'm sure Jellyseerr can handle it,
|
||||
@@ -76,6 +77,6 @@ func newJellyseerrDaemon(interval time.Duration, app *appContext) *GenericDaemon
|
||||
app.SynchronizeJellyseerrUsers()
|
||||
},
|
||||
)
|
||||
d.Name("Jellyseerr import daemon")
|
||||
d.Name("Jellyseerr import")
|
||||
return d
|
||||
}
|
||||
@@ -4,4 +4,6 @@ replace github.com/hrfee/jfa-go/common => ../common
|
||||
|
||||
go 1.18
|
||||
|
||||
require github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769 // indirect
|
||||
require github.com/hrfee/jfa-go/common v0.0.0-20240728190513-dabef831d769
|
||||
|
||||
require github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a // indirect
|
||||
|
||||
2
jellyseerr/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a h1:qbXZgCqb9eaPSJfLEXczQD2lxTv6jb6silMPIWW9j6o=
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a/go.mod h1:c5HKkLayo0GrEUDlJwT12b67BL9cdPjP271Xlv/KDRQ=
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
co "github.com/hrfee/jfa-go/common"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,13 +28,13 @@ type Jellyseerr struct {
|
||||
userCache map[string]User // Map of jellyfin IDs to users
|
||||
cacheExpiry time.Time
|
||||
cacheLength time.Duration
|
||||
timeoutHandler common.TimeoutHandler
|
||||
timeoutHandler co.TimeoutHandler
|
||||
LogRequestBodies bool
|
||||
AutoImportUsers bool
|
||||
}
|
||||
|
||||
// NewJellyseerr returns an Ombi object.
|
||||
func NewJellyseerr(server, key string, timeoutHandler common.TimeoutHandler) *Jellyseerr {
|
||||
func NewJellyseerr(server, key string, timeoutHandler co.TimeoutHandler) *Jellyseerr {
|
||||
if !strings.HasSuffix(server, API_SUFFIX) {
|
||||
server = server + API_SUFFIX
|
||||
}
|
||||
@@ -55,6 +55,11 @@ func NewJellyseerr(server, key string, timeoutHandler common.TimeoutHandler) *Je
|
||||
}
|
||||
}
|
||||
|
||||
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
||||
func (js *Jellyseerr) SetTransport(t *http.Transport) {
|
||||
js.httpClient.Transport = t
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) req(mode string, uri string, data any, queryParams url.Values, headers map[string]string, response bool) (string, int, error) {
|
||||
var params []byte
|
||||
if data != nil {
|
||||
@@ -82,25 +87,23 @@ func (js *Jellyseerr) req(mode string, uri string, data any, queryParams url.Val
|
||||
}
|
||||
}
|
||||
resp, err := js.httpClient.Do(req)
|
||||
reqFailed := err != nil || !(resp.StatusCode == 200 || resp.StatusCode == 201)
|
||||
err = co.GenericErrFromResponse(resp, err)
|
||||
defer js.timeoutHandler()
|
||||
var responseText string
|
||||
defer resp.Body.Close()
|
||||
if response || reqFailed {
|
||||
if response || err != nil {
|
||||
responseText, err = js.decodeResp(resp)
|
||||
if err != nil {
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
}
|
||||
if reqFailed {
|
||||
if err != nil {
|
||||
var msg ErrorDTO
|
||||
err = json.Unmarshal([]byte(responseText), &msg)
|
||||
if err != nil {
|
||||
return responseText, resp.StatusCode, err
|
||||
}
|
||||
if msg.Message == "" {
|
||||
err = fmt.Errorf("failed (error %d)", resp.StatusCode)
|
||||
} else {
|
||||
if msg.Message != "" {
|
||||
err = fmt.Errorf("got %d: %s", resp.StatusCode, msg.Message)
|
||||
}
|
||||
return responseText, resp.StatusCode, err
|
||||
@@ -145,14 +148,11 @@ func (js *Jellyseerr) ImportFromJellyfin(jfIDs ...string) ([]User, error) {
|
||||
params := map[string]interface{}{
|
||||
"jellyfinUserIds": jfIDs,
|
||||
}
|
||||
resp, status, err := js.post(js.server+"/user/import-from-jellyfin", params, true)
|
||||
resp, _, err := js.post(js.server+"/user/import-from-jellyfin", params, true)
|
||||
var data []User
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
err = json.Unmarshal([]byte(resp), &data)
|
||||
for _, u := range data {
|
||||
if u.JellyfinUserID != "" {
|
||||
@@ -197,15 +197,11 @@ func (js *Jellyseerr) getUserPage(page int) (GetUsersDTO, error) {
|
||||
if js.LogRequestBodies {
|
||||
fmt.Printf("Jellyseerr API Client: Sending with URL params \"%+v\"\n", params)
|
||||
}
|
||||
resp, status, err := js.get(js.server+"/user", nil, params)
|
||||
resp, _, err := js.get(js.server+"/user", nil, params)
|
||||
var data GetUsersDTO
|
||||
if status != 200 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
if err == nil {
|
||||
err = json.Unmarshal([]byte(resp), &data)
|
||||
}
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
err = json.Unmarshal([]byte(resp), &data)
|
||||
return data, err
|
||||
}
|
||||
|
||||
@@ -261,12 +257,9 @@ func (js *Jellyseerr) getUser(jfID string) (User, error) {
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) Me() (User, error) {
|
||||
resp, status, err := js.get(js.server+"/auth/me", nil, url.Values{})
|
||||
resp, _, err := js.get(js.server+"/auth/me", nil, url.Values{})
|
||||
var data User
|
||||
data.ID = -1
|
||||
if status != 200 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
@@ -281,13 +274,10 @@ func (js *Jellyseerr) GetPermissions(jfID string) (Permissions, error) {
|
||||
return data.Permissions, err
|
||||
}
|
||||
|
||||
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
|
||||
resp, _, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), nil, url.Values{})
|
||||
if err != nil {
|
||||
return data.Permissions, err
|
||||
}
|
||||
if status != 200 {
|
||||
return data.Permissions, fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
err = json.Unmarshal([]byte(resp), &data)
|
||||
return data.Permissions, err
|
||||
}
|
||||
@@ -298,13 +288,10 @@ func (js *Jellyseerr) SetPermissions(jfID string, perm Permissions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), permissionsDTO{Permissions: perm}, false)
|
||||
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/permissions", u.ID), permissionsDTO{Permissions: perm}, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
u.Permissions = perm
|
||||
js.userCache[jfID] = u
|
||||
return nil
|
||||
@@ -316,13 +303,10 @@ func (js *Jellyseerr) ApplyTemplateToUser(jfID string, tmpl UserTemplate) error
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), tmpl, false)
|
||||
_, _, err = js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), tmpl, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
u.UserTemplate = tmpl
|
||||
js.userCache[jfID] = u
|
||||
return nil
|
||||
@@ -337,13 +321,10 @@ func (js *Jellyseerr) ModifyUser(jfID string, conf map[UserField]any) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), conf, false)
|
||||
_, _, err = js.put(fmt.Sprintf(js.server+"/user/%d", u.ID), conf, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
// Lazily just invalidate the cache.
|
||||
js.cacheExpiry = time.Now()
|
||||
return nil
|
||||
@@ -355,10 +336,7 @@ func (js *Jellyseerr) DeleteUser(jfID string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := js.delete(fmt.Sprintf(js.server+"/user/%d", u.ID), nil)
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
_, err = js.delete(fmt.Sprintf(js.server+"/user/%d", u.ID), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -376,13 +354,10 @@ func (js *Jellyseerr) GetNotificationPreferences(jfID string) (Notifications, er
|
||||
|
||||
func (js *Jellyseerr) GetNotificationPreferencesByID(jellyseerrID int64) (Notifications, error) {
|
||||
var data Notifications
|
||||
resp, status, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
|
||||
resp, _, err := js.get(fmt.Sprintf(js.server+"/user/%d/settings/notifications", jellyseerrID), nil, url.Values{})
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
if status != 200 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
err = json.Unmarshal([]byte(resp), &data)
|
||||
return data, err
|
||||
}
|
||||
@@ -397,13 +372,10 @@ func (js *Jellyseerr) ApplyNotificationsTemplateToUser(jfID string, tmpl Notific
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), tmpl, false)
|
||||
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), tmpl, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -413,13 +385,10 @@ func (js *Jellyseerr) ModifyNotifications(jfID string, conf map[NotificationsFie
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), conf, false)
|
||||
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/notifications", u.ID), conf, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -429,11 +398,8 @@ func (js *Jellyseerr) GetUsers() (map[string]User, error) {
|
||||
}
|
||||
|
||||
func (js *Jellyseerr) UserByID(jellyseerrID int64) (User, error) {
|
||||
resp, status, err := js.get(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
|
||||
resp, _, err := js.get(js.server+fmt.Sprintf("/user/%d", jellyseerrID), nil, url.Values{})
|
||||
var data User
|
||||
if status != 200 {
|
||||
return data, fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
@@ -447,13 +413,10 @@ func (js *Jellyseerr) ModifyMainUserSettings(jfID string, conf MainUserSettings)
|
||||
return err
|
||||
}
|
||||
|
||||
_, status, err := js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
|
||||
_, _, err = js.post(fmt.Sprintf(js.server+"/user/%d/settings/main", u.ID), conf, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != 200 && status != 201 {
|
||||
return fmt.Errorf("failed (error %d)", status)
|
||||
}
|
||||
// Lazily just invalidate the cache.
|
||||
js.cacheExpiry = time.Now()
|
||||
return nil
|
||||
|
||||
39
lang.go
@@ -1,5 +1,7 @@
|
||||
package main
|
||||
|
||||
import "github.com/hrfee/jfa-go/common"
|
||||
|
||||
type langMeta struct {
|
||||
Name string `json:"name"`
|
||||
// Language to fall back on if strings are missing. Defaults to en-us.
|
||||
@@ -13,11 +15,11 @@ type quantityString struct {
|
||||
|
||||
type adminLangs map[string]adminLang
|
||||
|
||||
func (ls *adminLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
func (ls *adminLangs) getOptions() []common.Option {
|
||||
opts := make([]common.Option, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
opts[i] = common.Option{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
@@ -42,11 +44,11 @@ type adminLang struct {
|
||||
|
||||
type userLangs map[string]userLang
|
||||
|
||||
func (ls *userLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
func (ls *userLangs) getOptions() []common.Option {
|
||||
opts := make([]common.Option, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
opts[i] = common.Option{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
@@ -65,11 +67,11 @@ type userLang struct {
|
||||
|
||||
type pwrLangs map[string]pwrLang
|
||||
|
||||
func (ls *pwrLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
func (ls *pwrLangs) getOptions() []common.Option {
|
||||
opts := make([]common.Option, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
opts[i] = common.Option{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
@@ -82,11 +84,11 @@ type pwrLang struct {
|
||||
|
||||
type emailLangs map[string]emailLang
|
||||
|
||||
func (ls *emailLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
func (ls *emailLangs) getOptions() []common.Option {
|
||||
opts := make([]common.Option, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
opts[i] = common.Option{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
@@ -122,6 +124,7 @@ type setupLang struct {
|
||||
Login langSection `json:"login"`
|
||||
JellyfinEmby langSection `json:"jellyfinEmby"`
|
||||
Ombi langSection `json:"ombi"`
|
||||
Jellyseerr langSection `json:"jellyseerr"`
|
||||
Email langSection `json:"email"`
|
||||
Messages langSection `json:"messages"`
|
||||
Notifications langSection `json:"notifications"`
|
||||
@@ -134,11 +137,11 @@ type setupLang struct {
|
||||
JSON string
|
||||
}
|
||||
|
||||
func (ls *setupLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
func (ls *setupLangs) getOptions() []common.Option {
|
||||
opts := make([]common.Option, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
opts[i] = common.Option{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
@@ -151,11 +154,11 @@ type telegramLang struct {
|
||||
Strings langSection `json:"strings"`
|
||||
}
|
||||
|
||||
func (ts *telegramLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ts))
|
||||
func (ts *telegramLangs) getOptions() []common.Option {
|
||||
opts := make([]common.Option, len(*ts))
|
||||
i := 0
|
||||
for key, lang := range *ts {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
opts[i] = common.Option{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
"postSignupCardDescription": "Card shown to user after signing up. Overrides \"Success Message\". Overriden by \"Auto redirect on success\" setting.",
|
||||
"buildTime": "Build Time",
|
||||
"builtBy": "Built By",
|
||||
"buildTags": "Build Tags",
|
||||
"loginNotAdmin": "Not an Admin?",
|
||||
"referrer": "Referrer",
|
||||
"accountLinked": "{contactMethod} linked: {user}",
|
||||
@@ -201,7 +202,8 @@
|
||||
"backupCreated": "Backup created",
|
||||
"backupCanBeFound": "The backup can be found on the server at {filepath}.",
|
||||
"backupCanDownload": "Alternatively, click below to download the backup.",
|
||||
"wikiPage": "Wiki Page"
|
||||
"wikiPage": "Wiki Page",
|
||||
"wiki": "Wiki"
|
||||
},
|
||||
"notifications": {
|
||||
"pathCopied": "Full path copied to clipboard.",
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"errorConnection": "Couldn't connect to jfa-go.",
|
||||
"errorUnknown": "Unknown error.",
|
||||
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
||||
"errorSaveSettings": "Couldn't save settings."
|
||||
"errorSaveSettings": "Couldn't save settings.",
|
||||
"errorSpecialSymbols": "Field cannot contain special symbols."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"year": {
|
||||
@@ -64,4 +65,4 @@
|
||||
"plural": "{n} Days"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
88
lang/form/ckb-iq.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "کوردی سۆرانی"
|
||||
},
|
||||
"strings": {
|
||||
"pageTitle": "دروستکردنی هەژماری جێڵیفن",
|
||||
"createAccountHeader": "دروستکردنی هەژمار",
|
||||
"accountDetails": "زانیارییەکان",
|
||||
"emailAddress": "ئیمەیل",
|
||||
"username": "ناوی بەکارهێنەر",
|
||||
"oldPassword": "وشەی نهێنی کۆن",
|
||||
"newPassword": "وشەی نهێنی نوێ",
|
||||
"password": "وشەی نهێنی",
|
||||
"reEnterPassword": "دووبارە وشەی نهێنی بنووسەوە",
|
||||
"reEnterPasswordInvalid": "وشە نهێنییەکان یەک ناگرن.",
|
||||
"createAccountButton": "هەژمار دروستبکە",
|
||||
"passwordRequirementsHeader": "داواکارییەکانی وشەی نهێنی",
|
||||
"successHeader": "سەرکەوت!",
|
||||
"confirmationRequired": "دووپاتکردنەوەی ئیمەیل داواکراوە",
|
||||
"confirmationRequiredMessage": "تکایە سەیری نامەکانی ئیمەیلەکەت بکە بۆ دووپاتکردنەوەی ناونیشانەکەت.",
|
||||
"yourAccountIsValidUntil": "هەژمارەکەت تاکو {date} کاردەکات.",
|
||||
"sendPIN": "ئەم ژمارە نهێنییەی خوارەوە بۆ بۆتەکە بنێرە، پاشان وەرەوە بۆ پەیوەستکردنی هەژمارەکەت.",
|
||||
"sendPINDiscord": "{command} لە چەناڵی {server_channel}ی دیسکۆردەکەت بنوسە، پاشان ئەم ژمارە نهێنییەی خوارەوە بنێرە.",
|
||||
"matrixEnterUser": "",
|
||||
"welcomeUser": "{user}، بەخێربێیت!",
|
||||
"addContactMethod": "",
|
||||
"editContactMethod": "",
|
||||
"joinTheServer": "",
|
||||
"customMessagePlaceholderHeader": "",
|
||||
"customMessagePlaceholderContent": "",
|
||||
"userPageSuccessMessage": "",
|
||||
"resetPassword": "",
|
||||
"resetPasswordThroughJellyfin": "",
|
||||
"resetPasswordThroughLink": "",
|
||||
"resetPasswordThroughLinkStart": "",
|
||||
"resetPasswordThroughLinkEnd": "",
|
||||
"resetPasswordUsername": "",
|
||||
"resetPasswordEmail": "",
|
||||
"resetPasswordContactMethod": "",
|
||||
"resetSent": "",
|
||||
"resetSentDescription": "",
|
||||
"changePassword": "",
|
||||
"referralsDescription": "",
|
||||
"referralsWithExpiryDescription": "",
|
||||
"copyReferral": "",
|
||||
"invitedBy": ""
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "",
|
||||
"errorInvalidCode": "",
|
||||
"errorAccountLinked": "",
|
||||
"errorEmailLinked": "",
|
||||
"errorTelegramVerification": "",
|
||||
"errorDiscordVerification": "",
|
||||
"errorMatrixVerification": "",
|
||||
"errorInvalidPIN": "",
|
||||
"errorUnknown": "",
|
||||
"errorNoEmail": "",
|
||||
"errorCaptcha": "",
|
||||
"errorPassword": "",
|
||||
"errorNoMatch": "",
|
||||
"errorOldPassword": "",
|
||||
"passwordChanged": "",
|
||||
"verified": ""
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"uppercase": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"lowercase": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"number": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"special": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
"confirmationRequired": "E-Mail Bestätigung erforderlich",
|
||||
"confirmationRequiredMessage": "Bitte überprüfe deinen Posteingang und bestätige deine E-Mail-Adresse.",
|
||||
"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.",
|
||||
"sendPIN": "Sende die PIN unten an den Bot und komm dann hierher zurück, um dein Konto zu verknüpfen.",
|
||||
"sendPINDiscord": "Gib auf Discord {command} in {server_channel} ein und sende die untenstehende PIN.",
|
||||
"matrixEnterUser": "Gib deine Benutzer-ID ein und drücke auf Absenden. Anschließend erhälst du ein PIN, die hier eingegeben wird um fortzufahren.",
|
||||
"oldPassword": "Altes Passwort",
|
||||
@@ -30,7 +30,20 @@
|
||||
"joinTheServer": "Server beitreten:",
|
||||
"userPageSuccessMessage": "Du kannst die Details deines Accounts später in {myAccount} Seite einsehen und ändern.",
|
||||
"resetPassword": "Passwort zurücksetzen",
|
||||
"resetPasswordThroughJellyfin": "Um das Passwort zurückzusetzen, besuche {jfLink} und drücke auf die Schaltfläche \"Passwort vergessen\"."
|
||||
"resetPasswordThroughJellyfin": "Um das Passwort zurückzusetzen, besuche {jfLink} und drücke auf die Schaltfläche \"Passwort vergessen\".",
|
||||
"resetPasswordThroughLinkStart": "Um dein Passwort zurückzusetzen, gib eine der folgenden Möglichkeiten ein:",
|
||||
"resetPasswordContactMethod": "Den Benutzernamen einer Kontaktmethode, die mit deinem Konto verknüpft ist",
|
||||
"changePassword": "Passwort ändern",
|
||||
"resetPasswordThroughLink": "Um dein Passwort zurückzusetzen, gib deinen Benutzernamen, deine E-Mail-Adresse oder einer verlinkten Kontaktmethode ein und sende ihn ab. Du erhältst einen Link zum Zurücksetzen deines Passworts.",
|
||||
"resetSent": "Infos zum Zurücksetzen wurden gesendet.",
|
||||
"resetSentDescription": "Wenn ein Konto mit dem angegebenen Benutzernamen/der angegebenen Kontaktmethode existiert, wurde ein Link zum Zurücksetzen des Passworts über alle verfügbaren Kontaktmethoden verschickt. Der Code ist 30 Minuten gültig.",
|
||||
"referralsDescription": "Lade Freunde & Familie mit diesem Link zu Jellyfin ein. Wenn er abläuft, kannst du hier einen neuen Link anfordern.",
|
||||
"copyReferral": "Link kopieren",
|
||||
"invitedBy": "Du wurdest von {user} eingeladen.",
|
||||
"resetPasswordThroughLinkEnd": "Drücke dann auf Abschicken. Du erhältst einen Link, mit dem du dein Passwort zurücksetzen kannst.",
|
||||
"resetPasswordUsername": "Dein Jellyfin-Benutzername",
|
||||
"resetPasswordEmail": "Deine E-Mail Adresse",
|
||||
"referralsWithExpiryDescription": "Lade Freunde & Familie mit diesem Link zu Jellyfin ein. Der Link wird deaktiviert, sobald er abläuft."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
@@ -67,6 +80,10 @@
|
||||
"errorNoEmail": "E-Mail Adresse erforderlich.",
|
||||
"errorCaptcha": "Captcha falsch.",
|
||||
"errorPassword": "Prüfe die Passwortanforderungen.",
|
||||
"errorNoMatch": "Passwörter stimmen nicht überein."
|
||||
"errorNoMatch": "Passwörter stimmen nicht überein.",
|
||||
"errorAccountLinked": "Konto wird bereits verwendet.",
|
||||
"errorEmailLinked": "E-Mail wird bereits verwendet.",
|
||||
"passwordChanged": "Das Passwort wurde geändert.",
|
||||
"errorOldPassword": "Das alte Passwort ist falsch."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"endPage": {
|
||||
"finished": "Finished!",
|
||||
"restartMessage": "Features like Discord/Telegram/Matrix bots, custom Markdown messages, and a user-accessible \"My Account\" page can be found in Settings, so make sure to give it a browse. Click below to restart, then refresh the page.",
|
||||
"urlChangedNotice": "If you've changed the host, port, subfolder etc. that jfa-go is hosted on, check the URL is right.",
|
||||
"refreshPage": "Refresh"
|
||||
},
|
||||
"language": {
|
||||
@@ -47,7 +48,9 @@
|
||||
"title": "General",
|
||||
"listenAddress": "Listen Address",
|
||||
"urlBase": "URL Base",
|
||||
"urlBaseNotice": "Only needed if using a reverse proxy on a subdomain (e.g 'jellyf.in/accounts').",
|
||||
"urlBaseNotice": "Only needed if using a reverse proxy on a subfolder (e.g 'jellyf.in/accounts').",
|
||||
"externalURL": "External jfa-go URL",
|
||||
"externalURLNotice": "The URL you'll be accessing jfa-go from. Used to generate links for things like password resets. Make sure to include the above URL base if you set one.",
|
||||
"lightTheme": "Light",
|
||||
"darkTheme": "Dark",
|
||||
"useHTTPS": "Use HTTPS",
|
||||
@@ -94,7 +97,14 @@
|
||||
"ombi": {
|
||||
"title": "Ombi",
|
||||
"description": "By connecting to Ombi, both a Jellyfin and Ombi account will be created when a user joins through jfa-go. After setup is finished, go to Settings to set a default profile for new ombi users.",
|
||||
"apiKeyNotice": "Find this in the first tab of Ombi settings."
|
||||
"apiKeyNotice": "Find this in the first tab of Ombi settings.",
|
||||
"stabilityWarning": "Warning: Ombi integration is unstable, and can cause issues. Jellyseerr is recommended instead. See {n} for more info."
|
||||
},
|
||||
"jellyseerr": {
|
||||
"title": "Jellyseerr",
|
||||
"description": "Jellyseerr is an alternative to Ombi, and integrates with jfa-go slightly better. Again, after setup is finished, go to Settings to create a profile and add a template for new Jellyseerr accounts.",
|
||||
"importExisting": "Import existing users",
|
||||
"importExistingDescription": "If enabled, your existing users will have contact details and preferences from jfa-go synchronized."
|
||||
},
|
||||
"messages": {
|
||||
"title": "Messages",
|
||||
@@ -134,6 +144,7 @@
|
||||
"passwordResets": {
|
||||
"title": "Password Resets",
|
||||
"description": "When a user tries to reset their password, Jellyfin creates a file named 'passwordreset-*.json' which contains a PIN. jfa-go reads the file and sends the PIN to the user. If you enabled the \"User Page\" feature, a reset can also be performed there, given a username, email, or contact method.",
|
||||
"moreInfo": "More information about the different ways of resetting passwords can be found on {n}.",
|
||||
"pathToJellyfin": "Path to Jellyfin configuration directory",
|
||||
"pathToJellyfinNotice": "If you don't know where this is, try resetting your password in Jellyfin. A popup with '<path to jellyfin>/passwordreset-*.json' will appear. This is not necessary if you only want to use self-service password resets through the \"User Page\".",
|
||||
"resetLinks": "Send a link instead of a PIN",
|
||||
@@ -155,6 +166,7 @@
|
||||
"helpMessages": {
|
||||
"title": "Help Messages",
|
||||
"description": "These messages will display in the account creation page and in some emails.",
|
||||
"markdownMessageNotice": "Contents of some emails, pages and messages can be customized further with markdown in Settings.",
|
||||
"contactMessage": "Contact Message",
|
||||
"contactMessageNotice": "Displays at the bottom of all pages except admin.",
|
||||
"helpMessage": "Help Message",
|
||||
|
||||
@@ -52,6 +52,14 @@ func lshortfile() string {
|
||||
return Lshortfile(3)
|
||||
}
|
||||
|
||||
func LshortfileTree() string {
|
||||
out := ""
|
||||
for i := 6; i >= 0; i-- {
|
||||
out += strconv.Itoa(i) + ":" + Lshortfile(i) + " "
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func NewLogger(out io.Writer, prefix string, flag int, color c.Attribute) (l *Logger) {
|
||||
l = &Logger{}
|
||||
// Use reimplemented Lshortfile since wrapping the log functions messes them up
|
||||
@@ -84,6 +92,25 @@ func (l *Logger) Printf(format string, v ...interface{}) {
|
||||
l.logger.Print(out)
|
||||
}
|
||||
|
||||
func (l *Logger) PrintfCustomLevel(level int, format string, v ...interface{}) {
|
||||
if l.empty {
|
||||
return
|
||||
}
|
||||
var out string
|
||||
if l.shortfile {
|
||||
out = Lshortfile(level)
|
||||
}
|
||||
out += " " + l.printer.Sprintf(format, v...)
|
||||
l.logger.Print(out)
|
||||
}
|
||||
|
||||
func (l *Logger) PrintfNoFile(format string, v ...interface{}) {
|
||||
if l.empty {
|
||||
return
|
||||
}
|
||||
l.logger.Print(l.printer.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (l *Logger) Print(v ...interface{}) {
|
||||
if l.empty {
|
||||
return
|
||||
|
||||
3
logmessages/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/hrfee/jfa-go/logmessages
|
||||
|
||||
go 1.18
|
||||
389
logmessages/logmessages.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package logmessages
|
||||
|
||||
/* Log strings for (almost) all the program.
|
||||
* Helps avoid writing redundant, slightly different
|
||||
* strings constantly.
|
||||
* Also would help if I were to ever set up translation
|
||||
* for logs. Mostly split by file, but obviously there's
|
||||
* re-use, and occasionally related stuff is grouped.
|
||||
*/
|
||||
const (
|
||||
Jellyseerr = "Jellyseerr"
|
||||
Jellyfin = "Jellyfin"
|
||||
Ombi = "Ombi"
|
||||
Discord = "Discord"
|
||||
Telegram = "Telegram"
|
||||
Matrix = "Matrix"
|
||||
Email = "Email"
|
||||
|
||||
// main.go
|
||||
FailedLogging = "Failed to start log wrapper: %v\n"
|
||||
|
||||
NoConfig = "Couldn't find default config file"
|
||||
Write = "Wrote to \"%s\""
|
||||
FailedWriting = "Failed to write to \"%s\": %v"
|
||||
FailedCreateDir = "Failed to create directory \"%s\": %v"
|
||||
FailedReading = "Failed to read from \"%s\": %v"
|
||||
FailedOpen = "Failed to open \"%s\": %v"
|
||||
FailedStat = "Failed to stat \"%s\": %v"
|
||||
PathNotFound = "Path \"%s\" not found"
|
||||
|
||||
CopyConfig = "Copied default configuration to \"%s\""
|
||||
FailedCopyConfig = "Failed to copy default configuration to \"%s\": %v"
|
||||
LoadConfig = "Loaded config file \"%s\""
|
||||
FailedLoadConfig = "Failed to load config file \"%s\": %v"
|
||||
ModifyConfig = "Config saved to \"%s\""
|
||||
FailedModifyConfig = "Failed to modify config file \"%s\": %v"
|
||||
|
||||
SocketPath = "Socket Path: \"%s\""
|
||||
FailedSocketConnect = "Couldn't establish socket connection at \"%s\": %v"
|
||||
SocketCheckRunning = "Make sure jfa-go is running."
|
||||
FailedSocketRead = "Couldn't read message on socket \"%s\": %v"
|
||||
SocketWrite = "Command sent."
|
||||
FailedSocketWrite = "Coudln't write message on socket \"%s\": %v"
|
||||
|
||||
FailedLangLoad = "Failed to load language files: %v"
|
||||
|
||||
UsingTLS = "Using TLS/HTTP2"
|
||||
|
||||
UsingOmbi = "Starting " + Ombi + " client"
|
||||
UsingJellyseerr = "Starting " + Jellyseerr + " client"
|
||||
UsingEmby = "Using Emby server type (EXPERIMENTAL: PWRs are not available, and support is limited.)"
|
||||
UsingJellyfin = "Using " + Jellyfin + " server type"
|
||||
UsingJellyfinAuth = "Using " + Jellyfin + " for authentication"
|
||||
UsingLocalAuth = "Using local username/pw authentication (NOT RECOMMENDED)"
|
||||
|
||||
AuthJellyfin = "Authenticated with " + Jellyfin + " @ \"%s\""
|
||||
AsUser = "As user \"%s\""
|
||||
FailedAuthJellyfin = "Failed to authenticate with " + Jellyfin + " @ \"%s\" (code %d): %v"
|
||||
FailedAuth = "Failed to authenticate with %s @ \"%s\" (code %d): %v"
|
||||
|
||||
Unauthorized = "unauthorized, check credentials"
|
||||
Forbidden = "forbidden, the user may not have correct permissions"
|
||||
NotFound = "not found"
|
||||
TimedOut = "timed out"
|
||||
FailedGenericWithCode = "failed (code %d)"
|
||||
|
||||
InitDiscord = "Initialized Discord daemon"
|
||||
FailedInitDiscord = "Failed to initialize Discord daemon: %v"
|
||||
InitTelegram = "Initialized Telegram daemon"
|
||||
FailedInitTelegram = "Failed to initialize Telegram daemon: %v"
|
||||
InitMatrix = "Initialized Matrix daemon"
|
||||
FailedInitMatrix = "Failed to initialize Matrix daemon: %v"
|
||||
|
||||
InitRouter = "Initializing router"
|
||||
LoadRoutes = "Loading Routes"
|
||||
|
||||
LoadingSetup = "Loading setup @ \"%s\""
|
||||
ServingSetup = "Loaded, visit \"%s\" to start."
|
||||
|
||||
InvalidSSLCert = "Failed loading SSL Certificate \"%s\": %v"
|
||||
InvalidSSLKey = "Failed loading SSL Keyfile \"%s\": %v"
|
||||
|
||||
FailServeSSL = "Failure serving with SSL/TLS: %v"
|
||||
FailServe = "Failure serving: %v"
|
||||
|
||||
Serving = "Loaded @ \"%s\""
|
||||
|
||||
QuitReceived = "Restart/Quit signal received, please be patient."
|
||||
Quitting = "Shutting down..."
|
||||
Restarting = "Restarting..."
|
||||
FailedHardRestartWindows = "hard restarts not available on windows"
|
||||
Quit = "Server shut down."
|
||||
FailedQuit = "Server shutdown failed: %v"
|
||||
|
||||
// api-activities.go
|
||||
FailedDBReadActivities = "Failed to read activities from DB: %v"
|
||||
|
||||
// api-backups.go
|
||||
IgnoreInvalidFilename = "Invalid filename \"%s\", ignoring: %v"
|
||||
GetUpload = "Retrieved uploaded file \"%s\""
|
||||
FailedGetUpload = "Failed to retrieve file from form data: %v"
|
||||
|
||||
// api-invites.go
|
||||
DeleteOldInvite = "Deleting old invite \"%s\""
|
||||
DeleteInvite = "Deleting invite \"%s\""
|
||||
FailedDeleteInvite = "Failed to delete invite \"%s\": %v"
|
||||
GenerateInvite = "Generating new invite"
|
||||
FailedGenerateInvite = "Failed to generate new invite: %v"
|
||||
InvalidInviteCode = "Invalid invite code \"%s\""
|
||||
|
||||
FailedSendToTooltipNoUser = "Failed: \"%s\" not found"
|
||||
FailedSendToTooltipMultiUser = "Failed: \"%s\" linked to multiple users"
|
||||
|
||||
FailedParseTime = "Failed to parse time value: %v"
|
||||
|
||||
FailedGetContactMethod = "Failed to get contact method for \"%s\", make sure one is set."
|
||||
|
||||
SetAdminNotify = "Set \"%s\" to %t for admin address \"%s\""
|
||||
|
||||
// *jellyseerr*.go
|
||||
FailedGetUsers = "Failed to get user(s) from %s: %v"
|
||||
// FIXME: Once done, look back at uses of FailedGetUsers for places where this would make more sense.
|
||||
FailedGetUser = "Failed to get user \"%s\" from %s: %v"
|
||||
FailedGetJellyseerrNotificationPrefs = "Failed to get user \"%s\"'s notification prefs from " + Jellyseerr + ": %v"
|
||||
FailedSyncContactMethods = "Failed to sync contact methods with %s: %v"
|
||||
ImportJellyseerrUser = "Triggered import for " + Jellyseerr + " user \"%s\" (New ID: %d)"
|
||||
FailedImportUser = "Failed to get or trigger import for %s user \"%s\": %v"
|
||||
|
||||
// api-messages.go
|
||||
FailedGetCustomMessage = "Failed to get custom message \"%s\""
|
||||
SetContactPrefForService = "Set contact preference for %s (\"%s\"): %t"
|
||||
|
||||
// Matrix
|
||||
InvalidPIN = "Invalid PIN \"%s\""
|
||||
ExpiredPIN = "Expired PIN \"%s\""
|
||||
InvalidPassword = "Invalid Password"
|
||||
UnauthorizedPIN = "Unauthorized PIN \"%s\""
|
||||
FailedCreateRoom = "Failed to create room: %v"
|
||||
|
||||
// api-profiles.go
|
||||
SetDefaultProfile = "Setting default profile to \"%s\""
|
||||
|
||||
FailedApplyProfile = "Failed to apply profile for %s user \"%s\": %v"
|
||||
ApplyProfile = "Applying settings from profile \"%s\""
|
||||
FailedGetProfile = "Failed to find profile \"%s\""
|
||||
FailedApplyTemplate = "Failed to apply %s template for %s user \"%s\": %v"
|
||||
FallbackToDefault = ", using default"
|
||||
CreateProfileFromUser = "Creating profile from user \"%s\""
|
||||
FailedGetJellyfinDisplayPrefs = "Failed to get DisplayPreferences for user \"%s\" from " + Jellyfin + ": %v"
|
||||
ProfileNoHomescreen = "No homescreen template in profile \"%s\""
|
||||
Profile = "profile"
|
||||
Lang = "language"
|
||||
User = "user"
|
||||
ApplyingTemplatesFrom = "Applying templates from %s: \"%s\" to %d users"
|
||||
DelayingRequests = "Delay will be added between requests (count = %d)"
|
||||
|
||||
// api-userpage.go
|
||||
EmailConfirmationRequired = "User \"%s\" requires email confirmation"
|
||||
|
||||
ChangePassword = "Changed password for %s user \"%s\""
|
||||
FailedChangePassword = "Failed to change password for %s user \"%s\": %v"
|
||||
|
||||
GetReferralTemplate = "Found referral template \"%s\""
|
||||
FailedGetReferralTemplate = "Failed to find referral template \"%s\": %v"
|
||||
DeleteOldReferral = "Deleting old referral \"%s\""
|
||||
RenewOldReferral = "Renewing old referral \"%s\""
|
||||
|
||||
// api-users.go
|
||||
CreateUser = "Created %s user \"%s\""
|
||||
FailedCreateUser = "Failed to create new %s user \"%s\": %v"
|
||||
LinkUser = "Linked %s user \"%s\""
|
||||
FailedLinkUser = "Failed to link %s user \"%s\" with \"%s\": %v"
|
||||
DeleteUser = "Deleted %s user \"%s\""
|
||||
FailedDeleteUser = "Failed to delete %s user \"%s\": %v"
|
||||
FailedDeleteUsers = "Failed to delete %s user(s): %v"
|
||||
UserExists = "user already exists"
|
||||
AccountLinked = "account already linked and require_unique enabled"
|
||||
AccountUnverified = "unverified"
|
||||
FailedSetDiscordMemberRole = "Failed to apply/remove " + Discord + " member role: %v"
|
||||
InvalidChar = "Invalid character '%c'"
|
||||
|
||||
FailedSetEmailAddress = "Failed to set email address for %s user \"%s\": %v"
|
||||
|
||||
AdditionalErrors = "Additional errors from %s: %v"
|
||||
|
||||
IncorrectCaptcha = "captcha incorrect"
|
||||
|
||||
ExtendCreateExpiry = "Extended or created expiry for user \"%s\""
|
||||
|
||||
UserEmailAdjusted = "Email for user \"%s\" adjusted"
|
||||
UserAdminAdjusted = "Admin state for user \"%s\" set to %t"
|
||||
UserLabelAdjusted = "Label for user \"%s\" set to \"%s\""
|
||||
|
||||
// api.go
|
||||
ApplyUpdate = "Applied update"
|
||||
FailedApplyUpdate = "Failed to apply update: %v"
|
||||
UpdateManual = "update is manual"
|
||||
|
||||
// backups.go
|
||||
DeleteOldBackup = "Deleted old backup \"%s\""
|
||||
FailedDeleteOldBackup = "Failed to delete old backup \"%s\": %v"
|
||||
CreateBackup = "Created database backup \"%+v\""
|
||||
FailedParseBackup = "Failed to parse backup \"%s\": %v"
|
||||
FailedCreateBackup = "Faled to create database backup: %v"
|
||||
MoveOldDB = "Moved existing database to \"%s\""
|
||||
FailedMoveOldDB = "Failed to move existing database to \"%s\": %v"
|
||||
RestoreDB = "Restored database from \"%s\""
|
||||
FailedRestoreDB = "Failed to resotre database from \"%s\": %v"
|
||||
|
||||
// config.go
|
||||
EnableAllPWRMethods = "No PWR method preferences set in [user_page], all will be enabled"
|
||||
InitProxy = "Initialized proxy @ \"%s\""
|
||||
FailedInitProxy = "Failed to initialize proxy @ \"%s\": %v\nStartup will pause for a bit to grab your attention."
|
||||
NoURLSuffix = `Warning: Given "jfa_url"/"External jfa-go URL" value does not include "url_base" value!`
|
||||
BadURLBase = `Warning: Given reverse proxy subfolder "%s" may conflict with the applications subpaths.`
|
||||
NoExternalHost = `No "External jfa-go URL" provided, set one in Settings > General.`
|
||||
LoginWontSave = ` Your login won't save until you do.`
|
||||
SubpathBlockMessage = `URLs: Root subfolder = "%s", Admin = "%s", My Account = "%s", Invite forms = "%s"`
|
||||
|
||||
// discord.go
|
||||
StartDaemon = "Started %s daemon"
|
||||
FailedStartDaemon = "Failed to start %s daemon: %v"
|
||||
FailedGetDiscordGuildMembers = "Failed to get " + Discord + " guild members: %v"
|
||||
FailedGetDiscordGuild = "Failed to get " + Discord + " guild: %v"
|
||||
FailedGetDiscordRoles = "Failed to get " + Discord + " roles: %v"
|
||||
FailedCreateDiscordInviteChannel = "Failed to create " + Discord + " invite channel: %v"
|
||||
InviteChannelEmpty = "no invite channel set in settings"
|
||||
FailedGetDiscordChannels = "Failed to get " + Discord + " channel(s): %v"
|
||||
FailedGetDiscordChannel = "Failed to get " + Discord + " channel \"%s\": %v"
|
||||
MonitorAllDiscordChannels = "Will monitor all " + Discord + " channels"
|
||||
FailedCreateDiscordDMChannel = "Failed to create " + Discord + " private DM channel with \"%s\": %v"
|
||||
RegisterDiscordChoice = "Registered " + Discord + " %s choice \"%s\""
|
||||
FailedRegisterDiscordChoices = "Failed to register " + Discord + " %s choices: %v"
|
||||
FailedDeregDiscordChoice = "Failed to deregister " + Discord + " %s choice \"%s\": %v"
|
||||
RegisterDiscordCommand = "Registered " + Discord + " command \"%s\""
|
||||
FailedRegisterDiscordCommand = "Failed to register " + Discord + " command \"%s\": %v"
|
||||
FailedGetDiscordCommands = "Failed to get " + Discord + " commands: %v"
|
||||
FailedDeregDiscordCommand = "Failed to deregister " + Discord + " command \"%s\": %v"
|
||||
|
||||
FailedReply = "Failed to reply to %s message from \"%s\": %v"
|
||||
FailedMessage = "Failed to send %s message to \"%s\": %v"
|
||||
|
||||
IgnoreOutOfChannelMessage = "Ignoring out-of-channel %s message"
|
||||
|
||||
FailedGenerateDiscordInvite = "Failed to generate " + Discord + " invite: %v"
|
||||
|
||||
// email.go
|
||||
FailedInitSMTP = "Failed to initialize SMTP mailer: %v"
|
||||
FailedGeneratePWRLink = "Failed to generate PWR link: %v"
|
||||
|
||||
// housekeeping-d.go
|
||||
hk = "Housekeeping: "
|
||||
hkcu = hk + "cleaning up "
|
||||
HousekeepingEmail = hkcu + Email + " addresses"
|
||||
HousekeepingDiscord = hkcu + Discord + " IDs"
|
||||
HousekeepingTelegram = hkcu + Telegram + " IDs"
|
||||
HousekeepingMatrix = hkcu + Matrix + " IDs"
|
||||
HousekeepingCaptcha = hkcu + "PWR Captchas"
|
||||
HousekeepingActivity = hkcu + "Activity log"
|
||||
HousekeepingInvites = hkcu + "Invites"
|
||||
ActivityLogTxnTooBig = hk + "Activity log delete transaction was too big, going one-by-one"
|
||||
|
||||
// matrix*.go
|
||||
FailedSyncMatrix = "Failed to sync " + Matrix + " daemon: %v"
|
||||
FailedCreateMatrixRoom = "Failed to create " + Matrix + " room with user \"%s\": %v"
|
||||
MatrixOLMLog = "Matrix/OLM: %v"
|
||||
MatrixOLMTraceLog = "Matrix/OLM [TRACE]:"
|
||||
FailedDecryptMatrixMessage = "Failed to decrypt " + Matrix + " E2EE'd message: %v"
|
||||
FailedEnableMatrixEncryption = "Failed to enable encryption in " + Matrix + " room \"%s\": %v"
|
||||
|
||||
// NOTE: "migrations.go" is the one file where log messages are not part of logmessages/logmessages.go.
|
||||
|
||||
// pwreset.go
|
||||
PWRExpired = "PWR for user \"%s\" already expired @ %s, check system time!"
|
||||
|
||||
// router.go
|
||||
UseDefaultHTML = "Using default HTML \"%s\""
|
||||
UseCustomHTML = "Using custom HTML \"%s\""
|
||||
FailedLoadTemplates = "Failed to load %s templates: %v"
|
||||
Internal = "internal"
|
||||
External = "external"
|
||||
RegisterPprof = "Registered pprof"
|
||||
SwaggerWarning = "Warning: Swagger should not be used on a public instance."
|
||||
|
||||
// storage.go
|
||||
ConnectDB = "Connected to DB \"%s\""
|
||||
FailedConnectDB = "Failed to open/connect to database \"%s\": %v"
|
||||
|
||||
// updater.go
|
||||
NoUpdate = "No new updates available"
|
||||
FoundUpdate = "Found update"
|
||||
FailedGetUpdateTag = "Failed to get latest tag: %v"
|
||||
FailedGetUpdate = "Failed to get update: %v"
|
||||
UpdateTagDetails = "Update/Tag details: %+v"
|
||||
|
||||
// user-auth.go
|
||||
UserPage = "userpage"
|
||||
UserPageRequiresJellyfinAuth = "Jellyfin login must be enabled for user page access."
|
||||
|
||||
// user-d.go
|
||||
CheckUserExpiries = "Checking for user expiry"
|
||||
DeleteExpiryForOldUser = "Deleting expiry for old user \"%s\""
|
||||
DeleteExpiredUser = "Deleting expired user \"%s\""
|
||||
DisableExpiredUser = "Disabling expired user \"%s\""
|
||||
FailedDeleteOrDisableExpiredUser = "Failed to delete/disable expired user \"%s\": %v"
|
||||
|
||||
// views.go
|
||||
FailedServerPush = "Failed to use HTTP/2 Server Push: %v"
|
||||
IgnoreBotPWR = "Ignore PWR magic link visit from bot"
|
||||
ReCAPTCHA = "ReCAPTCHA"
|
||||
FailedGenerateCaptcha = "Failed to generate captcha: %v"
|
||||
CaptchaNotFound = "Captcha \"%s\" not found in invite \"%s\""
|
||||
FailedVerifyReCAPTCHA = "Failed to verify reCAPTCHA: %v"
|
||||
InvalidHostname = "invalid hostname (wanted \"%s\", got \"%s\")"
|
||||
|
||||
// webhooks.go
|
||||
WebhookRequest = "Webhook request send to \"%s\" (%d): %v"
|
||||
)
|
||||
|
||||
const (
|
||||
FailedGetCookies = "Failed to get cookie(s) \"%s\": %v"
|
||||
FailedParseJWT = "Failed to parse JWT: %v"
|
||||
FailedCastJWT = "JWT claims unreadable"
|
||||
InvalidJWT = "JWT was invalidated, of incorrect type or has expired"
|
||||
LocallyInvalidatedJWT = "JWT is listed as invalidated"
|
||||
FailedSignJWT = "Failed to sign JWT: %v"
|
||||
|
||||
RequestingToken = "Token requested (%s)"
|
||||
TokenLoginAttempt = "login attempt"
|
||||
TokenRefresh = "refresh token"
|
||||
UserTokenLoginAttempt = UserPage + " " + TokenLoginAttempt
|
||||
UserTokenRefresh = UserPage + " " + TokenRefresh
|
||||
GenerateToken = "Token generated for user \"%s\""
|
||||
FailedGenerateToken = "Failed to generate token: %v"
|
||||
|
||||
FailedAuthRequest = "Failed to authorize request: %v"
|
||||
InvalidAuthHeader = "invalid auth header"
|
||||
NonAdminToken = "token not for admin use"
|
||||
NonAdminUser = "user \"%s\" not admin"
|
||||
InvalidUserOrPass = "invalid user/pass"
|
||||
EmptyUserOrPass = "invalid user/pass"
|
||||
UserDisabled = "user is disabled"
|
||||
)
|
||||
|
||||
const (
|
||||
FailedConstructExpiryAdmin = "Failed to construct expiry notification for \"%s\": %v"
|
||||
FailedSendExpiryAdmin = "Failed to send expiry notification for \"%s\" to \"%s\": %v"
|
||||
SentExpiryAdmin = "Sent expiry notification for \"%s\" to \"%s\""
|
||||
|
||||
FailedConstructCreationAdmin = "Failed to construct creation notification for \"%s\": %v"
|
||||
FailedSendCreationAdmin = "Failed to send creation notification for \"%s\" to \"%s\": %v"
|
||||
SentCreationAdmin = "Sent creation notification for \"%s\" to \"%s\""
|
||||
|
||||
FailedConstructInviteMessage = "Failed to construct invite message for \"%s\": %v"
|
||||
FailedSendInviteMessage = "Failed to send invite message for \"%s\" to \"%s\": %v"
|
||||
SentInviteMessage = "Sent invite message for \"%s\" to \"%s\""
|
||||
|
||||
FailedConstructConfirmationEmail = "Failed to construct confirmation email for \"%s\": %v"
|
||||
FailedSendConfirmationEmail = "Failed to send confirmation email for \"%s\" to \"%s\": %v"
|
||||
SentConfirmationEmail = "Sent confirmation email for \"%s\" to \"%s\""
|
||||
|
||||
FailedConstructPWRMessage = "Failed to construct PWR message for \"%s\": %v"
|
||||
FailedSendPWRMessage = "Failed to send PWR message for \"%s\" to \"%s\": %v"
|
||||
SentPWRMessage = "Sent PWR message for \"%s\" to \"%s\""
|
||||
|
||||
FailedConstructWelcomeMessage = "Failed to construct welcome message for \"%s\": %v"
|
||||
FailedSendWelcomeMessage = "Failed to send welcome message for \"%s\" to \"%s\": %v"
|
||||
SentWelcomeMessage = "Sent welcome message for \"%s\" to \"%s\""
|
||||
|
||||
FailedConstructEnableDisableMessage = "Failed to construct enable/disable message for \"%s\": %v"
|
||||
FailedSendEnableDisableMessage = "Failed to send enable/disable message for \"%s\" to \"%s\": %v"
|
||||
SentEnableDisableMessage = "Sent enable/disable message for \"%s\" to \"%s\""
|
||||
|
||||
FailedConstructDeletionMessage = "Failed to construct account deletion message for \"%s\": %v"
|
||||
FailedSendDeletionMessage = "Failed to send account deletion message for \"%s\" to \"%s\": %v"
|
||||
SentDeletionMessage = "Sent account deletion message for \"%s\" to \"%s\""
|
||||
|
||||
FailedConstructExpiryAdjustmentMessage = "Failed to construct expiry adjustment message for \"%s\": %v"
|
||||
FailedSendExpiryAdjustmentMessage = "Failed to send expiry adjustment message for \"%s\" to \"%s\": %v"
|
||||
SentExpiryAdjustmentMessage = "Sent expiry adjustment message for \"%s\" to \"%s\""
|
||||
|
||||
FailedConstructExpiryMessage = "Failed to construct expiry message for \"%s\": %v"
|
||||
FailedSendExpiryMessage = "Failed to send expiry message for \"%s\" to \"%s\": %v"
|
||||
SentExpiryMessage = "Sent expiry message for \"%s\" to \"%s\""
|
||||
|
||||
FailedConstructAnnouncementMessage = "Failed to construct announcement message for \"%s\": %v"
|
||||
FailedSendAnnouncementMessage = "Failed to send announcement message for \"%s\" to \"%s\": %v"
|
||||
SentAnnouncementMessage = "Sent announcement message for \"%s\" to \"%s\""
|
||||
)
|
||||
272
main.go
@@ -27,10 +27,12 @@ import (
|
||||
"github.com/hrfee/jfa-go/easyproxy"
|
||||
"github.com/hrfee/jfa-go/jellyseerr"
|
||||
"github.com/hrfee/jfa-go/logger"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/jfa-go/ombi"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"gopkg.in/ini.v1"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -59,6 +61,7 @@ var (
|
||||
commit string
|
||||
buildTimeUnix string
|
||||
builtBy string
|
||||
buildTags []string
|
||||
_LOADBAK *string
|
||||
LOADBAK = ""
|
||||
)
|
||||
@@ -91,7 +94,8 @@ type appContext struct {
|
||||
config *ini.File
|
||||
configPath string
|
||||
configBasePath string
|
||||
configBase settings
|
||||
configBase common.Config
|
||||
patchedConfig common.Config
|
||||
dataPath string
|
||||
webFS httpFS
|
||||
cssClass string // Default theme, "light"|"dark".
|
||||
@@ -99,34 +103,37 @@ type appContext struct {
|
||||
adminUsers []User
|
||||
invalidTokens []string
|
||||
// Keeping jf name because I can't think of a better one
|
||||
jf *mediabrowser.MediaBrowser
|
||||
authJf *mediabrowser.MediaBrowser
|
||||
ombi *ombi.Ombi
|
||||
js *jellyseerr.Jellyseerr
|
||||
datePattern string
|
||||
timePattern string
|
||||
storage Storage
|
||||
validator Validator
|
||||
email *Emailer
|
||||
telegram *TelegramDaemon
|
||||
discord *DiscordDaemon
|
||||
matrix *MatrixDaemon
|
||||
info, debug, err *logger.Logger
|
||||
host string
|
||||
port int
|
||||
version string
|
||||
URLBase string
|
||||
updater *Updater
|
||||
newUpdate bool // Whether whatever's in update is new.
|
||||
tag Tag
|
||||
update Update
|
||||
proxyEnabled bool
|
||||
proxyTransport *http.Transport
|
||||
proxyConfig easyproxy.ProxyConfig
|
||||
internalPWRs map[string]InternalPWR
|
||||
pwrCaptchas map[string]Captcha
|
||||
ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request
|
||||
confirmationKeysLock sync.Mutex
|
||||
jf *mediabrowser.MediaBrowser
|
||||
authJf *mediabrowser.MediaBrowser
|
||||
ombi *OmbiWrapper
|
||||
js *JellyseerrWrapper
|
||||
thirdPartyServices []ThirdPartyService
|
||||
datePattern string
|
||||
timePattern string
|
||||
storage Storage
|
||||
validator Validator
|
||||
email *Emailer
|
||||
telegram *TelegramDaemon
|
||||
discord *DiscordDaemon
|
||||
matrix *MatrixDaemon
|
||||
contactMethods []ContactMethodLinker
|
||||
info, debug, err *logger.Logger
|
||||
host string
|
||||
port int
|
||||
version string
|
||||
ExternalURI, ExternalDomain string
|
||||
updater *Updater
|
||||
webhooks *WebhookSender
|
||||
newUpdate bool // Whether whatever's in update is new.
|
||||
tag Tag
|
||||
update Update
|
||||
proxyEnabled bool
|
||||
proxyTransport *http.Transport
|
||||
proxyConfig easyproxy.ProxyConfig
|
||||
internalPWRs map[string]InternalPWR
|
||||
pwrCaptchas map[string]Captcha
|
||||
ConfirmationKeys map[string]map[string]ConfirmationKey // Map of invite code to jwt to request
|
||||
confirmationKeysLock sync.Mutex
|
||||
}
|
||||
|
||||
func generateSecret(length int) (string, error) {
|
||||
@@ -140,7 +147,7 @@ func generateSecret(length int) (string, error) {
|
||||
|
||||
func test(app *appContext) {
|
||||
fmt.Printf("\n\n----\n\n")
|
||||
settings := map[string]interface{}{
|
||||
settings := map[string]any{
|
||||
"server": app.jf.Server,
|
||||
"server version": app.jf.ServerInfo.Version,
|
||||
"server name": app.jf.ServerInfo.Name,
|
||||
@@ -151,8 +158,8 @@ func test(app *appContext) {
|
||||
for n, v := range settings {
|
||||
fmt.Println(n, ":", v)
|
||||
}
|
||||
users, status, err := app.jf.GetUsers(false)
|
||||
fmt.Printf("GetUsers: code %d err %s maplength %d\n", status, err, len(users))
|
||||
users, err := app.jf.GetUsers(false)
|
||||
fmt.Printf("GetUsers: err %s maplength %d\n", err, len(users))
|
||||
fmt.Printf("View output? [y/n]: ")
|
||||
var choice string
|
||||
fmt.Scanln(&choice)
|
||||
@@ -163,8 +170,8 @@ func test(app *appContext) {
|
||||
fmt.Printf("Enter a user to grab: ")
|
||||
var username string
|
||||
fmt.Scanln(&username)
|
||||
user, status, err := app.jf.UserByName(username, false)
|
||||
fmt.Printf("UserByName (%s): code %d err %s", username, status, err)
|
||||
user, err := app.jf.UserByName(username, false)
|
||||
fmt.Printf("UserByName (%s): err %v", username, err)
|
||||
out, _ := json.MarshalIndent(user, "", " ")
|
||||
fmt.Print(string(out))
|
||||
}
|
||||
@@ -213,23 +220,22 @@ func start(asDaemon, firstCall bool) {
|
||||
firstRun = true
|
||||
dConfig, err := fs.ReadFile(localFS, "config-default.ini")
|
||||
if err != nil {
|
||||
app.err.Fatalf("Couldn't find default config file")
|
||||
app.err.Fatalf(lm.NoConfig)
|
||||
}
|
||||
nConfig, err := os.Create(app.configPath)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
err = os.MkdirAll(filepath.Dir(app.configPath), 0760)
|
||||
}
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't open config file for writing: \"%s\"", app.configPath)
|
||||
app.err.Fatalf("Error: %s", err)
|
||||
app.err.Fatalf(lm.FailedWriting, app.configPath, err)
|
||||
}
|
||||
defer nConfig.Close()
|
||||
_, err = nConfig.Write(dConfig)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Couldn't copy default config.")
|
||||
app.err.Fatalf(lm.FailedCopyConfig, app.configPath, err)
|
||||
}
|
||||
app.info.Printf("Copied default configuration to \"%s\"", app.configPath)
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
app.info.Printf(lm.CopyConfig, app.configPath)
|
||||
tempConfig, _ := ini.ShadowLoad(app.configPath)
|
||||
tempConfig.Section("").Key("first_run").SetValue("true")
|
||||
tempConfig.SaveTo(app.configPath)
|
||||
}
|
||||
@@ -237,8 +243,9 @@ func start(asDaemon, firstCall bool) {
|
||||
var debugMode bool
|
||||
var address string
|
||||
if err := app.loadConfig(); err != nil {
|
||||
app.err.Fatalf("Failed to load config file \"%s\": %v", app.configPath, err)
|
||||
app.err.Fatalf(lm.FailedLoadConfig, app.configPath, err)
|
||||
}
|
||||
app.info.Printf(lm.LoadConfig, app.configPath)
|
||||
|
||||
if app.config.Section("").Key("first_run").MustBool(false) {
|
||||
firstRun = true
|
||||
@@ -270,7 +277,7 @@ func start(asDaemon, firstCall bool) {
|
||||
os.Remove(SOCK)
|
||||
listener, err := net.Listen("unix", SOCK)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Couldn't establish socket connection at %s\n", SOCK)
|
||||
app.err.Fatalf(lm.FailedSocketConnect, SOCK, err)
|
||||
}
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
@@ -286,13 +293,13 @@ func start(asDaemon, firstCall bool) {
|
||||
for {
|
||||
con, err := listener.Accept()
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't read message on %s: %s", SOCK, err)
|
||||
app.err.Printf(lm.FailedSocketRead, SOCK, err)
|
||||
continue
|
||||
}
|
||||
buf := make([]byte, 512)
|
||||
nr, err := con.Read(buf)
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't read message on %s: %s", SOCK, err)
|
||||
app.err.Printf(lm.FailedSocketRead, SOCK, err)
|
||||
continue
|
||||
}
|
||||
command := string(buf[0:nr])
|
||||
@@ -317,13 +324,18 @@ func start(asDaemon, firstCall bool) {
|
||||
err = app.storage.loadLang(langFS, os.DirFS(externalLang))
|
||||
}
|
||||
if err != nil {
|
||||
app.info.Fatalf("Failed to load language files: %+v\n", err)
|
||||
app.info.Fatalf(lm.FailedLangLoad, err)
|
||||
}
|
||||
|
||||
// Read config-base for settings on web.
|
||||
app.configBasePath = "config-base.yaml"
|
||||
configBase, _ := fs.ReadFile(localFS, app.configBasePath)
|
||||
yaml.Unmarshal(configBase, &app.configBase)
|
||||
|
||||
if !firstRun {
|
||||
app.host = app.config.Section("ui").Key("host").String()
|
||||
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
||||
app.info.Println("Using TLS/HTTP2")
|
||||
app.info.Println(lm.UsingTLS)
|
||||
app.port = app.config.Section("advanced").Key("tls_port").MustInt(8057)
|
||||
} else {
|
||||
app.port = app.config.Section("ui").Key("port").MustInt(8056)
|
||||
@@ -348,29 +360,32 @@ func start(asDaemon, firstCall bool) {
|
||||
}
|
||||
address = fmt.Sprintf("%s:%d", app.host, app.port)
|
||||
|
||||
app.debug.Printf("Loaded config file \"%s\"", app.configPath)
|
||||
|
||||
// NOTE: As of writing this, the order in app.thirdPartServices doesn't matter,
|
||||
// but in future it might (like app.contactMethods does), so append to the end!
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
app.debug.Printf("Connecting to Ombi")
|
||||
app.ombi = &OmbiWrapper{}
|
||||
app.debug.Printf(lm.UsingOmbi)
|
||||
ombiServer := app.config.Section("ombi").Key("server").String()
|
||||
app.ombi = ombi.NewOmbi(
|
||||
app.ombi.Ombi = ombi.NewOmbi(
|
||||
ombiServer,
|
||||
app.config.Section("ombi").Key("api_key").String(),
|
||||
common.NewTimeoutHandler("Ombi", ombiServer, true),
|
||||
)
|
||||
|
||||
app.thirdPartyServices = append(app.thirdPartyServices, app.ombi)
|
||||
}
|
||||
|
||||
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) {
|
||||
app.debug.Printf("Connecting to Jellyseerr")
|
||||
app.js = &JellyseerrWrapper{}
|
||||
app.debug.Printf(lm.UsingJellyseerr)
|
||||
jellyseerrServer := app.config.Section("jellyseerr").Key("server").String()
|
||||
app.js = jellyseerr.NewJellyseerr(
|
||||
app.js.Jellyseerr = jellyseerr.NewJellyseerr(
|
||||
jellyseerrServer,
|
||||
app.config.Section("jellyseerr").Key("api_key").String(),
|
||||
common.NewTimeoutHandler("Jellyseerr", jellyseerrServer, true),
|
||||
)
|
||||
app.js.AutoImportUsers = app.config.Section("jellyseerr").Key("import_existing").MustBool(false)
|
||||
// app.js.LogRequestBodies = true
|
||||
app.thirdPartyServices = append(app.thirdPartyServices, app.js)
|
||||
|
||||
}
|
||||
|
||||
@@ -379,10 +394,8 @@ func start(asDaemon, firstCall bool) {
|
||||
app.ConnectDB()
|
||||
defer app.storage.db.Close()
|
||||
|
||||
// Read config-base for settings on web.
|
||||
app.configBasePath = "config-base.json"
|
||||
configBase, _ := fs.ReadFile(localFS, app.configBasePath)
|
||||
json.Unmarshal(configBase, &app.configBase)
|
||||
// copy it to app.patchedConfig, and patch in settings from app.config, and language stuff.
|
||||
app.PatchConfigBase()
|
||||
|
||||
secret, err := generateSecret(16)
|
||||
if err != nil {
|
||||
@@ -398,10 +411,9 @@ func start(asDaemon, firstCall bool) {
|
||||
if stringServerType == "emby" {
|
||||
serverType = mediabrowser.EmbyServer
|
||||
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."))
|
||||
app.info.Println(lm.UsingEmby)
|
||||
} else {
|
||||
app.info.Println("Using Jellyfin server type")
|
||||
app.info.Println(lm.UsingJellyfin)
|
||||
}
|
||||
|
||||
app.jf, err = mediabrowser.NewServer(
|
||||
@@ -415,15 +427,13 @@ func start(asDaemon, firstCall bool) {
|
||||
cacheTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ \"%s\": %v", server, err)
|
||||
app.err.Fatalf(lm.FailedAuthJellyfin, server, -1, err)
|
||||
}
|
||||
if debugMode {
|
||||
/*if debugMode {
|
||||
app.jf.Verbose = true
|
||||
}
|
||||
|
||||
if app.proxyEnabled {
|
||||
app.jf.SetTransport(app.proxyTransport)
|
||||
}
|
||||
}*/
|
||||
// It's probably best we leave this on
|
||||
app.jf.Verbose = true
|
||||
|
||||
var status int
|
||||
retryOpts := mediabrowser.MustAuthenticateOptions{
|
||||
@@ -431,11 +441,12 @@ func start(asDaemon, firstCall bool) {
|
||||
RetryGap: time.Duration(app.config.Section("advanced").Key("auth_retry_gap").MustInt(10)) * time.Second,
|
||||
LogFailures: true,
|
||||
}
|
||||
_, status, err = app.jf.MustAuthenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String(), retryOpts)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ \"%s\" (%d): %v", server, status, err)
|
||||
_, err = app.jf.MustAuthenticate(app.config.Section("jellyfin").Key("username").String(), app.config.Section("jellyfin").Key("password").String(), retryOpts)
|
||||
if err != nil {
|
||||
app.err.Fatalf(lm.FailedAuthJellyfin, server, status, err)
|
||||
}
|
||||
app.info.Printf("Authenticated with \"%s\"", server)
|
||||
app.info.Printf(lm.AuthJellyfin, server)
|
||||
app.debug.Printf(lm.AsUser, app.jf.Username)
|
||||
|
||||
runMigrations(app)
|
||||
|
||||
@@ -448,8 +459,9 @@ func start(asDaemon, firstCall bool) {
|
||||
user.Username = app.config.Section("ui").Key("username").String()
|
||||
user.Password = app.config.Section("ui").Key("password").String()
|
||||
app.adminUsers = append(app.adminUsers, user)
|
||||
app.info.Println(lm.UsingLocalAuth)
|
||||
} else {
|
||||
app.debug.Println("Using Jellyfin for authentication")
|
||||
app.debug.Println(lm.UsingJellyfinAuth)
|
||||
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
|
||||
if debugMode {
|
||||
app.authJf.Verbose = true
|
||||
@@ -457,6 +469,7 @@ func start(asDaemon, firstCall bool) {
|
||||
}
|
||||
|
||||
// Since email depends on language, the email reload in loadConfig won't work first time.
|
||||
// Email also handles its own proxying, as (SMTP atleast) doesn't use a HTTP transport.
|
||||
app.email = NewEmailer(app)
|
||||
app.loadStrftime()
|
||||
|
||||
@@ -512,34 +525,60 @@ func start(asDaemon, firstCall bool) {
|
||||
defer backupDaemon.Shutdown()
|
||||
}
|
||||
|
||||
if telegramEnabled {
|
||||
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()
|
||||
}
|
||||
}
|
||||
// NOTE: The order in which these are placed in app.contactMethods matters.
|
||||
// Add new ones to the end.
|
||||
// FIXME: Add proxies.
|
||||
if discordEnabled {
|
||||
app.discord, err = newDiscordDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to authenticate with Discord: %v", err)
|
||||
app.err.Printf(lm.FailedInitDiscord, err)
|
||||
discordEnabled = false
|
||||
} else {
|
||||
app.debug.Println(lm.InitDiscord)
|
||||
go app.discord.run()
|
||||
defer app.discord.Shutdown()
|
||||
app.contactMethods = append(app.contactMethods, app.discord)
|
||||
}
|
||||
}
|
||||
if telegramEnabled {
|
||||
app.telegram, err = newTelegramDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf(lm.FailedInitTelegram, err)
|
||||
telegramEnabled = false
|
||||
} else {
|
||||
app.debug.Println(lm.InitTelegram)
|
||||
go app.telegram.run()
|
||||
defer app.telegram.Shutdown()
|
||||
app.contactMethods = append(app.contactMethods, app.telegram)
|
||||
}
|
||||
}
|
||||
if matrixEnabled {
|
||||
app.matrix, err = newMatrixDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to initialize Matrix daemon: %v", err)
|
||||
app.err.Printf(lm.FailedInitMatrix, err)
|
||||
matrixEnabled = false
|
||||
} else {
|
||||
app.debug.Println(lm.InitMatrix)
|
||||
go app.matrix.run()
|
||||
defer app.matrix.Shutdown()
|
||||
app.contactMethods = append(app.contactMethods, app.matrix)
|
||||
}
|
||||
}
|
||||
|
||||
// Non-consequential if we don't need it
|
||||
app.webhooks = NewWebhookSender(
|
||||
common.NewTimeoutHandler("Webhook", "?", true),
|
||||
app.debug,
|
||||
)
|
||||
|
||||
// Updater proxy set in config.go, don't worry!
|
||||
if app.proxyEnabled {
|
||||
app.jf.SetTransport(app.proxyTransport)
|
||||
for _, c := range app.thirdPartyServices {
|
||||
c.SetTransport(app.proxyTransport)
|
||||
}
|
||||
for _, c := range app.contactMethods {
|
||||
c.SetTransport(app.proxyTransport)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -558,7 +597,7 @@ func start(asDaemon, firstCall bool) {
|
||||
app.storage.lang.SetupPath = "setup"
|
||||
err := app.storage.loadLangSetup(langFS)
|
||||
if err != nil {
|
||||
app.info.Fatalf("Failed to load language files: %+v\n", err)
|
||||
app.info.Fatalf(lm.FailedLangLoad, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,14 +605,14 @@ func start(asDaemon, firstCall bool) {
|
||||
// workaround for potentially broken windows mime types
|
||||
mime.AddExtensionType(".js", "application/javascript")
|
||||
|
||||
app.info.Println("Initializing router")
|
||||
app.info.Println(lm.InitRouter)
|
||||
router := app.loadRouter(address, debugMode)
|
||||
app.info.Println("Loading routes")
|
||||
app.info.Println(lm.LoadRoutes)
|
||||
if !firstRun {
|
||||
app.loadRoutes(router)
|
||||
} else {
|
||||
app.loadSetup(router)
|
||||
app.info.Printf("Loading setup @ %s", address)
|
||||
app.info.Printf(lm.LoadingSetup, address)
|
||||
}
|
||||
go func() {
|
||||
if app.config.Section("advanced").Key("tls").MustBool(false) {
|
||||
@@ -581,45 +620,45 @@ func start(asDaemon, firstCall bool) {
|
||||
key := app.config.Section("advanced").Key("tls_key").MustString("")
|
||||
if err := SRV.ListenAndServeTLS(cert, key); err != nil {
|
||||
filesToCheck := []string{cert, key}
|
||||
fileNames := []string{"Certificate", "Key"}
|
||||
fileNames := []string{lm.InvalidSSLCert, lm.InvalidSSLKey}
|
||||
for i, v := range filesToCheck {
|
||||
_, err := os.Stat(v)
|
||||
if err != nil {
|
||||
app.err.Printf("SSL/TLS %s: %v\n", fileNames[i], err)
|
||||
app.err.Printf(fileNames[i], v, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err == http.ErrServerClosed {
|
||||
app.err.Printf("Failure serving with SSL/TLS: %s", err)
|
||||
app.err.Printf(lm.FailServeSSL, err)
|
||||
} else {
|
||||
app.err.Fatalf("Failure serving with SSL/TLS: %s", err)
|
||||
app.err.Fatalf(lm.FailServeSSL, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := SRV.ListenAndServe(); err != nil {
|
||||
if err == http.ErrServerClosed {
|
||||
app.err.Printf("Failure serving: %s", err)
|
||||
app.err.Printf(lm.FailServe, err)
|
||||
} else {
|
||||
app.err.Fatalf("Failure serving: %s", err)
|
||||
app.err.Fatalf(lm.FailServe, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
if firstRun {
|
||||
app.info.Printf("Loaded, visit %s to start.", address)
|
||||
app.info.Printf(lm.ServingSetup, address)
|
||||
} else {
|
||||
app.info.Printf("Loaded @ %s", address)
|
||||
app.info.Printf(lm.Serving, address)
|
||||
}
|
||||
|
||||
waitForRestart()
|
||||
|
||||
app.info.Printf("Restart/Quit signal received, give me a second!")
|
||||
app.info.Printf(lm.QuitReceived)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
if err := SRV.Shutdown(ctx); err != nil {
|
||||
app.err.Fatalf("Server shutdown error: %s", err)
|
||||
app.err.Fatalf(lm.FailedQuit, err)
|
||||
}
|
||||
app.info.Println("Server shut down.")
|
||||
app.info.Println(lm.Quit)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -631,7 +670,7 @@ func shutdown() {
|
||||
}
|
||||
|
||||
func (app *appContext) shutdown() {
|
||||
app.info.Println("Shutting down...")
|
||||
app.info.Println(lm.Quitting)
|
||||
shutdown()
|
||||
}
|
||||
|
||||
@@ -659,7 +698,7 @@ func flagPassed(name string) (found bool) {
|
||||
}
|
||||
|
||||
// @title jfa-go internal API
|
||||
// @version 0.5.1
|
||||
// @version 0.5.2
|
||||
// @description API for the jfa-go frontend
|
||||
// @contact.name Harvey Tindall
|
||||
// @contact.email hrfee@hrfee.dev
|
||||
@@ -715,15 +754,22 @@ func printVersion() {
|
||||
fmt.Println(info("jfa-go version: %s (%s)%s\n", hiwhite(version), white(commit), tray))
|
||||
}
|
||||
|
||||
const SYSTEMD_SERVICE = "jfa-go.service"
|
||||
|
||||
func main() {
|
||||
// Generate list of "-tags" for about page.
|
||||
BuildTagsE2EE()
|
||||
BuildTagsTray()
|
||||
BuildTagsExternal()
|
||||
|
||||
f, err := logOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to start logging: %v\n", err)
|
||||
fmt.Printf(lm.FailedLogging, err)
|
||||
}
|
||||
defer f()
|
||||
printVersion()
|
||||
SOCK = filepath.Join(temp, SOCK)
|
||||
fmt.Println("Socket:", SOCK)
|
||||
fmt.Printf(lm.SocketPath+"\n", SOCK)
|
||||
if flagPassed("test") {
|
||||
TEST = true
|
||||
}
|
||||
@@ -752,24 +798,26 @@ func main() {
|
||||
} else if flagPassed("stop") {
|
||||
con, err := net.Dial("unix", SOCK)
|
||||
if err != nil {
|
||||
fmt.Printf("Couldn't dial socket %s, are you sure jfa-go is running?\n", SOCK)
|
||||
fmt.Printf(lm.FailedSocketConnect+"\n", SOCK, err)
|
||||
fmt.Println(lm.SocketCheckRunning)
|
||||
os.Exit(1)
|
||||
}
|
||||
_, err = con.Write([]byte("stop"))
|
||||
if err != nil {
|
||||
fmt.Printf("Couldn't send command to socket %s, are you sure jfa-go is running?\n", SOCK)
|
||||
fmt.Printf(lm.FailedSocketWrite+"\n", SOCK, err)
|
||||
fmt.Println(lm.SocketCheckRunning)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Sent.")
|
||||
fmt.Println(lm.SocketWrite)
|
||||
} else if flagPassed("daemon") {
|
||||
start(true, true)
|
||||
} else if flagPassed("systemd") {
|
||||
service, err := fs.ReadFile(localFS, "jfa-go.service")
|
||||
service, err := fs.ReadFile(localFS, SYSTEMD_SERVICE)
|
||||
if err != nil {
|
||||
fmt.Printf("Couldn't read jfa-go.service: %v\n", err)
|
||||
fmt.Printf(lm.FailedReading+"\n", SYSTEMD_SERVICE, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
absPath, err := filepath.Abs(os.Args[0])
|
||||
absPath, err := os.Executable()
|
||||
if err != nil {
|
||||
absPath = os.Args[0]
|
||||
}
|
||||
@@ -780,13 +828,13 @@ func main() {
|
||||
}
|
||||
}
|
||||
service = []byte(strings.Replace(string(service), "{executable}", command, 1))
|
||||
err = os.WriteFile("jfa-go.service", service, 0666)
|
||||
err = os.WriteFile(SYSTEMD_SERVICE, service, 0666)
|
||||
if err != nil {
|
||||
fmt.Printf("Couldn't write jfa-go.service: %v\n", err)
|
||||
fmt.Printf(lm.FailedWriting+"\n", SYSTEMD_SERVICE, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(info(`If you want to execute jfa-go with special arguments, re-run this command with them.
|
||||
Move the newly created "jfa-go.service" file to ~/.config/systemd/user (Creating it if necessary).
|
||||
Move the newly created SYSTEMD_SERVICE file to ~/.config/systemd/user (Creating it if necessary).
|
||||
Then run "systemctl --user daemon-reload".
|
||||
You can then run:
|
||||
|
||||
|
||||
183
matrix.go
@@ -1,29 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
var (
|
||||
DEVICE_ID = id.DeviceID("jfa-go")
|
||||
)
|
||||
|
||||
type MatrixDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
bot *mautrix.Client
|
||||
userID id.UserID
|
||||
tokens map[string]UnverifiedUser // Map of tokens to users
|
||||
languages map[id.RoomID]string // Map of roomIDs to language codes
|
||||
Encryption bool
|
||||
isEncrypted map[id.RoomID]bool
|
||||
crypto Crypto
|
||||
app *appContext
|
||||
start int64
|
||||
Stopped bool
|
||||
bot *mautrix.Client
|
||||
userID id.UserID
|
||||
homeserver string
|
||||
tokens map[string]UnverifiedUser // Map of tokens to users
|
||||
languages map[id.RoomID]string // Map of roomIDs to language codes
|
||||
Encryption bool
|
||||
crypto *Crypto
|
||||
app *appContext
|
||||
start int64
|
||||
cancellation sync.WaitGroup
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type UnverifiedUser struct {
|
||||
@@ -31,15 +41,6 @@ type UnverifiedUser struct {
|
||||
User *MatrixUser
|
||||
}
|
||||
|
||||
type MatrixUser struct {
|
||||
RoomID string
|
||||
Encrypted bool
|
||||
UserID string
|
||||
Lang string
|
||||
Contact bool
|
||||
JellyfinID string `badgerhold:"key"`
|
||||
}
|
||||
|
||||
var matrixFilter = mautrix.Filter{
|
||||
Room: mautrix.RoomFilter{
|
||||
Timeline: mautrix.FilterPart{
|
||||
@@ -63,23 +64,33 @@ var matrixFilter = mautrix.Filter{
|
||||
},
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) renderUserID(uid id.UserID) id.UserID {
|
||||
if uid[0] != '@' {
|
||||
uid = "@" + uid
|
||||
}
|
||||
if !strings.ContainsRune(string(uid), ':') {
|
||||
uid = id.UserID(string(uid) + ":" + d.homeserver)
|
||||
}
|
||||
return uid
|
||||
}
|
||||
|
||||
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: id.UserID(matrix.Key("user_id").String()),
|
||||
tokens: map[string]UnverifiedUser{},
|
||||
languages: map[id.RoomID]string{},
|
||||
isEncrypted: map[id.RoomID]bool{},
|
||||
app: app,
|
||||
start: time.Now().UnixNano() / 1e6,
|
||||
userID: id.UserID(matrix.Key("user_id").String()),
|
||||
homeserver: matrix.Key("homeserver").String(),
|
||||
tokens: map[string]UnverifiedUser{},
|
||||
languages: map[id.RoomID]string{},
|
||||
app: app,
|
||||
start: time.Now().UnixNano() / 1e6,
|
||||
}
|
||||
d.bot, err = mautrix.NewClient(homeserver, d.userID, token)
|
||||
d.userID = d.renderUserID(d.userID)
|
||||
d.bot, err = mautrix.NewClient(d.homeserver, d.userID, token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
d.bot.DeviceID = DEVICE_ID
|
||||
// resp, err := d.bot.CreateFilter(&matrixFilter)
|
||||
// if err != nil {
|
||||
// return
|
||||
@@ -89,12 +100,16 @@ func newMatrixDaemon(app *appContext) (d *MatrixDaemon, err error) {
|
||||
if user.Lang != "" {
|
||||
d.languages[id.RoomID(user.RoomID)] = user.Lang
|
||||
}
|
||||
d.isEncrypted[id.RoomID(user.RoomID)] = user.Encrypted
|
||||
}
|
||||
err = InitMatrixCrypto(d)
|
||||
return
|
||||
}
|
||||
|
||||
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
||||
func (d *MatrixDaemon) SetTransport(t *http.Transport) {
|
||||
d.bot.Client.Transport = t
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string) (string, error) {
|
||||
req := &mautrix.ReqLogin{
|
||||
Type: mautrix.AuthTypePassword,
|
||||
@@ -103,13 +118,13 @@ func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string
|
||||
User: username,
|
||||
},
|
||||
Password: password,
|
||||
DeviceID: id.DeviceID("jfa-go-" + commit),
|
||||
DeviceID: DEVICE_ID,
|
||||
}
|
||||
bot, err := mautrix.NewClient(homeserver, id.UserID(username), "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := bot.Login(req)
|
||||
resp, err := bot.Login(context.TODO(), req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -117,25 +132,28 @@ func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) run() {
|
||||
startTime := d.start
|
||||
d.app.info.Println("Starting Matrix bot daemon")
|
||||
syncer := d.bot.Syncer.(*mautrix.DefaultSyncer)
|
||||
HandleSyncerCrypto(startTime, d, syncer)
|
||||
syncer.OnEventType(event.EventMessage, d.handleMessage)
|
||||
|
||||
if err := d.bot.Sync(); err != nil {
|
||||
d.app.err.Printf("Matrix sync failed: %v", err)
|
||||
d.app.info.Printf(lm.StartDaemon, lm.Matrix)
|
||||
|
||||
var syncCtx context.Context
|
||||
syncCtx, d.cancel = context.WithCancel(context.Background())
|
||||
d.cancellation.Add(1)
|
||||
|
||||
if err := d.bot.SyncWithContext(syncCtx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
d.app.err.Printf(lm.FailedSyncMatrix, err)
|
||||
}
|
||||
d.cancellation.Done()
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) Shutdown() {
|
||||
CryptoShutdown(d)
|
||||
d.bot.StopSync()
|
||||
d.cancel()
|
||||
d.cancellation.Wait()
|
||||
d.Stopped = true
|
||||
close(d.ShutdownChannel)
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) handleMessage(source mautrix.EventSource, evt *event.Event) {
|
||||
func (d *MatrixDaemon) handleMessage(ctx context.Context, evt *event.Event) {
|
||||
if evt.Timestamp < d.start {
|
||||
return
|
||||
}
|
||||
@@ -166,11 +184,12 @@ func (d *MatrixDaemon) commandLang(evt *event.Event, code, lang string) {
|
||||
list += fmt.Sprintf("%s: %s\n", c, d.app.storage.lang.Telegram[c].Meta.Name)
|
||||
}
|
||||
_, err := d.bot.SendText(
|
||||
context.TODO(),
|
||||
evt.RoomID,
|
||||
list,
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Matrix: Failed to send message to \"%s\": %v", evt.Sender, err)
|
||||
d.app.err.Printf(lm.FailedReply, lm.Matrix, evt.Sender, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -184,9 +203,9 @@ func (d *MatrixDaemon) commandLang(evt *event.Event, code, lang string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) CreateRoom(userID string) (roomID id.RoomID, encrypted bool, err error) {
|
||||
func (d *MatrixDaemon) CreateRoom(userID string) (roomID id.RoomID, err error) {
|
||||
var room *mautrix.RespCreateRoom
|
||||
room, err = d.bot.CreateRoom(&mautrix.ReqCreateRoom{
|
||||
room, err = d.bot.CreateRoom(context.TODO(), &mautrix.ReqCreateRoom{
|
||||
Visibility: "private",
|
||||
Invite: []id.UserID{id.UserID(userID)},
|
||||
Topic: d.app.config.Section("matrix").Key("topic").String(),
|
||||
@@ -195,15 +214,16 @@ func (d *MatrixDaemon) CreateRoom(userID string) (roomID id.RoomID, encrypted bo
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encrypted = EncryptRoom(d, room, id.UserID(userID))
|
||||
// encrypted = EncryptRoom(d, room, id.UserID(userID))
|
||||
roomID = room.RoomID
|
||||
err = EncryptRoom(d, roomID)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
||||
roomID, encrypted, err := d.CreateRoom(userID)
|
||||
roomID, err := d.CreateRoom(userID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Failed to create room for user \"%s\": %v", userID, err)
|
||||
d.app.err.Printf(lm.FailedCreateMatrixRoom, userID, err)
|
||||
return
|
||||
}
|
||||
lang := "en-us"
|
||||
@@ -211,10 +231,9 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
||||
d.tokens[pin] = UnverifiedUser{
|
||||
false,
|
||||
&MatrixUser{
|
||||
RoomID: string(roomID),
|
||||
UserID: userID,
|
||||
Lang: lang,
|
||||
Encrypted: encrypted,
|
||||
RoomID: string(roomID),
|
||||
UserID: userID,
|
||||
Lang: lang,
|
||||
},
|
||||
}
|
||||
err = d.sendToRoom(
|
||||
@@ -226,7 +245,7 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
||||
roomID,
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Matrix: Failed to send welcome message to \"%s\": %v", userID, err)
|
||||
d.app.err.Printf(lm.FailedMessage, lm.Matrix, userID, err)
|
||||
return
|
||||
}
|
||||
ok = true
|
||||
@@ -234,16 +253,17 @@ func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) sendToRoom(content *event.MessageEventContent, roomID id.RoomID) (err error) {
|
||||
if encrypted, ok := d.isEncrypted[roomID]; ok && encrypted {
|
||||
return d.send(content, roomID)
|
||||
/*if encrypted, ok := d.isEncrypted[roomID]; ok && encrypted {
|
||||
err = SendEncrypted(d, content, roomID)
|
||||
} else {
|
||||
_, err = d.bot.SendMessageEvent(roomID, event.EventMessage, content, mautrix.ReqSendEvent{})
|
||||
_, err = d.bot.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content, mautrix.ReqSendEvent{})
|
||||
}
|
||||
return
|
||||
return*/
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) send(content *event.MessageEventContent, roomID id.RoomID) (err error) {
|
||||
_, err = d.bot.SendMessageEvent(roomID, event.EventMessage, content, mautrix.ReqSendEvent{})
|
||||
_, err = d.bot.SendMessageEvent(context.TODO(), roomID, event.EventMessage, content, mautrix.ReqSendEvent{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -276,6 +296,57 @@ func (d *MatrixDaemon) UserExists(userID string) bool {
|
||||
return err != nil || c > 0
|
||||
}
|
||||
|
||||
// Exists returns whether or not the given user exists.
|
||||
func (d *MatrixDaemon) Exists(user ContactMethodUser) bool {
|
||||
return d.UserExists(user.Name())
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
func (d *MatrixDaemon) PIN(req newUserDTO) string { return req.MatrixPIN }
|
||||
|
||||
func (d *MatrixDaemon) Name() string { return lm.Matrix }
|
||||
|
||||
func (d *MatrixDaemon) Required() bool {
|
||||
return d.app.config.Section("telegram").Key("required").MustBool(false)
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) UniqueRequired() bool {
|
||||
return d.app.config.Section("telegram").Key("require_unique").MustBool(false)
|
||||
}
|
||||
|
||||
// TokenVerified returns whether or not a token with the given PIN has been verified, and the token itself.
|
||||
func (d *MatrixDaemon) TokenVerified(pin string) (token UnverifiedUser, ok bool) {
|
||||
token, ok = d.tokens[pin]
|
||||
// delete(t.verifiedTokens, pin)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteVerifiedToken removes the token with the given PIN.
|
||||
func (d *MatrixDaemon) DeleteVerifiedToken(PIN string) {
|
||||
delete(d.tokens, PIN)
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) UserVerified(PIN string) (ContactMethodUser, bool) {
|
||||
token, ok := d.TokenVerified(PIN)
|
||||
if !ok {
|
||||
return &MatrixUser{}, false
|
||||
}
|
||||
return token.User, ok
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) PostVerificationTasks(string, ContactMethodUser) error { return nil }
|
||||
|
||||
func (m *MatrixUser) Name() string { return m.UserID }
|
||||
func (m *MatrixUser) SetMethodID(id any) { m.UserID = id.(string) }
|
||||
func (m *MatrixUser) MethodID() any { return m.UserID }
|
||||
func (m *MatrixUser) SetJellyfin(id string) { m.JellyfinID = id }
|
||||
func (m *MatrixUser) Jellyfin() string { return m.JellyfinID }
|
||||
func (m *MatrixUser) SetAllowContactFromDTO(req newUserDTO) { m.Contact = req.MatrixContact }
|
||||
func (m *MatrixUser) SetAllowContact(contact bool) { m.Contact = contact }
|
||||
func (m *MatrixUser) AllowContact() bool { return m.Contact }
|
||||
func (m *MatrixUser) Store(st *Storage) {
|
||||
st.SetMatrixKey(m.Jellyfin(), *m)
|
||||
}
|
||||
|
||||
221
matrix_crypto.go
@@ -1,224 +1,61 @@
|
||||
//go:build e2ee
|
||||
// +build e2ee
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"context"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"maunium.net/go/mautrix/crypto/cryptohelper"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
type Crypto struct {
|
||||
cryptoStore *crypto.GobStore
|
||||
olm *crypto.OlmMachine
|
||||
helper *cryptohelper.CryptoHelper
|
||||
}
|
||||
|
||||
func BuildTagsE2EE() {
|
||||
buildTags = append(buildTags, "e2ee")
|
||||
}
|
||||
|
||||
func MatrixE2EE() bool { return true }
|
||||
|
||||
type stateStore struct {
|
||||
isEncrypted *map[id.RoomID]bool
|
||||
}
|
||||
|
||||
func (m *stateStore) IsEncrypted(roomID id.RoomID) bool {
|
||||
// encrypted, ok := (*m.isEncrypted)[roomID]
|
||||
// return ok && encrypted
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *stateStore) GetEncryptionEvent(roomID id.RoomID) *event.EncryptionEventContent {
|
||||
return &event.EncryptionEventContent{
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
RotationPeriodMillis: 7 * 24 * 60 * 60 * 1000,
|
||||
RotationPeriodMessages: 100,
|
||||
}
|
||||
}
|
||||
|
||||
// Users are assumed to only have one common channel with the bot, so we can stub this out.
|
||||
func (m *stateStore) FindSharedRooms(userID id.UserID) []id.RoomID {
|
||||
// for _, user := range m.app.storage.matrix {
|
||||
// if id.UserID(user.UserID) == userID {
|
||||
// return []id.RoomID{id.RoomID(user.RoomID)}
|
||||
// }
|
||||
// }
|
||||
return []id.RoomID{}
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) getUserIDs(roomID id.RoomID) (list []id.UserID, err error) {
|
||||
members, err := d.bot.JoinedMembers(roomID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
list = make([]id.UserID, len(members.Joined))
|
||||
i := 0
|
||||
for id := range members.Joined {
|
||||
list[i] = id
|
||||
i++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type olmLogger struct {
|
||||
app *appContext
|
||||
}
|
||||
|
||||
func (o olmLogger) Error(message string, args ...interface{}) {
|
||||
o.app.err.Printf("OLM: "+message+"\n", args)
|
||||
}
|
||||
|
||||
func (o olmLogger) Warn(message string, args ...interface{}) {
|
||||
o.app.info.Printf("OLM: "+message+"\n", args)
|
||||
}
|
||||
|
||||
func (o olmLogger) Debug(message string, args ...interface{}) {
|
||||
o.app.debug.Printf("OLM: "+message+"\n", args)
|
||||
}
|
||||
|
||||
func (o olmLogger) Trace(message string, args ...interface{}) {
|
||||
if strings.HasPrefix(message, "Got membership state event") {
|
||||
return
|
||||
}
|
||||
o.app.debug.Printf("OLM [TRACE]: "+message+"\n", args)
|
||||
}
|
||||
|
||||
func InitMatrixCrypto(d *MatrixDaemon) (err error) {
|
||||
func InitMatrixCrypto(d *MatrixDaemon) error {
|
||||
d.Encryption = d.app.config.Section("matrix").Key("encryption").MustBool(false)
|
||||
if !d.Encryption {
|
||||
return
|
||||
}
|
||||
for _, user := range d.app.storage.matrix {
|
||||
d.isEncrypted[id.RoomID(user.RoomID)] = user.Encrypted
|
||||
// return fmt.Errorf("encryption disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
dbPath := d.app.config.Section("files").Key("matrix_sql").String()
|
||||
// If the db is maintained after restart, element reports "The secure channel with the sender was corrupted" when sending a message from the bot.
|
||||
// This obviously isn't right, but it seems to work.
|
||||
// Since its not really used anyway, just use the deprecated GobStore. This reduces cgo usage anyway.
|
||||
var cryptoStore *crypto.GobStore
|
||||
cryptoStore, err = crypto.NewGobStore(dbPath)
|
||||
// d.db, err = sql.Open("sqlite3", dbPath)
|
||||
var err error
|
||||
d.crypto = &Crypto{}
|
||||
d.crypto.helper, err = cryptohelper.NewCryptoHelper(d.bot, []byte("jfa-go"), dbPath)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
olmLog := &olmLogger{d.app}
|
||||
// deviceID := "jfa-go" + commit
|
||||
// cryptoStore := crypto.NewSQLCryptoStore(d.db, "sqlite3", string(d.userID)+deviceID, id.DeviceID(deviceID), []byte("jfa-go"), olmLog)
|
||||
// err = cryptoStore.CreateTables()
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
olm := crypto.NewOlmMachine(d.bot, olmLog, cryptoStore, &stateStore{&d.isEncrypted})
|
||||
olm.AllowUnverifiedDevices = true
|
||||
err = olm.Load()
|
||||
|
||||
err = d.crypto.helper.Init(context.TODO())
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
d.crypto = Crypto{
|
||||
cryptoStore: cryptoStore,
|
||||
olm: olm,
|
||||
}
|
||||
return
|
||||
|
||||
d.bot.Crypto = d.crypto.helper
|
||||
|
||||
d.Encryption = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleSyncerCrypto(startTime int64, d *MatrixDaemon, syncer *mautrix.DefaultSyncer) {
|
||||
func EncryptRoom(d *MatrixDaemon, roomID id.RoomID) error {
|
||||
if !d.Encryption {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
syncer.OnSync(func(resp *mautrix.RespSync, since string) bool {
|
||||
d.crypto.olm.ProcessSyncResponse(resp, since)
|
||||
return true
|
||||
})
|
||||
syncer.OnEventType(event.StateMember, func(source mautrix.EventSource, evt *event.Event) {
|
||||
d.crypto.olm.HandleMemberEvent(evt)
|
||||
// if evt.Content.AsMember().Membership != event.MembershipJoin {
|
||||
// return
|
||||
// }
|
||||
// userIDs, err := d.getUserIDs(evt.RoomID)
|
||||
// if err != nil || len(userIDs) < 2 {
|
||||
// fmt.Println("FS", err)
|
||||
// return
|
||||
// }
|
||||
// err = d.crypto.olm.ShareGroupSession(evt.RoomID, userIDs)
|
||||
// if err != nil {
|
||||
// fmt.Println("FS", err)
|
||||
// return
|
||||
// }
|
||||
})
|
||||
syncer.OnEventType(event.EventEncrypted, func(source mautrix.EventSource, evt *event.Event) {
|
||||
if evt.Timestamp < startTime {
|
||||
return
|
||||
}
|
||||
decrypted, err := d.crypto.olm.DecryptMegolmEvent(evt)
|
||||
// if strings.Contains(err.Error(), crypto.NoSessionFound.Error()) {
|
||||
// d.app.err.Printf("Failed to decrypt Matrix message: no session found")
|
||||
// return
|
||||
// }
|
||||
if err != nil {
|
||||
d.app.err.Printf("Failed to decrypt Matrix message: %v", err)
|
||||
return
|
||||
}
|
||||
d.handleMessage(source, decrypted)
|
||||
})
|
||||
}
|
||||
|
||||
func CryptoShutdown(d *MatrixDaemon) {
|
||||
if d.Encryption {
|
||||
d.crypto.olm.FlushStore()
|
||||
}
|
||||
}
|
||||
|
||||
func EncryptRoom(d *MatrixDaemon, room *mautrix.RespCreateRoom, userID id.UserID) (encrypted bool) {
|
||||
if !d.Encryption {
|
||||
return
|
||||
}
|
||||
_, err := d.bot.SendStateEvent(room.RoomID, event.StateEncryption, "", &event.EncryptionEventContent{
|
||||
_, err := d.bot.SendStateEvent(context.TODO(), roomID, event.StateEncryption, "", event.EncryptionEventContent{
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
RotationPeriodMillis: 7 * 24 * 60 * 60 * 1000,
|
||||
RotationPeriodMessages: 100,
|
||||
})
|
||||
if err == nil {
|
||||
encrypted = true
|
||||
} else {
|
||||
d.app.debug.Printf("Matrix: Failed to enable encryption in room: %v", err)
|
||||
return
|
||||
}
|
||||
d.isEncrypted[room.RoomID] = encrypted
|
||||
var userIDs []id.UserID
|
||||
userIDs, err = d.getUserIDs(room.RoomID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
userIDs = append(userIDs, userID)
|
||||
return
|
||||
}
|
||||
|
||||
func SendEncrypted(d *MatrixDaemon, content *event.MessageEventContent, roomID id.RoomID) (err error) {
|
||||
if !d.Encryption {
|
||||
err = d.send(content, roomID)
|
||||
return
|
||||
}
|
||||
var encrypted *event.EncryptedEventContent
|
||||
encrypted, err = d.crypto.olm.EncryptMegolmEvent(roomID, event.EventMessage, content)
|
||||
if err == crypto.SessionExpired || err == crypto.SessionNotShared || err == crypto.NoGroupSession {
|
||||
// err = d.crypto.olm.ShareGroupSession(id.RoomID(user.RoomID), []id.UserID{id.UserID(user.UserID), d.userID})
|
||||
var userIDs []id.UserID
|
||||
userIDs, err = d.getUserIDs(roomID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = d.crypto.olm.ShareGroupSession(roomID, userIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encrypted, err = d.crypto.olm.EncryptMegolmEvent(roomID, event.EventMessage, content)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = d.bot.SendMessageEvent(roomID, event.EventEncrypted, &event.Content{Parsed: encrypted})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
//go:build !e2ee
|
||||
// +build !e2ee
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
import "maunium.net/go/mautrix/id"
|
||||
|
||||
type Crypto struct{}
|
||||
|
||||
func BuildTagsE2EE() {}
|
||||
|
||||
func MatrixE2EE() bool { return false }
|
||||
|
||||
func InitMatrixCrypto(d *MatrixDaemon) (err error) {
|
||||
@@ -17,19 +16,4 @@ func InitMatrixCrypto(d *MatrixDaemon) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func HandleSyncerCrypto(startTime int64, d *MatrixDaemon, syncer *mautrix.DefaultSyncer) {
|
||||
return
|
||||
}
|
||||
|
||||
func CryptoShutdown(d *MatrixDaemon) {
|
||||
return
|
||||
}
|
||||
|
||||
func EncryptRoom(d *MatrixDaemon, room *mautrix.RespCreateRoom, userID id.UserID) (encrypted bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func SendEncrypted(d *MatrixDaemon, content *event.MessageEventContent, roomID id.RoomID) (err error) {
|
||||
err = d.send(content, roomID)
|
||||
return
|
||||
}
|
||||
func EncryptRoom(d *MatrixDaemon, roomID id.RoomID) error { return nil }
|
||||
|
||||
@@ -9,9 +9,12 @@ import (
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// NOTE: This is the one file where log messages are not part of logmessages/logmessages.go
|
||||
|
||||
func runMigrations(app *appContext) {
|
||||
migrateProfiles(app)
|
||||
migrateBootstrap(app)
|
||||
migrateExternalURL(app)
|
||||
migrateEmailStorage(app)
|
||||
migrateNotificationMethods(app)
|
||||
linkExistingOmbiDiscordTelegram(app)
|
||||
@@ -57,7 +60,7 @@ func migrateBootstrap(app *appContext) {
|
||||
}
|
||||
|
||||
func migrateEmailConfig(app *appContext) {
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
tempConfig, _ := ini.ShadowLoad(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 {
|
||||
@@ -108,7 +111,7 @@ func migrateEmailStorage(app *appContext) error {
|
||||
return fmt.Errorf("email address was type %T, not string: \"%+v\"\n", addr, addr)
|
||||
}
|
||||
}
|
||||
config, err := ini.Load(app.configPath)
|
||||
config, err := ini.ShadowLoad(app.configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -183,14 +186,14 @@ func linkExistingOmbiDiscordTelegram(app *appContext) error {
|
||||
idList[user.JellyfinID] = vals
|
||||
}
|
||||
for jfID, ids := range idList {
|
||||
ombiUser, status, err := app.getOmbiUser(jfID)
|
||||
if status != 200 || err != nil {
|
||||
app.debug.Printf("Failed to get Ombi user with Discord/Telegram \"%s\"/\"%s\" (%d): %v", ids[0], ids[1], status, err)
|
||||
ombiUser, err := app.getOmbiUser(jfID)
|
||||
if err != nil {
|
||||
app.debug.Printf("Failed to get Ombi user with Discord/Telegram \"%s\"/\"%s\": %v", ids[0], ids[1], err)
|
||||
continue
|
||||
}
|
||||
_, status, err = app.ombi.SetNotificationPrefs(ombiUser, ids[0], ids[1])
|
||||
if status != 200 || err != nil {
|
||||
app.debug.Printf("Failed to set prefs for Ombi user \"%s\" (%d): %v", ombiUser["userName"].(string), status, err)
|
||||
_, err = app.ombi.SetNotificationPrefs(ombiUser, ids[0], ids[1])
|
||||
if err != nil {
|
||||
app.debug.Printf("Failed to set prefs for Ombi user \"%s\": %v", ombiUser["userName"].(string), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -461,3 +464,37 @@ func intialiseCustomContent(app *appContext) {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Migrate poorly-named and duplicate "url_base" settings to the single "external jfa-go URL" setting.
|
||||
func migrateExternalURL(app *appContext) {
|
||||
tempConfig, _ := ini.ShadowLoad(app.configPath)
|
||||
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to backup config: %v", err)
|
||||
return
|
||||
}
|
||||
url1 := app.config.Section("password_resets").Key("url_base").String()
|
||||
url2 := app.config.Section("invite_emails").Key("url_base").String()
|
||||
if tempConfig.Section("ui").Key("jfa_url").String() != "" || (url1 == "" && url2 == "") {
|
||||
return
|
||||
}
|
||||
|
||||
preferred := url1
|
||||
// the PWR setting (url1) is preferred, as it has always been defined as the URL root, while
|
||||
// the invite email setting (url2) once asked for "/invite" at the end.
|
||||
if url1 == "" {
|
||||
preferred = strings.TrimSuffix(url2, "/invite")
|
||||
}
|
||||
|
||||
fmt.Println(warning("The duplicate URL Base settings in \"Invite emails\" and \"Password Resets\" have been merged into General > External jfa-go URL. A backup config has been made."))
|
||||
|
||||
tempConfig.Section("ui").Key("jfa_url").SetValue(preferred)
|
||||
app.config.Section("password_resets").DeleteKey("url_base")
|
||||
app.config.Section("invite_emails").DeleteKey("url_base")
|
||||
|
||||
err = tempConfig.SaveTo(app.configPath)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to save new config: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
92
models.go
@@ -1,10 +1,13 @@
|
||||
package main
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type stringResponse struct {
|
||||
Response string `json:"response" example:"message"`
|
||||
Error string `json:"error" example:"errorDescription"`
|
||||
Response string `json:"response" example:"message"`
|
||||
Error/*Text*/ string `json:"error" example:"No special symbols allowed."`
|
||||
// ErrorCode string `json:"error_code" example:"errorSpecialSymbols"`
|
||||
}
|
||||
|
||||
type boolResponse struct {
|
||||
@@ -31,18 +34,18 @@ type newUserDTO struct {
|
||||
|
||||
type newUserResponse struct {
|
||||
User bool `json:"user" binding:"required"` // Whether user was created successfully
|
||||
Email bool `json:"email"` // Whether welcome email was successfully sent (always true if feature is disabled
|
||||
Email bool `json:"email"` // Whether welcome email was successfully sent (always true if feature is disabled)
|
||||
Error string `json:"error"` // Optional error message.
|
||||
}
|
||||
|
||||
type deleteUserDTO struct {
|
||||
Users []string `json:"users" binding:"required"` // List of usernames to delete
|
||||
Users []string `json:"users" binding:"required"` // List of user IDs.
|
||||
Notify bool `json:"notify"` // Whether to notify users of deletion
|
||||
Reason string `json:"reason"` // Account deletion reason (for notification)
|
||||
}
|
||||
|
||||
type enableDisableUserDTO struct {
|
||||
Users []string `json:"users" binding:"required"` // List of usernames to delete
|
||||
Users []string `json:"users" binding:"required"` // List of userIDs.
|
||||
Enabled bool `json:"enabled"` // True = enable users, False = disable.
|
||||
Notify bool `json:"notify"` // Whether to notify users of deletion
|
||||
Reason string `json:"reason"` // Account deletion reason (for notification)
|
||||
@@ -86,6 +89,10 @@ type getProfilesDTO struct {
|
||||
DefaultProfile string `json:"default_profile"`
|
||||
}
|
||||
|
||||
type getProfileNamesDTO struct {
|
||||
Profiles []string `json:"profiles"` // List of profiles (name only)
|
||||
}
|
||||
|
||||
type profileChangeDTO struct {
|
||||
Name string `json:"name" example:"DefaultProfile" binding:"required"` // Name of the profile
|
||||
}
|
||||
@@ -121,8 +128,7 @@ type inviteDTO struct {
|
||||
}
|
||||
|
||||
type getInvitesDTO struct {
|
||||
Profiles []string `json:"profiles"` // List of profiles (name only)
|
||||
Invites []inviteDTO `json:"invites"` // List of invites
|
||||
Invites []inviteDTO `json:"invites"` // List of invites
|
||||
}
|
||||
|
||||
// fake DTO, if i actually used this the code would be a lot longer
|
||||
@@ -206,41 +212,6 @@ type errorListDTO map[string]map[string]string
|
||||
|
||||
type configDTO map[string]interface{}
|
||||
|
||||
// Below are for sending config
|
||||
|
||||
type meta struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Advanced bool `json:"advanced,omitempty"`
|
||||
DependsTrue string `json:"depends_true,omitempty"`
|
||||
DependsFalse string `json:"depends_false,omitempty"`
|
||||
}
|
||||
|
||||
type setting struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Required bool `json:"required"`
|
||||
Advanced bool `json:"advanced,omitempty"`
|
||||
RequiresRestart bool `json:"requires_restart"`
|
||||
Type string `json:"type"` // Type (string, number, bool, etc.)
|
||||
Value interface{} `json:"value"`
|
||||
Options [][2]string `json:"options,omitempty"`
|
||||
DependsTrue string `json:"depends_true,omitempty"` // If specified, this field is enabled when the specified bool setting is enabled.
|
||||
DependsFalse string `json:"depends_false,omitempty"` // If specified, opposite behaviour of DependsTrue.
|
||||
Style string `json:"style,omitempty"`
|
||||
}
|
||||
|
||||
type section struct {
|
||||
Meta meta `json:"meta"`
|
||||
Order []string `json:"order"`
|
||||
Settings map[string]setting `json:"settings"`
|
||||
}
|
||||
|
||||
type settings struct {
|
||||
Order []string `json:"order"`
|
||||
Sections map[string]section `json:"sections"`
|
||||
}
|
||||
|
||||
type langDTO map[string]string
|
||||
|
||||
type emailListDTO map[string]emailListEl
|
||||
@@ -475,12 +446,39 @@ type GetActivityCountDTO struct {
|
||||
}
|
||||
|
||||
type CreateBackupDTO struct {
|
||||
Size string `json:"size"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Date int64 `json:"date"`
|
||||
Size string `json:"size"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Date int64 `json:"date"`
|
||||
Commit string `json:"commit"`
|
||||
}
|
||||
|
||||
type GetBackupsDTO struct {
|
||||
Backups []CreateBackupDTO `json:"backups"`
|
||||
}
|
||||
|
||||
type ConfirmationKey struct {
|
||||
newUserDTO
|
||||
completeContactMethods []ContactMethodKey
|
||||
}
|
||||
|
||||
type ContactMethodKey struct {
|
||||
Verified bool
|
||||
PIN string
|
||||
User ContactMethodUser
|
||||
}
|
||||
|
||||
type PagePaths struct {
|
||||
// The base subfolder the app is hosted on.
|
||||
Base string `json:"Base"`
|
||||
// Those for other pages
|
||||
Admin string `json:"Admin"`
|
||||
MyAccount string `json:"MyAccount"`
|
||||
Form string `json:"Form"`
|
||||
}
|
||||
|
||||
type PagePathsDTO struct {
|
||||
PagePaths
|
||||
// The subdirectory this bit of the app is hosted on (e.g. admin is usually on "/", myacc is usually on "/my/account")
|
||||
Current string `json:"Current"`
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//go:build !tray
|
||||
// +build !tray
|
||||
|
||||
package main
|
||||
|
||||
var TRAY = false
|
||||
|
||||
func BuildTagsTray() {}
|
||||
|
||||
func RunTray() {}
|
||||
|
||||
func QuitTray() {}
|
||||
|
||||
@@ -2,4 +2,8 @@ module github.com/hrfee/jfa-go/ombi
|
||||
|
||||
replace github.com/hrfee/jfa-go/common => ../common
|
||||
|
||||
go 1.15
|
||||
go 1.18
|
||||
|
||||
require github.com/hrfee/jfa-go/common v0.0.0-20240806200606-6308db495a0a
|
||||
|
||||
require github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a // indirect
|
||||
|
||||
2
ombi/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a h1:qbXZgCqb9eaPSJfLEXczQD2lxTv6jb6silMPIWW9j6o=
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a/go.mod h1:c5HKkLayo0GrEUDlJwT12b67BL9cdPjP271Xlv/KDRQ=
|
||||
48
ombi/ombi.go
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
co "github.com/hrfee/jfa-go/common"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,11 +26,11 @@ type Ombi struct {
|
||||
userCache []map[string]interface{}
|
||||
cacheExpiry time.Time
|
||||
cacheLength int
|
||||
timeoutHandler common.TimeoutHandler
|
||||
timeoutHandler co.TimeoutHandler
|
||||
}
|
||||
|
||||
// NewOmbi returns an Ombi object.
|
||||
func NewOmbi(server, key string, timeoutHandler common.TimeoutHandler) *Ombi {
|
||||
func NewOmbi(server, key string, timeoutHandler co.TimeoutHandler) *Ombi {
|
||||
return &Ombi{
|
||||
server: server,
|
||||
key: key,
|
||||
@@ -46,6 +46,11 @@ func NewOmbi(server, key string, timeoutHandler common.TimeoutHandler) *Ombi {
|
||||
}
|
||||
}
|
||||
|
||||
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
||||
func (ombi *Ombi) SetTransport(t *http.Transport) {
|
||||
ombi.httpClient.Transport = t
|
||||
}
|
||||
|
||||
// does a GET and returns the response as a string.
|
||||
func (ombi *Ombi) getJSON(url string, params map[string]string) (string, int, error) {
|
||||
if ombi.key == "" {
|
||||
@@ -133,17 +138,19 @@ func (ombi *Ombi) put(url string, data map[string]interface{}, response bool) (s
|
||||
}
|
||||
|
||||
// ModifyUser applies the given modified user object to the corresponding user.
|
||||
func (ombi *Ombi) ModifyUser(user map[string]interface{}) (status int, err error) {
|
||||
func (ombi *Ombi) ModifyUser(user map[string]interface{}) (err error) {
|
||||
if _, ok := user["id"]; !ok {
|
||||
err = fmt.Errorf("No ID provided")
|
||||
return
|
||||
}
|
||||
var status int
|
||||
_, status, err = ombi.put(ombi.server+"/api/v1/Identity/", user, false)
|
||||
err = co.GenericErr(status, err)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteUser deletes the user corresponding to the given ID.
|
||||
func (ombi *Ombi) DeleteUser(id string) (code int, err error) {
|
||||
func (ombi *Ombi) DeleteUser(id string) (err error) {
|
||||
url := fmt.Sprintf("%s/api/v1/Identity/%s", ombi.server, id)
|
||||
req, _ := http.NewRequest("DELETE", url, nil)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
@@ -152,18 +159,19 @@ func (ombi *Ombi) DeleteUser(id string) (code int, err error) {
|
||||
}
|
||||
resp, err := ombi.httpClient.Do(req)
|
||||
defer ombi.timeoutHandler()
|
||||
return resp.StatusCode, err
|
||||
return co.GenericErr(resp.StatusCode, err)
|
||||
}
|
||||
|
||||
// UserByID returns the user corresponding to the provided ID.
|
||||
func (ombi *Ombi) UserByID(id string) (result map[string]interface{}, code int, err error) {
|
||||
func (ombi *Ombi) UserByID(id string) (result map[string]interface{}, err error) {
|
||||
resp, code, err := ombi.getJSON(fmt.Sprintf("%s/api/v1/Identity/User/%s", ombi.server, id), nil)
|
||||
err = co.GenericErr(code, err)
|
||||
json.Unmarshal([]byte(resp), &result)
|
||||
return
|
||||
}
|
||||
|
||||
// GetUsers returns all users on the Ombi instance.
|
||||
func (ombi *Ombi) GetUsers() ([]map[string]interface{}, int, error) {
|
||||
func (ombi *Ombi) GetUsers() ([]map[string]interface{}, error) {
|
||||
if time.Now().After(ombi.cacheExpiry) {
|
||||
resp, code, err := ombi.getJSON(fmt.Sprintf("%s/api/v1/Identity/Users", ombi.server), nil)
|
||||
var result []map[string]interface{}
|
||||
@@ -172,9 +180,10 @@ func (ombi *Ombi) GetUsers() ([]map[string]interface{}, int, error) {
|
||||
if (code == 200 || code == 204) && err == nil {
|
||||
ombi.cacheExpiry = time.Now().Add(time.Minute * time.Duration(ombi.cacheLength))
|
||||
}
|
||||
return result, code, err
|
||||
err = co.GenericErr(code, err)
|
||||
return result, err
|
||||
}
|
||||
return ombi.userCache, 200, nil
|
||||
return ombi.userCache, nil
|
||||
}
|
||||
|
||||
// Strip these from a user when saving as a template.
|
||||
@@ -190,9 +199,9 @@ var stripFromOmbi = []string{
|
||||
}
|
||||
|
||||
// TemplateByID returns a template based on the user corresponding to the provided ID's settings.
|
||||
func (ombi *Ombi) TemplateByID(id string) (result map[string]interface{}, code int, err error) {
|
||||
result, code, err = ombi.UserByID(id)
|
||||
if err != nil || code != 200 {
|
||||
func (ombi *Ombi) TemplateByID(id string) (result map[string]interface{}, err error) {
|
||||
result, err = ombi.UserByID(id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, key := range stripFromOmbi {
|
||||
@@ -209,24 +218,25 @@ func (ombi *Ombi) TemplateByID(id string) (result map[string]interface{}, code i
|
||||
}
|
||||
|
||||
// NewUser creates a new user with the given username, password and email address.
|
||||
func (ombi *Ombi) NewUser(username, password, email string, template map[string]interface{}) ([]string, int, error) {
|
||||
func (ombi *Ombi) NewUser(username, password, email string, template map[string]interface{}) ([]string, error) {
|
||||
url := fmt.Sprintf("%s/api/v1/Identity", ombi.server)
|
||||
user := template
|
||||
user["userName"] = username
|
||||
user["password"] = password
|
||||
user["emailAddress"] = email
|
||||
resp, code, err := ombi.post(url, user, true)
|
||||
err = co.GenericErr(code, err)
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal([]byte(resp), &data)
|
||||
if err != nil || code != 200 {
|
||||
if err != nil {
|
||||
var lst []string
|
||||
if data["errors"] != nil {
|
||||
lst = data["errors"].([]string)
|
||||
}
|
||||
return lst, code, err
|
||||
return lst, err
|
||||
}
|
||||
ombi.cacheExpiry = time.Now()
|
||||
return nil, code, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type NotificationPref struct {
|
||||
@@ -236,7 +246,7 @@ type NotificationPref struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func (ombi *Ombi) SetNotificationPrefs(user map[string]interface{}, discordID, telegramUser string) (result string, code int, err error) {
|
||||
func (ombi *Ombi) SetNotificationPrefs(user map[string]interface{}, discordID, telegramUser string) (result string, err error) {
|
||||
id := user["id"].(string)
|
||||
url := fmt.Sprintf("%s/api/v1/Identity/NotificationPreferences", ombi.server)
|
||||
data := []NotificationPref{}
|
||||
@@ -246,6 +256,8 @@ func (ombi *Ombi) SetNotificationPrefs(user map[string]interface{}, discordID, t
|
||||
if telegramUser != "" {
|
||||
data = append(data, NotificationPref{NotifAgentTelegram, id, telegramUser, true})
|
||||
}
|
||||
var code int
|
||||
result, code, err = ombi.send("POST", url, data, true, map[string]string{"UserName": user["userName"].(string)})
|
||||
err = co.GenericErr(code, err)
|
||||
return
|
||||
}
|
||||
|
||||
2076
package-lock.json
generated
@@ -27,11 +27,11 @@
|
||||
"inline-source": "^8.0.2",
|
||||
"jsdom": "^22.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mjml": "^4.14.1",
|
||||
"mjml": "^4.15.3",
|
||||
"nightwind": "^1.1.13",
|
||||
"perl-regex": "^1.0.4",
|
||||
"postcss": "^8.4.24",
|
||||
"remixicon": "^3.3.0",
|
||||
"postcss": "^8.4.31",
|
||||
"remixicon": "^4.3.0",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.1.3",
|
||||
|
||||
45
pwreset.go
@@ -2,19 +2,21 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
// GenInternalReset generates a local password reset PIN, for use with the PWR option on the Admin page.
|
||||
func (app *appContext) GenInternalReset(userID string) (InternalPWR, error) {
|
||||
pin := genAuthToken()
|
||||
user, status, err := app.jf.UserByID(userID, false)
|
||||
if err != nil || status != 200 {
|
||||
user, err := app.jf.UserByID(userID, false)
|
||||
if err != nil {
|
||||
return InternalPWR{}, err
|
||||
}
|
||||
pwr := InternalPWR{
|
||||
@@ -28,10 +30,10 @@ func (app *appContext) GenInternalReset(userID string) (InternalPWR, error) {
|
||||
|
||||
// GenResetLink generates and returns a password reset link.
|
||||
func (app *appContext) GenResetLink(pin string) (string, error) {
|
||||
url := app.config.Section("password_resets").Key("url_base").String()
|
||||
url := app.ExternalURI
|
||||
var pinLink string
|
||||
if url == "" {
|
||||
return pinLink, fmt.Errorf("disabled as no URL Base provided. Set in Settings > Password Resets.")
|
||||
return pinLink, errors.New(lm.NoExternalHost)
|
||||
}
|
||||
// Strip /invite from end of this URL, ik it's ugly.
|
||||
pinLink = fmt.Sprintf("%s/reset?pin=%s", url, pin)
|
||||
@@ -39,16 +41,16 @@ func (app *appContext) GenResetLink(pin string) (string, error) {
|
||||
}
|
||||
|
||||
func (app *appContext) StartPWR() {
|
||||
app.info.Println("Starting password reset daemon")
|
||||
app.info.Printf(lm.StartDaemon, "PWR")
|
||||
path := app.config.Section("password_resets").Key("watch_directory").String()
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
app.err.Printf("Failed to start password reset daemon: Directory \"%s\" doesn't exist", path)
|
||||
app.err.Printf(lm.FailedStartDaemon, "PWR", fmt.Sprintf(lm.PathNotFound, path))
|
||||
return
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
app.err.Printf("Couldn't initialise password reset daemon")
|
||||
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
||||
return
|
||||
}
|
||||
defer watcher.Close()
|
||||
@@ -56,7 +58,7 @@ func (app *appContext) StartPWR() {
|
||||
go pwrMonitor(app, watcher)
|
||||
err = watcher.Add(path)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to start password reset daemon: %s", err)
|
||||
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
||||
}
|
||||
|
||||
waitForRestart()
|
||||
@@ -84,43 +86,36 @@ 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)
|
||||
app.debug.Printf(lm.FailedReading, event.Name, err)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(data, &pwr)
|
||||
if len(pwr.Pin) == 0 || err != nil {
|
||||
app.debug.Printf("PWR: Failed to read PIN: %v", err)
|
||||
app.debug.Printf(lm.FailedReading, event.Name, err)
|
||||
continue
|
||||
}
|
||||
app.info.Printf("New password reset for user \"%s\"", pwr.Username)
|
||||
if currentTime := time.Now(); pwr.Expiry.After(currentTime) {
|
||||
user, status, err := app.jf.UserByName(pwr.Username, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get users from Jellyfin: Code %d", status)
|
||||
app.debug.Printf("Error: %s", err)
|
||||
user, err := app.jf.UserByName(pwr.Username, false)
|
||||
if err != nil || user.ID == "" {
|
||||
app.err.Printf(lm.FailedGetUser, pwr.Username, lm.Jellyfin, err)
|
||||
return
|
||||
}
|
||||
uid := user.ID
|
||||
if uid == "" {
|
||||
app.err.Printf("Couldn't get user ID for user \"%s\"", pwr.Username)
|
||||
return
|
||||
}
|
||||
name := app.getAddressOrName(uid)
|
||||
if name != "" {
|
||||
msg, err := app.email.constructReset(pwr, app, false)
|
||||
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct password reset message for \"%s\"", pwr.Username)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
app.err.Printf(lm.FailedConstructPWRMessage, pwr.Username, err)
|
||||
} else if err := app.sendByID(msg, uid); err != nil {
|
||||
app.err.Printf("Failed to send password reset message to \"%s\"", name)
|
||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||
app.err.Printf(lm.FailedSendPWRMessage, pwr.Username, name, err)
|
||||
} else {
|
||||
app.info.Printf("Sent password reset message to \"%s\"", name)
|
||||
app.err.Printf(lm.SentPWRMessage, pwr.Username, name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
app.err.Printf("Password reset for user \"%s\" has already expired (%s). Check your time settings.", pwr.Username, pwr.Expiry)
|
||||
app.err.Printf(lm.PWRExpired, pwr.Username, pwr.Expiry)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -128,7 +123,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
app.err.Printf("Password reset daemon: %s", err)
|
||||
app.err.Printf(lm.FailedStartDaemon, "PWR", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
)
|
||||
|
||||
func (app *appContext) HardRestart() error {
|
||||
return fmt.Errorf("hard restarts not available on windows")
|
||||
return fmt.Errorf(lm.FailedHardRestartWindows)
|
||||
}
|
||||
|
||||
53
router.go
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/gin-contrib/pprof"
|
||||
"github.com/gin-contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
@@ -21,17 +22,17 @@ func (app *appContext) loadHTML(router *gin.Engine) {
|
||||
templatePath := "html"
|
||||
htmlFiles, err := fs.ReadDir(localFS, templatePath)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Couldn't access template directory: \"%s\"", templatePath)
|
||||
app.err.Fatalf(lm.FailedReading, templatePath, err)
|
||||
return
|
||||
}
|
||||
loadInternal := []string{}
|
||||
loadExternal := []string{}
|
||||
for _, f := range htmlFiles {
|
||||
if _, err := os.Stat(filepath.Join(customPath, f.Name())); os.IsNotExist(err) {
|
||||
app.debug.Printf("Using default \"%s\"", f.Name())
|
||||
app.debug.Printf(lm.UseDefaultHTML, f.Name())
|
||||
loadInternal = append(loadInternal, FSJoin(templatePath, f.Name()))
|
||||
} else {
|
||||
app.info.Printf("Using custom \"%s\"", f.Name())
|
||||
app.info.Printf(lm.UseCustomHTML, f.Name())
|
||||
loadExternal = append(loadExternal, filepath.Join(filepath.Join(customPath, f.Name())))
|
||||
}
|
||||
}
|
||||
@@ -39,13 +40,13 @@ func (app *appContext) loadHTML(router *gin.Engine) {
|
||||
if len(loadInternal) != 0 {
|
||||
tmpl, err = template.ParseFS(localFS, loadInternal...)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to load templates: %v", err)
|
||||
app.err.Fatalf(lm.FailedLoadTemplates, lm.Internal, err)
|
||||
}
|
||||
}
|
||||
if len(loadExternal) != 0 {
|
||||
tmpl, err = tmpl.ParseFiles(loadExternal...)
|
||||
if err != nil {
|
||||
app.err.Fatalf("Failed to load external templates: %v", err)
|
||||
app.err.Fatalf(lm.FailedLoadTemplates, lm.External, err)
|
||||
}
|
||||
}
|
||||
router.SetHTMLTemplate(tmpl)
|
||||
@@ -96,7 +97,7 @@ func (app *appContext) loadRouter(address string, debug bool) *gin.Engine {
|
||||
router.Use(static.Serve("/", app.webFS))
|
||||
router.NoRoute(app.NoRouteHandler)
|
||||
if *PPROF {
|
||||
app.debug.Println("Loading pprof")
|
||||
app.debug.Println(lm.RegisterPprof)
|
||||
pprof.Register(router)
|
||||
}
|
||||
SRV = &http.Server{
|
||||
@@ -107,8 +108,8 @@ func (app *appContext) loadRouter(address string, debug bool) *gin.Engine {
|
||||
}
|
||||
|
||||
func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
routePrefixes := []string{app.URLBase}
|
||||
if app.URLBase != "" {
|
||||
routePrefixes := []string{PAGES.Base}
|
||||
if PAGES.Base != "" {
|
||||
routePrefixes = append(routePrefixes, "")
|
||||
}
|
||||
|
||||
@@ -117,7 +118,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
for _, p := range routePrefixes {
|
||||
router.GET(p+"/lang/:page", app.GetLanguages)
|
||||
router.Use(static.Serve(p+"/", app.webFS))
|
||||
router.GET(p+"/", app.AdminPage)
|
||||
router.GET(p+PAGES.Admin, app.AdminPage)
|
||||
|
||||
if app.config.Section("password_resets").Key("link_reset").MustBool(false) {
|
||||
router.GET(p+"/reset", app.ResetPassword)
|
||||
@@ -126,38 +127,39 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
}
|
||||
}
|
||||
|
||||
router.GET(p+"/accounts", app.AdminPage)
|
||||
router.GET(p+"/settings", app.AdminPage)
|
||||
router.GET(p+"/activity", app.AdminPage)
|
||||
router.GET(p+"/accounts/user/:userID", app.AdminPage)
|
||||
router.GET(p+"/invites/:code", app.AdminPage)
|
||||
router.GET(p+PAGES.Admin+"/accounts", app.AdminPage)
|
||||
router.GET(p+PAGES.Admin+"/settings", app.AdminPage)
|
||||
router.GET(p+PAGES.Admin+"/activity", app.AdminPage)
|
||||
router.GET(p+PAGES.Admin+"/accounts/user/:userID", app.AdminPage)
|
||||
router.GET(p+PAGES.Admin+"/invites/:code", app.AdminPage)
|
||||
router.GET(p+"/lang/:page/:file", app.ServeLang)
|
||||
router.GET(p+"/token/login", app.getTokenLogin)
|
||||
router.GET(p+"/token/refresh", app.getTokenRefresh)
|
||||
router.POST(p+"/newUser", app.NewUser)
|
||||
router.Use(static.Serve(p+"/invite/", app.webFS))
|
||||
router.GET(p+"/invite/:invCode", app.InviteProxy)
|
||||
router.POST(p+"/user/invite", app.NewUserFromInvite)
|
||||
router.Use(static.Serve(p+PAGES.Form, app.webFS))
|
||||
router.GET(p+PAGES.Form+"/:invCode", app.InviteProxy)
|
||||
if app.config.Section("captcha").Key("enabled").MustBool(false) {
|
||||
router.GET(p+"/captcha/gen/:invCode", app.GenCaptcha)
|
||||
router.GET(p+"/captcha/img/:invCode/:captchaID", app.GetCaptcha)
|
||||
router.POST(p+"/captcha/verify/:invCode/:captchaID/:text", app.VerifyCaptcha)
|
||||
}
|
||||
if telegramEnabled {
|
||||
router.GET(p+"/invite/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
|
||||
router.GET(p+PAGES.Form+"/:invCode/telegram/verified/:pin", app.TelegramVerifiedInvite)
|
||||
}
|
||||
if discordEnabled {
|
||||
router.GET(p+"/invite/:invCode/discord/verified/:pin", app.DiscordVerifiedInvite)
|
||||
router.GET(p+PAGES.Form+"/: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)
|
||||
router.GET(p+PAGES.Form+"/: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.GET(p+PAGES.Form+"/:invCode/matrix/verified/:userID/:pin", app.MatrixCheckPIN)
|
||||
router.POST(p+PAGES.Form+"/:invCode/matrix/user", app.MatrixSendPIN)
|
||||
router.POST(p+"/users/matrix", app.MatrixConnect)
|
||||
}
|
||||
if userPageEnabled {
|
||||
router.GET(p+"/my/account", app.MyUserPage)
|
||||
router.GET(p+PAGES.MyAccount, app.MyUserPage)
|
||||
router.GET(p+PAGES.MyAccount+"/password/reset", app.MyUserPage)
|
||||
router.GET(p+"/my/token/login", app.getUserTokenLogin)
|
||||
router.GET(p+"/my/token/refresh", app.getUserTokenRefresh)
|
||||
router.GET(p+"/my/confirm/:jwt", app.ConfirmMyAction)
|
||||
@@ -165,7 +167,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
}
|
||||
}
|
||||
if *SWAGGER {
|
||||
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
||||
app.info.Print(warning(lm.SwaggerWarning))
|
||||
for _, p := range routePrefixes {
|
||||
router.GET(p+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
@@ -181,7 +183,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
router.POST(p+"/logout", app.Logout)
|
||||
api.DELETE(p+"/users", app.DeleteUsers)
|
||||
api.GET(p+"/users", app.GetUsers)
|
||||
api.POST(p+"/users", app.NewUserAdmin)
|
||||
api.POST(p+"/user", app.NewUserFromAdmin)
|
||||
api.POST(p+"/users/extend", app.ExtendExpiry)
|
||||
api.DELETE(p+"/users/:id/expiry", app.RemoveExpiry)
|
||||
api.POST(p+"/users/enable", app.EnableDisableUsers)
|
||||
@@ -190,6 +192,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
api.DELETE(p+"/invites", app.DeleteInvite)
|
||||
api.POST(p+"/invites/profile", app.SetProfile)
|
||||
api.GET(p+"/profiles", app.GetProfiles)
|
||||
api.GET(p+"/profiles/names", app.GetProfileNames)
|
||||
api.POST(p+"/profiles/default", app.SetDefaultProfile)
|
||||
api.POST(p+"/profiles", app.CreateProfile)
|
||||
api.DELETE(p+"/profiles", app.DeleteProfile)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module github.com/hrfee/jfa-go/scripts/account-gen
|
||||
|
||||
go 1.20
|
||||
go 1.18
|
||||
|
||||
require github.com/hrfee/mediabrowser v0.3.8 // indirect
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import subprocess
|
||||
import shutil
|
||||
import os
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from multiprocessing import Process
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-o", "--output", help="output directory for .html and .txt files")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
def runcmd(cmd):
|
||||
if os.name == "nt":
|
||||
return subprocess.check_output(cmd, shell=True)
|
||||
with subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) as proc:
|
||||
return proc.communicate()
|
||||
|
||||
def compile(mjml: Path):
|
||||
fname = mjml.with_suffix(".html")
|
||||
runcmd(f"npx mjml {str(mjml)} -o {str(fname)}")
|
||||
if fname.is_file():
|
||||
print(f"Compiled {mjml.name}")
|
||||
|
||||
local_path = Path("mail")
|
||||
|
||||
threads = []
|
||||
|
||||
for mjml in [f for f in local_path.iterdir() if f.is_file() and "mjml" in f.suffix]:
|
||||
p = Process(target=compile, args=(mjml,))
|
||||
p.start()
|
||||
threads.append(p)
|
||||
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
html = [f for f in local_path.iterdir() if f.is_file() and "html" in f.suffix]
|
||||
|
||||
output = Path(args.output) # local_path.parent / "build" / "data"
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for f in html:
|
||||
shutil.copy(str(f), str(output / f.name))
|
||||
print(f"Copied {f.name} to {str(output / f.name)}")
|
||||
txtfile = f.with_suffix(".txt")
|
||||
if txtfile.is_file():
|
||||
shutil.copy(str(txtfile), str(output / txtfile.name))
|
||||
print(f"Copied {txtfile.name} to {str(output / txtfile.name)}")
|
||||
else:
|
||||
print(
|
||||
f"Warning: {txtfile.name} does not exist. Text versions of emails should be supplied."
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# scan all typescript and automatically add dark variants to color tags if they're not already present.
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Since go doesn't order its json, this script adds ordered lists
|
||||
# of section/setting names for the settings tab to use.
|
||||
import json, argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-i", "--input", help="input config base from jf-accounts")
|
||||
parser.add_argument("-o", "--output", help="output config base for jfa-go")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.input, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
newconfig = {"sections": {}, "order": []}
|
||||
|
||||
for sect in config["sections"]:
|
||||
newconfig["order"].append(sect)
|
||||
newconfig["sections"][sect] = {}
|
||||
newconfig["sections"][sect]["order"] = []
|
||||
newconfig["sections"][sect]["meta"] = config["sections"][sect]["meta"]
|
||||
newconfig["sections"][sect]["settings"] = {}
|
||||
for setting in config["sections"][sect]["settings"]:
|
||||
newconfig["sections"][sect]["order"].append(setting)
|
||||
newconfig["sections"][sect]["settings"][setting] = config["sections"][sect]["settings"][setting]
|
||||
|
||||
with open(args.output, 'w') as f:
|
||||
f.write(json.dumps(newconfig, indent=4))
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# Generates config file
|
||||
import configparser
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
def fix_description(desc):
|
||||
return "; " + desc.replace("\n", "\n; ")
|
||||
|
||||
def generate_ini(base_file, ini_file):
|
||||
"""
|
||||
Generates .ini file from config-base file.
|
||||
"""
|
||||
with open(Path(base_file), "r") as f:
|
||||
config_base = json.load(f)
|
||||
|
||||
ini = configparser.RawConfigParser(allow_no_value=True)
|
||||
|
||||
for section in config_base["sections"]:
|
||||
ini.add_section(section)
|
||||
if "meta" in config_base["sections"][section]:
|
||||
ini.set(section, fix_description(config_base["sections"][section]["meta"]["description"]))
|
||||
for entry in config_base["sections"][section]["settings"]:
|
||||
if config_base["sections"][section]["settings"][entry]["type"] == "note":
|
||||
continue
|
||||
if "description" in config_base["sections"][section]["settings"][entry]:
|
||||
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()
|
||||
else:
|
||||
value = str(value)
|
||||
ini.set(section, entry, value)
|
||||
|
||||
with open(Path(ini_file), "w") as config_file:
|
||||
ini.write(config_file)
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-i", "--input", help="input config base from jf-accounts")
|
||||
parser.add_argument("-o", "--output", help="output ini")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(generate_ini(base_file=args.input, ini_file=args.output))
|
||||
12
scripts/ini/go.mod
Normal file
@@ -0,0 +1,12 @@
|
||||
module github.com/hrfee/jfa-go/scripts/ini
|
||||
|
||||
replace github.com/hrfee/jfa-go/common => ../../common
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20240824141650-fcdd4e451882 // indirect
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
7
scripts/ini/go.sum
Normal file
@@ -0,0 +1,7 @@
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a h1:qbXZgCqb9eaPSJfLEXczQD2lxTv6jb6silMPIWW9j6o=
|
||||
github.com/hrfee/jfa-go/logmessages v0.0.0-20240806200606-6308db495a0a/go.mod h1:c5HKkLayo0GrEUDlJwT12b67BL9cdPjP271Xlv/KDRQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
130
scripts/ini/main.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
"gopkg.in/ini.v1"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func fixDescription(desc string) string {
|
||||
return "; " + strings.ReplaceAll(desc, "\n", "\n; ")
|
||||
}
|
||||
|
||||
func generateIni(yamlPath string, iniPath string) {
|
||||
yamlFile, err := os.ReadFile(yamlPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
configBase := common.Config{}
|
||||
err = yaml.Unmarshal(yamlFile, &configBase)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conf := ini.Empty()
|
||||
|
||||
for _, section := range configBase.Sections {
|
||||
cSection, err := conf.NewSection(section.Section)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if section.Meta.Description != "" {
|
||||
cSection.Comment = fixDescription(section.Meta.Description)
|
||||
}
|
||||
for _, setting := range section.Settings {
|
||||
if setting.Type == common.NoteType {
|
||||
continue
|
||||
}
|
||||
val := ""
|
||||
if setting.Value != nil {
|
||||
// Easy way to convert bools and numbers to strings,
|
||||
// Instead of checking setting.Type
|
||||
val = fmt.Sprintf("%v", setting.Value)
|
||||
}
|
||||
cKey, err := cSection.NewKey(setting.Setting, val)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if setting.Description != "" {
|
||||
cKey.Comment = fixDescription(setting.Description)
|
||||
}
|
||||
// Explain how to use list type
|
||||
if setting.Type == common.ListType {
|
||||
if cKey.Comment != "" {
|
||||
cKey.Comment += "\n"
|
||||
}
|
||||
cKey.Comment += `List type: duplicate and edit the line to add more entries.`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = conf.SaveTo(iniPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Compares two inis, used to check this script does the equivalent of the old generate_ini.py.
|
||||
func compareInis(p1, p2 string) {
|
||||
cA, err := ini.ShadowLoad(p1)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cB, err := ini.ShadowLoad(p2)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, pair := range [][2]*ini.File{{cA, cB}, {cB, cA}} {
|
||||
s1 := pair[0].Sections()
|
||||
s2 := pair[1].Sections()
|
||||
for i := range s1 {
|
||||
if s1[i].Name() != s2[i].Name() {
|
||||
panic(fmt.Errorf("mismatching section order: s0[i]=%s, s1[i]=%s", s1[i].Name(), s2[i].Name()))
|
||||
}
|
||||
// fmt.Println("Section order matches")
|
||||
st1 := s1[i].Keys()
|
||||
st2 := s2[i].Keys()
|
||||
for i := range st1 {
|
||||
if st1[i].Name() != st2[i].Name() {
|
||||
panic(fmt.Errorf("mismatching setting order: st1[i]=%s, st2[i]=%s", st1[i].Name(), st2[i].Name()))
|
||||
}
|
||||
if st1[i].Value() != st2[i].Value() {
|
||||
panic(fmt.Errorf("mismatching setting values: st1[i]=%s, st2[i]=%s", st1[i].Value(), st2[i].Value()))
|
||||
}
|
||||
// fmt.Println("Setting matches")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
var yamlPath string
|
||||
var iniPath string
|
||||
var comparePath string
|
||||
flag.StringVar(&yamlPath, "in", "", "Input of the config base in yaml.")
|
||||
flag.StringVar(&iniPath, "out", "", "Output path of an ini file.")
|
||||
flag.StringVar(&comparePath, "comp", "", "Path to ini file to compare against.")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if yamlPath == "" {
|
||||
panic(errors.New("invalid yaml path"))
|
||||
}
|
||||
if iniPath == "" {
|
||||
panic(errors.New("invalid ini path"))
|
||||
}
|
||||
|
||||
generateIni(yamlPath, iniPath)
|
||||
|
||||
if comparePath != "" {
|
||||
compareInis(iniPath, comparePath)
|
||||
fmt.Println("Passed.")
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ function fixHTML(infile, outfile) {
|
||||
}
|
||||
}
|
||||
let doc = new parser.load(f);
|
||||
for (let item of ["badge", "chip", "shield", "input", "table", "button", "portal", "select", "aside", "card", "field", "textarea"]) {
|
||||
for (let item of ["badge", "chip", "shield", "input", "table", "button", "portal", "select", "aside", "card", "field", "textarea", "section"]) {
|
||||
let items = doc("."+item);
|
||||
items.each((i, elem) => {
|
||||
let hasColor = false;
|
||||
@@ -50,8 +50,8 @@ function fixHTML(infile, outfile) {
|
||||
}
|
||||
if (!hasColor) {
|
||||
if (!hasDark(doc(elem))) {
|
||||
// card without ~neutral look different than with.
|
||||
if (item != "card") doc(elem).addClass("~neutral");
|
||||
// card (and sections in sectioned cards) without ~neutral look different than with.
|
||||
if (item != "card" && item != "section") doc(elem).addClass("~neutral");
|
||||
doc(elem).addClass("dark:~d_neutral");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
# sets version environment variable for goreleaser to use
|
||||
# scripts/version.sh goreleaser ...
|
||||
|
||||
@@ -15,6 +15,22 @@ else
|
||||
export JFA_GO_MINIFY=""
|
||||
fi
|
||||
|
||||
if [[ -z "${INTERNAL}" ]]; then
|
||||
export INTERNAL=on
|
||||
fi
|
||||
if [[ "${INTERNAL}" == "on" ]]; then
|
||||
export JFA_GO_TAG=""
|
||||
else
|
||||
export JFA_GO_TAG="external"
|
||||
fi
|
||||
|
||||
if [[ -z "${UPDATER}" ]]; then
|
||||
export UPDATER=on
|
||||
export JFA_GO_UPDATER=binary
|
||||
else
|
||||
export JFA_GO_UPDATER=$UPDATER
|
||||
fi
|
||||
|
||||
JFA_GO_VERSION=$(git describe --exact-match HEAD 2> /dev/null || echo 'vgit')
|
||||
TIMEOUT=60m
|
||||
|
||||
|
||||
39
setup.go
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/easyproxy"
|
||||
lm "github.com/hrfee/jfa-go/logmessages"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
)
|
||||
|
||||
@@ -38,9 +39,12 @@ func (app *appContext) ServeSetup(gc *gin.Context) {
|
||||
respond(500, "Failed to fetch default values", gc)
|
||||
return
|
||||
}
|
||||
pages := PagePathsDTO{PagePaths: PAGES}
|
||||
gc.HTML(200, "setup.html", gin.H{
|
||||
"cssVersion": cssVersion,
|
||||
"pages": pages,
|
||||
"lang": app.storage.lang.Setup[lang],
|
||||
"strings": app.storage.lang.Setup[lang].Strings,
|
||||
"emailLang": app.storage.lang.Email[emailLang],
|
||||
"language": app.storage.lang.Setup[lang].JSON,
|
||||
"messages": string(msg),
|
||||
@@ -90,21 +94,24 @@ func (app *appContext) TestJF(gc *gin.Context) {
|
||||
tempjf.SetTransport(transport)
|
||||
}
|
||||
|
||||
user, status, err := tempjf.Authenticate(req.Username, req.Password)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
user, err := tempjf.Authenticate(req.Username, req.Password)
|
||||
if err != nil {
|
||||
msg := ""
|
||||
switch status {
|
||||
case 0:
|
||||
msg = "errorConnectionRefused"
|
||||
status = 500
|
||||
case 401:
|
||||
status := 500
|
||||
switch err.(type) {
|
||||
case mediabrowser.ErrUnauthorized:
|
||||
msg = "errorInvalidUserPass"
|
||||
case 403:
|
||||
status = 401
|
||||
case mediabrowser.ErrForbidden:
|
||||
msg = "errorUserDisabled"
|
||||
case 404:
|
||||
status = 403
|
||||
case mediabrowser.ErrNotFound:
|
||||
msg = "error404"
|
||||
status = 404
|
||||
default:
|
||||
msg = "errorConnectionRefused"
|
||||
}
|
||||
app.info.Printf("Auth failed with code %d (%s)", status, err)
|
||||
app.err.Printf(lm.FailedAuthJellyfin, req.Server, status, err)
|
||||
if msg != "" {
|
||||
respond(status, msg, gc)
|
||||
} else {
|
||||
@@ -151,16 +158,20 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
|
||||
loadedLangs[fsIndex][lang.Meta.Fallback+".json"] = true
|
||||
patchLang(&lang.Strings, &fallback.Strings, &english.Strings)
|
||||
patchLang(&lang.StartPage, &fallback.StartPage, &english.StartPage)
|
||||
patchLang(&lang.EndPage, &fallback.EndPage, &english.EndPage)
|
||||
patchLang(&lang.General, &fallback.General, &english.General)
|
||||
patchLang(&lang.Updates, &fallback.Updates, &english.Updates)
|
||||
patchLang(&lang.Proxy, &fallback.Proxy, &english.Proxy)
|
||||
patchLang(&lang.EndPage, &fallback.EndPage, &english.EndPage)
|
||||
patchLang(&lang.Language, &fallback.Language, &english.Language)
|
||||
patchLang(&lang.Login, &fallback.Login, &english.Login)
|
||||
patchLang(&lang.JellyfinEmby, &fallback.JellyfinEmby, &english.JellyfinEmby)
|
||||
patchLang(&lang.Ombi, &fallback.Ombi, &english.Ombi)
|
||||
patchLang(&lang.Jellyseerr, &fallback.Jellyseerr, &english.Jellyseerr)
|
||||
patchLang(&lang.Email, &fallback.Email, &english.Email)
|
||||
patchLang(&lang.Messages, &fallback.Messages, &english.Messages)
|
||||
patchLang(&lang.Notifications, &fallback.Notifications, &english.Notifications)
|
||||
patchLang(&lang.UserPage, &fallback.UserPage, &english.UserPage)
|
||||
patchLang(&lang.WelcomeEmails, &fallback.WelcomeEmails, &english.WelcomeEmails)
|
||||
patchLang(&lang.PasswordResets, &fallback.PasswordResets, &english.PasswordResets)
|
||||
patchLang(&lang.InviteEmails, &fallback.InviteEmails, &english.InviteEmails)
|
||||
patchLang(&lang.PasswordValidation, &fallback.PasswordValidation, &english.PasswordValidation)
|
||||
@@ -170,16 +181,20 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
|
||||
if (lang.Meta.Fallback != "" && err != nil) || lang.Meta.Fallback == "" {
|
||||
patchLang(&lang.Strings, &english.Strings)
|
||||
patchLang(&lang.StartPage, &english.StartPage)
|
||||
patchLang(&lang.EndPage, &english.EndPage)
|
||||
patchLang(&lang.General, &english.General)
|
||||
patchLang(&lang.Updates, &english.Updates)
|
||||
patchLang(&lang.Proxy, &english.Proxy)
|
||||
patchLang(&lang.EndPage, &english.EndPage)
|
||||
patchLang(&lang.Language, &english.Language)
|
||||
patchLang(&lang.Login, &english.Login)
|
||||
patchLang(&lang.JellyfinEmby, &english.JellyfinEmby)
|
||||
patchLang(&lang.Ombi, &english.Ombi)
|
||||
patchLang(&lang.Jellyseerr, &english.Jellyseerr)
|
||||
patchLang(&lang.Email, &english.Email)
|
||||
patchLang(&lang.Messages, &english.Messages)
|
||||
patchLang(&lang.Notifications, &english.Notifications)
|
||||
patchLang(&lang.UserPage, &english.UserPage)
|
||||
patchLang(&lang.WelcomeEmails, &english.WelcomeEmails)
|
||||
patchLang(&lang.PasswordResets, &english.PasswordResets)
|
||||
patchLang(&lang.InviteEmails, &english.InviteEmails)
|
||||
patchLang(&lang.PasswordValidation, &english.PasswordValidation)
|
||||
|
||||
@@ -52,7 +52,7 @@ sudo apt-get install jfa-go-tray
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-container" id="page-container">
|
||||
<div class="page-container m-2 lg:my-20 lg:mx-72" id="page-container">
|
||||
<div class="card ~neutral @low mb-1">
|
||||
<div class="row col flex center">
|
||||
<span class="heading welcome">jellyfin-accounts (go)</span>
|
||||
|
||||
805
site/package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"a17t": "^0.10.1",
|
||||
"esbuild": "^0.18.1 ",
|
||||
"esbuild": "^0.25.0",
|
||||
"remixicon": "^3.3.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"uncss": "^0.17.3"
|
||||
|
||||