Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9b11012bc | ||
|
|
e7cb1f516b | ||
|
|
375022ba95 | ||
|
|
75fdf6ec3d | ||
|
|
59ebf52fe2 | ||
|
|
89fb3fa619 | ||
|
|
9bd6abadf4 | ||
|
|
4e826f4167 | ||
|
|
e97b90d4d7 | ||
|
|
fb6256d1ed | ||
|
|
7035a3fe9c | ||
|
|
62c29d55cc | ||
|
|
a83dbcf3ab | ||
|
|
e48bdcc45b | ||
|
|
0b473ef01f | ||
|
|
e03525a1d1 | ||
|
|
087172c79e | ||
|
|
8fd919bf04 | ||
|
|
2ad84db482 | ||
|
|
85536ff79e | ||
|
|
8b62c91d13 | ||
|
|
e7d1693517 | ||
|
|
e78b4882b3 | ||
|
|
e01144950b | ||
|
|
86ef665b12 | ||
|
|
f419a57e6d | ||
|
|
d7e8ec95de | ||
|
|
5a9bc1c66f | ||
|
|
ac842e6273 | ||
|
|
478b40d0ff | ||
|
|
8b816dc725 | ||
|
|
81a58f628b | ||
|
|
23800bb892 | ||
|
|
b47cb91f55 | ||
|
|
2d9e3fbc1d | ||
|
|
bf67e27737 | ||
|
|
3427c97e3e | ||
|
|
81e69a7166 | ||
|
|
564098b9d8 | ||
|
|
ec659174fb | ||
|
|
1a42d8280c | ||
|
|
b14f10d79d | ||
|
|
ee8facd1bf | ||
|
|
811657b553 | ||
|
|
95936f7c29 | ||
|
|
613d4cd9af | ||
|
|
7beb3d9974 | ||
|
|
6f2bb7f0b5 | ||
|
|
315b5fda91 | ||
|
|
a6aa89e502 | ||
|
|
e931c09a34 |
20
.drone.yml
@@ -10,6 +10,9 @@ steps:
|
||||
- git fetch --tags
|
||||
- name: release
|
||||
image: golang:latest
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
path: /id_rsa
|
||||
environment:
|
||||
BUILDRONE_KEY:
|
||||
from_secret: BUILDRONE_KEY
|
||||
@@ -26,7 +29,13 @@ steps:
|
||||
- ./scripts/version.sh ../goreleaser
|
||||
- wget https://builds.hrfee.pw/upload.py -P ../
|
||||
- pip3 install requests
|
||||
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
|
||||
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
|
||||
- bash -c 'python3 ../upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
host:
|
||||
path: /root/.ssh/id_rsa_packaging
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
@@ -73,6 +82,9 @@ type: docker
|
||||
steps:
|
||||
- name: build
|
||||
image: golang:latest
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
path: /id_rsa
|
||||
commands:
|
||||
- apt-get update -y
|
||||
- apt-get install build-essential python3-pip curl software-properties-common sed upx gcc libgtk-3-dev libappindicator3-dev gcc-mingw-w64-x86-64 -y
|
||||
@@ -83,11 +95,17 @@ steps:
|
||||
- ./scripts/version.sh ./goreleaser --snapshot --skip-publish --rm-dist
|
||||
- wget https://builds.hrfee.pw/upload.py
|
||||
- pip3 install requests
|
||||
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.zip ./dist/*.deb ./dist/*.rpm ./dist/*.apk --tag internal-git=true'
|
||||
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
|
||||
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
|
||||
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.zip ./dist/*.rpm ./dist/*.apk --tag internal-git=true'
|
||||
environment:
|
||||
BUILDRONE_KEY:
|
||||
from_secret: BUILDRONE_KEY
|
||||
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
host:
|
||||
path: /root/.ssh/id_rsa_packaging
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
|
||||
@@ -50,7 +50,7 @@ builds:
|
||||
flags:
|
||||
- -tags=tray
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary
|
||||
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary -H=windowsgui
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
@@ -110,7 +110,8 @@ changelog:
|
||||
- '^test:'
|
||||
nfpms:
|
||||
- id: notray
|
||||
file_name_template: '{{ .ProjectName }}_{{ .Arch }}_v{{ .Version }}'
|
||||
file_name_template: '{{ .ProjectName }}{{ if .IsSnapshot }}-git{{ end }}_{{ .Arch }}_{{ if .IsSnapshot }}{{ .ShortCommit }}{{ else }}v{{ .Version }}{{ end }}'
|
||||
package_name: jfa-go
|
||||
homepage: https://github.com/hrfee/jfa-go
|
||||
description: A web app for managing users on Jellyfin
|
||||
maintainer: Harvey Tindall <hrfee@hrfee.dev>
|
||||
@@ -127,7 +128,8 @@ nfpms:
|
||||
- deb
|
||||
- rpm
|
||||
- id: tray
|
||||
file_name_template: '{{ .ProjectName }}_TrayIcon_{{ .Arch }}_v{{ .Version }}'
|
||||
file_name_template: '{{ .ProjectName }}{{ if .IsSnapshot }}-git{{ end }}_TrayIcon_{{ .Arch }}_{{ if .IsSnapshot }}{{ .ShortCommit }}{{ else }}v{{ .Version }}{{ end }}'
|
||||
package_name: jfa-go-tray
|
||||
homepage: https://github.com/hrfee/jfa-go
|
||||
description: A web app for managing users on Jellyfin
|
||||
maintainer: Harvey Tindall <hrfee@hrfee.dev>
|
||||
@@ -145,6 +147,10 @@ nfpms:
|
||||
- rpm
|
||||
overrides:
|
||||
deb:
|
||||
conflicts:
|
||||
- jfa-go
|
||||
replaces:
|
||||
- jfa-go
|
||||
dependencies:
|
||||
- libappindicator3-1
|
||||
rpm:
|
||||
@@ -153,4 +159,3 @@ nfpms:
|
||||
apk:
|
||||
dependencies:
|
||||
- libappindicator
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#### Code
|
||||
I use 4 spaces for indentation. Go should ideally be formatted with `goimports` and/or `gofmt`. I don't use a formatter on typescript, so don't worry about that.
|
||||
|
||||
Code in Go should ideally use `PascalCase` for exported values, and `camelCase` for non-exported, JSON for transferring data should use `snake_case`, and Typescript should use `camelCase`. Forgive me for my many inconsistencies in this, and feel free to fix them if you want.
|
||||
|
||||
Functions in Go that need to access `*appContext` should be generally be receivers, except when the behaviour could be seen as somewhat independent from it (`email.go` is the best example, its behaviour is broadly independent from the main app except from a couple config values).
|
||||
|
||||
|
||||
#### Compiling
|
||||
|
||||
Prefix each of these with `make DEBUG=on INTERNAL=off `:
|
||||
|
||||
9
Makefile
@@ -52,6 +52,13 @@ else
|
||||
TYPECHECK :=
|
||||
endif
|
||||
|
||||
RACE ?= off
|
||||
ifeq ($(RACE), on)
|
||||
RACEDETECTOR := -race
|
||||
else
|
||||
RACEDETECTOR :=
|
||||
endif
|
||||
|
||||
npm:
|
||||
$(info installing npm dependencies)
|
||||
npm install
|
||||
@@ -91,7 +98,7 @@ compile:
|
||||
$(GOBINARY) mod download
|
||||
$(info Building)
|
||||
mkdir -p build
|
||||
$(GOBINARY) build -ldflags="$(LDFLAGS)" $(TAGS) -o build/jfa-go
|
||||
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o build/jfa-go
|
||||
|
||||
compress:
|
||||
upx --lzma build/jfa-go
|
||||
|
||||
74
README.md
@@ -4,12 +4,15 @@
|
||||
[](https://weblate.hrfee.pw/engage/jfa-go/)
|
||||
|
||||
##### Downloads:
|
||||
##### [dockerhub](https://hub.docker.com/r/hrfee/jfa-go) | [stable](https://github.com/hrfee/jfa-go/releases) | [nightly](https://builds.hrfee.pw/view/hrfee/jfa-go) | [aur stable](https://aur.archlinux.org/packages/jfa-go) | [aur binary](https://aur.archlinux.org/packages/jfa-go-bin) | [aur nightly](https://aur.archlinux.org/packages/jfa-go-git)
|
||||
##### [docker](#docker) | [debian/ubuntu](#debian) | [arch (aur)](#aur) | [other platforms](#other-platforms)
|
||||
---
|
||||
jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) (and now [Emby](https://emby.media/)) that provides invite-based account creation as well as other features that make one's instance much easier to manage.
|
||||
|
||||
a rewrite of [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) (original naming for both, ik
|
||||
😂).
|
||||
|
||||
#### Features
|
||||
* 🧑 Invite based account creation: Sends invites to your friends or family, and let them choose their own username and password without relying on you.
|
||||
* 🧑 Invite based account creation: Send invites to your friends or family, and let them choose their own username and password without relying on you.
|
||||
* Send invites via a link and/or email
|
||||
* Granular control over invites: Validity period as well as number of uses can be specified.
|
||||
* Account profiles: Assign settings profiles to invites so new users have your predefined permissions, homescreen layout, etc. applied to their account on creation.
|
||||
@@ -17,7 +20,7 @@ jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jelly
|
||||
* ⌛ User expiry: Specify a validity period, and new users accounts will be disabled/deleted after it. The period can be manually extended too.
|
||||
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
|
||||
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
|
||||
* Telegram Integration: Verify users via telegram, and send Password Resets, Announcements, etc. through it.
|
||||
* Telegram & Discord Integration: Verify users via a Telegram or Discord bot, and send Password Resets, Announcements, etc. through it.
|
||||
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
||||
* Email addresses can optionally be used instead of usernames
|
||||
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email/telegram.
|
||||
@@ -43,7 +46,9 @@ jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jelly
|
||||
|
||||
#### Install
|
||||
|
||||
The [Docker](https://hub.docker.com/r/hrfee/jfa-go) image is your best bet.
|
||||
**Note**: `TrayIcon` builds include a tray icon to start/stop/restart, and an option to automatically start when you log-in to your computer. For Linux users, these builds depend on the `libappindicator3-1`/`libappindicator-gtk3`/`libappindicator` package for Debian/Ubuntu, Fedora, and Alpine respectively.
|
||||
|
||||
##### [Docker](https://hub.docker.com/r/hrfee/jfa-go)
|
||||
```sh
|
||||
docker create \
|
||||
--name "jfa-go" \ # Whatever you want to name it
|
||||
@@ -54,9 +59,41 @@ docker create \
|
||||
-v /etc/localtime:/etc/localtime:ro \ # Makes sure time is correct
|
||||
hrfee/jfa-go # hrfee/jfa-go:unstable for latest build from git
|
||||
```
|
||||
Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/), [jfa-go-bin](https://aur.archlinux.org/packages/jfa-go) or [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/).
|
||||
|
||||
For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.dev/view/hrfee/jfa-go)), and extract the `jfa-go` executable to somewhere useful.
|
||||
##### [Debian/Ubuntu](https://apt.hrfee.dev)
|
||||
```sh
|
||||
sudo apt-get update && sudo apt-get install curl apt-transport-https gnupg
|
||||
curl https://apt.hrfee.dev/hrfee.pubkey.gpg | sudo apt-key add -
|
||||
|
||||
# For stable releases
|
||||
echo "deb https://apt.hrfee.dev trusty main" | sudo tee /etc/apt/sources.list.d/hrfee.list
|
||||
# ------
|
||||
# For unstable releases
|
||||
echo "deb https://apt.hrfee.dev trusty-unstable main" | sudo tee /etc/apt/sources.list.d/hrfee.list
|
||||
# ------
|
||||
|
||||
sudo apt-get update
|
||||
|
||||
# For servers
|
||||
sudo apt-get install jfa-go
|
||||
# ------
|
||||
# For desktops/servers with GUI (has dependencies)
|
||||
sudo apt-get install jfa-go-tray
|
||||
# ------
|
||||
```
|
||||
|
||||
##### Arch
|
||||
Available on the AUR as:
|
||||
* [jfa-go](https://aur.archlinux.org/packages/jfa-go/) (stable)
|
||||
* [jfa-go-bin](https://aur.archlinux.org/packages/jfa-go) (pre-compiled, stable)
|
||||
* [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/) (nightly)
|
||||
|
||||
##### Other platforms
|
||||
Download precompiled binaries from:
|
||||
* [The releases section](https://github.com/hrfee/jfa-go/releases) (stable)
|
||||
* [Buildrone](https://builds.hrfee.dev/view/hrfee/jfa-go) (nightly)
|
||||
|
||||
unzip the `jfa-go`/`jfa-go.exe` executable to somewhere useful.
|
||||
* For \*nix/macOS users, `chmod +x jfa-go` then place it somewhere in your PATH like `/usr/bin`.
|
||||
|
||||
Run the executable to start.
|
||||
@@ -71,17 +108,28 @@ Otherwise, full build instructions can be found [here](https://github.com/hrfee/
|
||||
Simply run `jfa-go` to start the application. A setup wizard will start on `localhost:8056` (or your own specified address). Upon completion, refresh the page.
|
||||
|
||||
```
|
||||
Usage of ./jfa-go:
|
||||
-config string
|
||||
alternate path to config file. (default "~/.config/jfa-go/config.ini")
|
||||
-data string
|
||||
alternate path to data directory. (default "~/.config/jfa-go")
|
||||
Usage of jfa-go:
|
||||
start
|
||||
start jfa-go as a daemon and run in the background.
|
||||
stop
|
||||
stop a daemonized instance of jfa-go.
|
||||
systemd
|
||||
generate a systemd .service file.
|
||||
|
||||
-config, -c string
|
||||
alternate path to config file. (default "/home/hrfee/.config/jfa-go/config.ini")
|
||||
-data, -d string
|
||||
alternate path to data directory. (default "/home/hrfee/.config/jfa-go")
|
||||
-debug
|
||||
Enables debug logging and exposes pprof.
|
||||
Enables debug logging.
|
||||
-help, -h
|
||||
prints this message.
|
||||
-host string
|
||||
alternate address to host web ui on.
|
||||
-port int
|
||||
-port, -p int
|
||||
alternate port to host web ui on.
|
||||
-pprof
|
||||
Exposes pprof profiler on /debug/pprof.
|
||||
-swagger
|
||||
Enable swagger at /swagger/index.html
|
||||
```
|
||||
|
||||
245
api.go
@@ -354,6 +354,34 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
}
|
||||
}
|
||||
}
|
||||
var matrixUser MatrixUser
|
||||
matrixVerified := false
|
||||
if matrixEnabled {
|
||||
if req.MatrixPIN == "" {
|
||||
if app.config.Section("matrix").Key("required").MustBool(false) {
|
||||
f = func(gc *gin.Context) {
|
||||
app.debug.Printf("%s: New user failed: Matrix verification not completed", req.Code)
|
||||
respond(401, "errorMatrixVerification", gc)
|
||||
}
|
||||
success = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
user, ok := app.matrix.tokens[req.MatrixPIN]
|
||||
if !ok || !user.Verified {
|
||||
matrixVerified = false
|
||||
f = func(gc *gin.Context) {
|
||||
app.debug.Printf("%s: New user failed: Matrix PIN was invalid", req.Code)
|
||||
respond(401, "errorInvalidPIN", gc)
|
||||
}
|
||||
success = false
|
||||
return
|
||||
}
|
||||
matrixVerified = user.Verified
|
||||
matrixUser = *user.User
|
||||
|
||||
}
|
||||
}
|
||||
telegramTokenIndex := -1
|
||||
if telegramEnabled {
|
||||
if req.TelegramPIN == "" {
|
||||
@@ -536,7 +564,17 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
|
||||
app.telegram.verifiedTokens = app.telegram.verifiedTokens[:len(app.telegram.verifiedTokens)-1]
|
||||
}
|
||||
}
|
||||
|
||||
if matrixVerified {
|
||||
matrixUser.Contact = req.MatrixContact
|
||||
delete(app.matrix.tokens, req.MatrixPIN)
|
||||
if app.storage.matrix == nil {
|
||||
app.storage.matrix = map[string]MatrixUser{}
|
||||
}
|
||||
app.storage.matrix[user.ID] = matrixUser
|
||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||
app.err.Printf("Failed to store Matrix users: %v", err)
|
||||
}
|
||||
}
|
||||
if (emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "") || telegramTokenIndex != -1 || discordVerified {
|
||||
name := app.getAddressOrName(user.ID)
|
||||
app.debug.Printf("%s: Sending welcome message to %s", req.Username, name)
|
||||
@@ -1239,10 +1277,14 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
user.Telegram = tgUser.Username
|
||||
user.NotifyThroughTelegram = tgUser.Contact
|
||||
}
|
||||
if dc, ok := app.storage.discord[jfUser.ID]; ok {
|
||||
user.Discord = dc.Username + "#" + dc.Discriminator
|
||||
user.DiscordID = dc.ID
|
||||
user.NotifyThroughDiscord = dc.Contact
|
||||
if mxUser, ok := app.storage.matrix[jfUser.ID]; ok {
|
||||
user.Matrix = mxUser.UserID
|
||||
user.NotifyThroughMatrix = mxUser.Contact
|
||||
}
|
||||
if dcUser, ok := app.storage.discord[jfUser.ID]; ok {
|
||||
user.Discord = dcUser.Username + "#" + dcUser.Discriminator
|
||||
user.DiscordID = dcUser.ID
|
||||
user.NotifyThroughDiscord = dcUser.Contact
|
||||
}
|
||||
resp.UserList[i] = user
|
||||
i++
|
||||
@@ -1498,6 +1540,8 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
resp.Sections["email"].Settings["language"] = el
|
||||
resp.Sections["password_resets"].Settings["language"] = pl
|
||||
resp.Sections["telegram"].Settings["language"] = tl
|
||||
resp.Sections["discord"].Settings["language"] = tl
|
||||
resp.Sections["matrix"].Settings["language"] = tl
|
||||
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
@@ -1506,6 +1550,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
||||
// @Produce json
|
||||
// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Router /config [post]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
@@ -1531,7 +1576,11 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
tempConfig.SaveTo(app.configPath)
|
||||
if err := tempConfig.SaveTo(app.configPath); err != nil {
|
||||
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.debug.Println("Config saved")
|
||||
gc.JSON(200, map[string]bool{"success": true})
|
||||
if req["restart-program"] != nil && req["restart-program"].(bool) {
|
||||
@@ -2089,6 +2138,7 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
if tgUser, ok := app.storage.telegram[req.ID]; ok {
|
||||
change := tgUser.Contact != req.Telegram
|
||||
tgUser.Contact = req.Telegram
|
||||
app.storage.telegram[req.ID] = tgUser
|
||||
if err := app.storage.storeTelegramUsers(); err != nil {
|
||||
@@ -2096,13 +2146,16 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
app.err.Printf("Telegram: Failed to store users: %v", err)
|
||||
return
|
||||
}
|
||||
msg := ""
|
||||
if !req.Telegram {
|
||||
msg = " not"
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Telegram {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
|
||||
}
|
||||
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
|
||||
}
|
||||
if dcUser, ok := app.storage.discord[req.ID]; ok {
|
||||
change := dcUser.Contact != req.Discord
|
||||
dcUser.Contact = req.Discord
|
||||
app.storage.discord[req.ID] = dcUser
|
||||
if err := app.storage.storeDiscordUsers(); err != nil {
|
||||
@@ -2110,13 +2163,33 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
app.err.Printf("Discord: Failed to store users: %v", err)
|
||||
return
|
||||
}
|
||||
msg := ""
|
||||
if !req.Discord {
|
||||
msg = " not"
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Discord {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||
}
|
||||
}
|
||||
if mxUser, ok := app.storage.matrix[req.ID]; ok {
|
||||
change := mxUser.Contact != req.Matrix
|
||||
mxUser.Contact = req.Matrix
|
||||
app.storage.matrix[req.ID] = mxUser
|
||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||
respondBool(500, false, gc)
|
||||
app.err.Printf("Matrix: Failed to store users: %v", err)
|
||||
return
|
||||
}
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Matrix {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
|
||||
}
|
||||
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||
}
|
||||
if email, ok := app.storage.emails[req.ID]; ok {
|
||||
change := email.Contact != req.Email
|
||||
email.Contact = req.Email
|
||||
app.storage.emails[req.ID] = email
|
||||
if err := app.storage.storeEmails(); err != nil {
|
||||
@@ -2124,11 +2197,13 @@ func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
app.err.Printf("Failed to store emails: %v", err)
|
||||
return
|
||||
}
|
||||
msg := ""
|
||||
if !req.Email {
|
||||
msg = " not"
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Email {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
|
||||
}
|
||||
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
@@ -2233,6 +2308,140 @@ 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.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID."
|
||||
// @Router /invite/{invCode}/matrix/user [post]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
var req MatrixSendPINDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.UserID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
ok := app.matrix.SendStart(req.UserID)
|
||||
if !ok {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Check whether a matrix PIN is valid, and mark the token as verified if so. Requires invite code.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Param userID path string true "Matrix User ID"
|
||||
// @Router /invite/{invCode}/matrix/verified/{userID}/{pin} [get]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.invites[code]; !ok {
|
||||
app.debug.Println("Matrix: Invite code was invalid")
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
userID := gc.Param("userID")
|
||||
pin := gc.Param("pin")
|
||||
user, ok := app.matrix.tokens[pin]
|
||||
if !ok {
|
||||
app.debug.Println("Matrix: PIN not found")
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
if user.User.UserID != userID {
|
||||
app.debug.Println("Matrix: User ID of PIN didn't match")
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
user.Verified = true
|
||||
app.matrix.tokens[pin] = user
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Generates a Matrix access token from a username and password.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} stringResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param MatrixLoginDTO body MatrixLoginDTO true "Username & password."
|
||||
// @Router /matrix/login [post]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixLogin(gc *gin.Context) {
|
||||
var req MatrixLoginDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.Username == "" || req.Password == "" {
|
||||
respond(400, "errorLoginBlank", gc)
|
||||
return
|
||||
}
|
||||
token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password)
|
||||
if err != nil {
|
||||
app.err.Printf("Matrix: Failed to generate token: %v", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
matrix := tempConfig.Section("matrix")
|
||||
matrix.Key("enabled").SetValue("true")
|
||||
matrix.Key("homeserver").SetValue(req.Homeserver)
|
||||
matrix.Key("token").SetValue(token)
|
||||
matrix.Key("user_id").SetValue(req.Username)
|
||||
if err := tempConfig.SaveTo(app.configPath); err != nil {
|
||||
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Links a Matrix user to a Jellyfin account via user IDs. Notifications are turned on by default.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param MatrixConnectUserDTO body MatrixConnectUserDTO true "User's Jellyfin ID & Matrix user ID."
|
||||
// @Router /users/matrix [post]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixConnect(gc *gin.Context) {
|
||||
var req MatrixConnectUserDTO
|
||||
gc.BindJSON(&req)
|
||||
if app.storage.matrix == nil {
|
||||
app.storage.matrix = map[string]MatrixUser{}
|
||||
}
|
||||
roomID, err := app.matrix.CreateRoom(req.UserID)
|
||||
if err != nil {
|
||||
app.err.Printf("Matrix: Failed to create room: %v", err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.storage.matrix[req.JellyfinID] = MatrixUser{
|
||||
UserID: req.UserID,
|
||||
RoomID: roomID,
|
||||
Lang: "en-us",
|
||||
Contact: true,
|
||||
}
|
||||
if err := app.storage.storeMatrixUsers(); err != nil {
|
||||
app.err.Printf("Failed to store Matrix users: %v", err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
|
||||
// @Produce json
|
||||
// @Success 200 {object} DiscordUsersDTO
|
||||
|
||||
@@ -15,6 +15,7 @@ var emailEnabled = false
|
||||
var messagesEnabled = false
|
||||
var telegramEnabled = false
|
||||
var discordEnabled = false
|
||||
var matrixEnabled = false
|
||||
|
||||
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
||||
val := app.config.Section(sect).Key(key).MustString("")
|
||||
@@ -43,7 +44,7 @@ func (app *appContext) loadConfig() error {
|
||||
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users"} {
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users"} {
|
||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
||||
}
|
||||
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
|
||||
@@ -83,22 +84,26 @@ func (app *appContext) loadConfig() error {
|
||||
app.MustSetValue("user_expiry", "email_html", "jfa-go:"+"user-expired.html")
|
||||
app.MustSetValue("user_expiry", "email_text", "jfa-go:"+"user-expired.txt")
|
||||
|
||||
app.MustSetValue("matrix", "topic", "Jellyfin notifications")
|
||||
|
||||
app.config.Section("jellyfin").Key("version").SetValue(version)
|
||||
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
|
||||
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
|
||||
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
|
||||
discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false)
|
||||
matrixEnabled = app.config.Section("matrix").Key("enabled").MustBool(false)
|
||||
if !messagesEnabled {
|
||||
emailEnabled = false
|
||||
telegramEnabled = false
|
||||
discordEnabled = false
|
||||
matrixEnabled = false
|
||||
} else if app.config.Section("email").Key("method").MustString("") == "" {
|
||||
emailEnabled = false
|
||||
} else {
|
||||
emailEnabled = true
|
||||
}
|
||||
if !emailEnabled && !telegramEnabled && !discordEnabled {
|
||||
if !emailEnabled && !telegramEnabled && !discordEnabled && !matrixEnabled {
|
||||
messagesEnabled = false
|
||||
}
|
||||
|
||||
|
||||
@@ -568,7 +568,7 @@
|
||||
"depends_true": "enabled",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Require Discord connection on sign-up."
|
||||
"description": "Require Discord connection on sign-up. See the jfa-go wiki for info on setting this up."
|
||||
},
|
||||
"token": {
|
||||
"name": "API Token",
|
||||
@@ -633,7 +633,7 @@
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Telegram",
|
||||
"description": "Settings for Telegram signup/notifications"
|
||||
"description": "Settings for Telegram signup/notifications. See the jfa-go wiki for info on setting this up."
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
@@ -676,6 +676,80 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"matrix": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
"name": "Matrix",
|
||||
"description": "Settings for Matrix invites/signup/notifications. See the jfa-go wiki for info on setting this up."
|
||||
},
|
||||
"settings": {
|
||||
"enabled": {
|
||||
"name": "Enabled",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Enable signup verification through Matrix and the sending of notifications through it.\nSee the jfa-go wiki for setting up a bot."
|
||||
},
|
||||
"required": {
|
||||
"name": "Require on sign-up",
|
||||
"required": false,
|
||||
"required_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "bool",
|
||||
"value": false,
|
||||
"description": "Require Matrix connection on sign-up."
|
||||
},
|
||||
"homeserver": {
|
||||
"name": "Home Server URL",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Matrix Home server URL."
|
||||
},
|
||||
"token": {
|
||||
"name": "Access Token",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Matrix Bot API Token."
|
||||
},
|
||||
"user_id": {
|
||||
"name": "Bot User ID",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "User ID of bot account (Example: @jfa-bot:riot.im)"
|
||||
},
|
||||
"topic": {
|
||||
"name": "Chat topic",
|
||||
"required": false,
|
||||
"requires_restart": true,
|
||||
"depends_true": "enabled",
|
||||
"type": "text",
|
||||
"value": "Jellyfin notifications",
|
||||
"description": "Topic of Matrix private chats."
|
||||
},
|
||||
"language": {
|
||||
"name": "Language",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"depends_true": "enabled",
|
||||
"type": "select",
|
||||
"options": [
|
||||
["en-us", "English (US)"]
|
||||
],
|
||||
"value": "en-us",
|
||||
"description": "Default Matrix message language. Visit weblate if you'd like to translate."
|
||||
}
|
||||
}
|
||||
},
|
||||
"password_resets": {
|
||||
"order": [],
|
||||
"meta": {
|
||||
@@ -1225,6 +1299,14 @@
|
||||
"value": "",
|
||||
"description": "Stores telegram user IDs and language preferences."
|
||||
},
|
||||
"matrix_users": {
|
||||
"name": "Matrix users",
|
||||
"required": false,
|
||||
"requires_restart": false,
|
||||
"type": "text",
|
||||
"value": "",
|
||||
"description": "Stores matrix user IDs and language preferences."
|
||||
},
|
||||
"discord_users": {
|
||||
"name": "Discord users",
|
||||
"required": false,
|
||||
|
||||
@@ -77,11 +77,17 @@ func (d *DiscordDaemon) run() {
|
||||
d.app.err.Printf("Discord: Failed to start daemon: %v", err)
|
||||
return
|
||||
}
|
||||
// Sometimes bot.State isn't populated quick enough
|
||||
// Wait for everything to populate, it's slow sometimes.
|
||||
for d.bot.State == nil {
|
||||
continue
|
||||
}
|
||||
for d.bot.State.User == nil {
|
||||
continue
|
||||
}
|
||||
d.username = d.bot.State.User.Username
|
||||
for d.bot.State.Guilds == nil {
|
||||
continue
|
||||
}
|
||||
// Choose the last guild (server), for now we don't really support multiple anyway
|
||||
d.guildID = d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID
|
||||
guild, err := d.bot.Guild(d.guildID)
|
||||
|
||||
9
email.go
@@ -25,6 +25,8 @@ import (
|
||||
"github.com/mailgun/mailgun-go/v4"
|
||||
)
|
||||
|
||||
var renderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
||||
|
||||
// implements email sending, right now via smtp or mailgun.
|
||||
type EmailClient interface {
|
||||
Send(fromName, fromAddr string, message *Message, address ...string) error
|
||||
@@ -337,7 +339,6 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a
|
||||
|
||||
func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Message, error) {
|
||||
email := &Message{Subject: subject}
|
||||
renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
|
||||
html := markdown.ToHTML([]byte(md), nil, renderer)
|
||||
text := stripMarkdown(md)
|
||||
message := app.config.Section("messages").Key("message").String()
|
||||
@@ -817,6 +818,12 @@ func (app *appContext) sendByID(email *Message, ID ...string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if mxChat, ok := app.storage.matrix[id]; ok && mxChat.Contact && matrixEnabled {
|
||||
err = app.matrix.Send(email, mxChat.RoomID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if address, ok := app.storage.emails[id]; ok && address.Contact && emailEnabled {
|
||||
err = app.email.send(email, address.Addr)
|
||||
if err != nil {
|
||||
|
||||
6
go.mod
@@ -38,7 +38,9 @@ require (
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4
|
||||
github.com/mailgun/mailgun-go/v4 v4.5.1
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
|
||||
github.com/swaggo/gin-swagger v1.3.0
|
||||
@@ -47,8 +49,8 @@ require (
|
||||
github.com/ugorji/go v1.2.0 // indirect
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea // indirect
|
||||
golang.org/x/tools v0.1.1 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
|
||||
12
go.sum
@@ -189,6 +189,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 h1:ZtO5uywdd5dLDCud4r0r55eP4j9FuUNpl60Gmntcop4=
|
||||
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
@@ -221,6 +223,8 @@ github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
@@ -306,6 +310,10 @@ golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210521195947-fe42d452be8f h1:Si4U+UcgJzya9kpiEUJKQvjr512OLli+gL4poHrz93U=
|
||||
golang.org/x/net v0.0.0-20210521195947-fe42d452be8f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -338,6 +346,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210521203332-0cec03c779c1 h1:lCnv+lfrU9FRPGf8NeRuWAAPjNnema5WtBinMgs1fD8=
|
||||
golang.org/x/sys v0.0.0-20210521203332-0cec03c779c1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
window.emailEnabled = {{ .email_enabled }};
|
||||
window.telegramEnabled = {{ .telegram_enabled }};
|
||||
window.discordEnabled = {{ .discord_enabled }};
|
||||
window.matrixEnabled = {{ .matrix_enabled }};
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.usernameEnabled = {{ .username }};
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
@@ -340,6 +341,19 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div id="modal-matrix" class="modal">
|
||||
<form class="modal-content card" id="form-matrix" href="">
|
||||
<span class="heading">{{ .strings.linkMatrix }}</span>
|
||||
<p class="content">{{ .strings.linkMatrixDescription }}</p>
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.matrixHomeServer }}" id="matrix-homeserver">
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.username }}" id="matrix-user">
|
||||
<input type="password" class="field input ~neutral !high mt-half mb-1" placeholder="{{ .strings.password }}" id="matrix-password">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="notification-box"></div>
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
@@ -545,6 +559,9 @@
|
||||
{{ if .telegram_enabled }}
|
||||
<th>Telegram</th>
|
||||
{{ end }}
|
||||
{{ if .matrix_enabled }}
|
||||
<th>Matrix</th>
|
||||
{{ end }}
|
||||
{{ if .discord_enabled }}
|
||||
<th>Discord</th>
|
||||
{{ end }}
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
window.discordPIN = "{{ .discordPIN }}";
|
||||
window.discordInviteLink = {{ .discordInviteLink }};
|
||||
window.discordServerName = "{{ .discordServerName }}";
|
||||
window.matrixEnabled = {{ .matrixEnabled }};
|
||||
window.matrixRequired = {{ .matrixRequired }};
|
||||
window.matrixUserID = "{{ .matrixUser }}";
|
||||
</script>
|
||||
<script src="js/form.js" type="module"></script>
|
||||
{{ end }}
|
||||
|
||||
@@ -48,6 +48,24 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .matrixEnabled }}
|
||||
<div id="modal-matrix" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading mb-1">{{ .strings.linkMatrix }}</span>
|
||||
<p class="content mb-1"> {{ .strings.matrixEnterUser }}</p>
|
||||
<input type="text" class="input ~neutral !high" placeholder="@user:riot.im" id="matrix-userid">
|
||||
<div class="subheading link-center mt-1">
|
||||
<span class="shield ~info mr-1">
|
||||
<span class="icon">
|
||||
<i class="ri-chat-3-line"></i>
|
||||
</span>
|
||||
</span>
|
||||
{{ .matrixUser }}
|
||||
</div>
|
||||
<span class="button ~info !normal full-width center mt-1" id="matrix-send">{{ .strings.submit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
@@ -84,7 +102,10 @@
|
||||
{{ if .discordEnabled }}
|
||||
<span class="button ~info !normal full-width center mb-1" id="link-discord">{{ .strings.linkDiscord }}</span>
|
||||
{{ end }}
|
||||
{{ if or (.telegramEnabled) (.discordEnabled) }}
|
||||
{{ if .matrixEnabled }}
|
||||
<span class="button ~info !normal full-width center mb-1" id="link-matrix">{{ .strings.linkMatrix }}</span>
|
||||
{{ end }}
|
||||
{{ if or (.telegramEnabled) (or .discordEnabled .matrixEnabled) }}
|
||||
<div id="contact-via" class="unfocused">
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="contact-via" value="email"><span>Contact through Email</span>
|
||||
@@ -99,6 +120,11 @@
|
||||
<input type="radio" name="contact-via" value="discord" id="contact-via-discord"><span>Contact through Discord</span>
|
||||
</label>
|
||||
{{ end }}
|
||||
{{ if .matrixEnabled }}
|
||||
<label class="row switch pb-1">
|
||||
<input type="radio" name="contact-via" value="matrix" id="contact-via-matrix"><span>Contact through Matrix</span>
|
||||
</label>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
<label class="label supra" for="create-password">{{ .strings.password }}</label>
|
||||
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 109 KiB |
BIN
images/matrix/1.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
images/matrix/2.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
images/matrix/3.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
images/matrix/4.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
@@ -32,7 +32,7 @@
|
||||
"modifySettings": "Einstellungen ändern",
|
||||
"modifySettingsDescription": "Wende Einstellungen von einem bestehenden Profil an, oder beziehe sie direkt von einem Benutzer.",
|
||||
"applyHomescreenLayout": "Startbildschirmlayout anwenden",
|
||||
"sendDeleteNotificationEmail": "Benachrichtigungs-E-Mail senden",
|
||||
"sendDeleteNotificationEmail": "Benachrichtigung senden",
|
||||
"sendDeleteNotifiationExample": "Dein Konto wurde gelöscht.",
|
||||
"settingsRestartRequired": "Neustart erforderlich",
|
||||
"settingsRestartRequiredDescription": "Ein Neustart ist notwendig, um einige Einstellungen anzuwenden, die du geändert hast. Jetzt oder später neu starten?",
|
||||
@@ -72,7 +72,7 @@
|
||||
"customizeMessages": "E-Mails anpassen",
|
||||
"customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
|
||||
"announce": "Ankündigen",
|
||||
"subject": "E-Mail-Betreff",
|
||||
"subject": "Betreff",
|
||||
"message": "Nachricht",
|
||||
"markdownSupported": "Markdown wird unterstützt.",
|
||||
"advancedSettings": "Erweiterte Einstellungen",
|
||||
@@ -87,7 +87,14 @@
|
||||
"update": "Aktualisieren",
|
||||
"updates": "Aktualisierungen",
|
||||
"expiry": "Ablaufdatum",
|
||||
"extendExpiry": "Ablaufdatum verlängern"
|
||||
"extendExpiry": "Ablaufdatum verlängern",
|
||||
"reEnable": "Wieder aktivieren",
|
||||
"disable": "Deaktivieren",
|
||||
"donate": "Spenden",
|
||||
"conditionals": "Bedingungen",
|
||||
"contactThrough": "Kontakt über:",
|
||||
"sendPIN": "Bitte den Benutzer, die unten stehende PIN an den Bot zu senden.",
|
||||
"inviteMonths": "Monate"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "E-Mail-Adresse von {n} geändert.",
|
||||
@@ -116,7 +123,7 @@
|
||||
"errorFailureCheckLogs": "Fehlgeschlagen (überprüfe die Konsole/Logs)",
|
||||
"errorPartialFailureCheckLogs": "Teilweiser Fehlschlag (überprüfe die Konsole/Logs)",
|
||||
"errorUserCreated": "Fehler beim Erstellen des Benutzers {n}.",
|
||||
"errorSendWelcomeEmail": "Fehler beim Senden der Willkommens-E-Mail (überprüfe die Konsole/Logs)",
|
||||
"errorSendWelcomeEmail": "Fehler beim Senden der Willkommensnachricht (überprüfe die Konsole/Logs)",
|
||||
"saveEmail": "E-Mail gespeichert.",
|
||||
"errorSaveEmail": "Fehler beim Speichern der E-Mail.",
|
||||
"sentAnnouncement": "Ankündigung gesendet.",
|
||||
@@ -124,7 +131,9 @@
|
||||
"errorApplyUpdate": "Fehler beim Anwenden der Aktualisierung, versuche es manuell.",
|
||||
"errorCheckUpdate": "Fehler beim Suchen nach Aktualisierungen.",
|
||||
"updateAvailable": "Eine neue Aktualisierung ist verfügbar, überprüfe die Einstellungen.",
|
||||
"noUpdatesAvailable": "Keinen neuen Aktualisierungen verfügbar."
|
||||
"noUpdatesAvailable": "Keinen neuen Aktualisierungen verfügbar.",
|
||||
"updateAppliedRefresh": "Update angewendet, bitte aktualisieren.",
|
||||
"telegramVerified": "Telegram-Konto verifiziert."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
@@ -162,6 +171,22 @@
|
||||
"extendedExpiry": {
|
||||
"singular": "Ablaufdatum für {n} Benutzer verlängern.",
|
||||
"plural": "Ablaufdatum für {n} Benutzer verlängern."
|
||||
},
|
||||
"disabledUser": {
|
||||
"plural": "Benutzer {n} Deaktiviert.",
|
||||
"singular": "Benutzer {n} Deaktiviert."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "Benutzer {n} Aktiviert.",
|
||||
"plural": "Benutzer {n} Aktiviert."
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Benutzer {n} deaktivieren",
|
||||
"plural": "Deaktiviere {n} Benutzer"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Benutzer {n} wieder aktivieren",
|
||||
"plural": "Benutzer {n} wieder aktivieren"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,9 @@
|
||||
"notifyUserCreation": "On user creation",
|
||||
"sendPIN": "Ask the user to send the PIN below to the bot.",
|
||||
"searchDiscordUser": "Start typing the Discord username to find the user.",
|
||||
"findDiscordUser": "Find Discord user"
|
||||
"findDiscordUser": "Find Discord user",
|
||||
"linkMatrixDescription": "Enter the username and password of the user to use as a bot. Once submitted, the app will restart.",
|
||||
"matrixHomeServer": "Home server address"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Changed email address of {n}.",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"modifySettings": "Modifier les paramètres",
|
||||
"modifySettingsDescription": "Appliquez les paramètres à partir d'un profil existant ou obtenez-les directement auprès d'un utilisateur.",
|
||||
"applyHomescreenLayout": "Appliquer la disposition de l'écran d'accueil",
|
||||
"sendDeleteNotificationEmail": "Envoyer un e-mail de notification",
|
||||
"sendDeleteNotificationEmail": "Envoyer un message de notification",
|
||||
"sendDeleteNotifiationExample": "Votre compte a été supprimé.",
|
||||
"settingsRestartRequired": "Redémarrage nécessaire",
|
||||
"settingsRestartRequiredDescription": "Un redémarrage est nécessaire pour appliquer certains paramètres que vous avez modifiés. Redémarrer maintenant ou plus tard ?",
|
||||
@@ -93,7 +93,9 @@
|
||||
"conditionals": "Conditions",
|
||||
"userExpiryDescription": "Un laps de temps spécifié après chaque inscription, jfa-go supprimera / désactivera le compte. Vous pouvez modifier ce comportement dans les paramètres.",
|
||||
"donate": "Faire un don",
|
||||
"extendExpiry": "Prolonger l'expiration"
|
||||
"extendExpiry": "Prolonger l'expiration",
|
||||
"contactThrough": "Contactez par :",
|
||||
"sendPIN": "Demandez à l'utilisateur d'envoyer le code PIN ci-dessous au bot."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Adresse e-mail modifiée de {n}.",
|
||||
@@ -122,7 +124,7 @@
|
||||
"errorFailureCheckLogs": "Échec (vérifier la console / les journaux)",
|
||||
"errorPartialFailureCheckLogs": "Panne partielle (vérifier la console / les journaux)",
|
||||
"errorUserCreated": "Echec lors de la création de l'utilisateur {n}.",
|
||||
"errorSendWelcomeEmail": "Echec lors de l'envoi du mail de bienvenue (vérifier la console/les journaux)",
|
||||
"errorSendWelcomeEmail": "Echec lors de l'envoi du message de bienvenue (vérifier la console/les journaux)",
|
||||
"sentAnnouncement": "Annonce envoyée.",
|
||||
"saveEmail": "Email enregistré.",
|
||||
"errorSaveEmail": "Échec de l'enregistrement de l'e-mail.",
|
||||
@@ -130,7 +132,9 @@
|
||||
"errorApplyUpdate": "Échec de l'application de la mise à jour, essayez manuellement.",
|
||||
"errorCheckUpdate": "Échec de la vérification de la mise à jour.",
|
||||
"updateAvailable": "Une nouvelle mise à jour est disponible, vérifiez les paramètres.",
|
||||
"noUpdatesAvailable": "Aucune nouvelle mise à jour disponible."
|
||||
"noUpdatesAvailable": "Aucune nouvelle mise à jour disponible.",
|
||||
"telegramVerified": "Compte Telegram vérifié.",
|
||||
"updateAppliedRefresh": "Mise à jour appliquée, veuillez actualiser."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"modifySettings": "Instellingen aanpassen",
|
||||
"modifySettingsDescription": "Pas instellingen van een bestaand profiel toe, of neem ze direct over van een gebruiker.",
|
||||
"applyHomescreenLayout": "Sla startpagina indeling op",
|
||||
"sendDeleteNotificationEmail": "Stuur meldingse-mail",
|
||||
"sendDeleteNotificationEmail": "Stuur melding",
|
||||
"sendDeleteNotifiationExample": "Je account is verwijderd.",
|
||||
"settingsRestartRequired": "Herstart nodig",
|
||||
"settingsRestartRequiredDescription": "Er is een herstart nodig om de wijzigingen door te voeren. Herstart nu of later?",
|
||||
@@ -67,7 +67,7 @@
|
||||
"settingsRestarting": "Aan het herstarten…",
|
||||
"announce": "Aankondiging",
|
||||
"markdownSupported": "Markdown wordt ondersteund.",
|
||||
"subject": "E-mailonderwerp",
|
||||
"subject": "Onderwerp",
|
||||
"message": "Bericht",
|
||||
"variables": "Variabelen",
|
||||
"customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
|
||||
@@ -92,7 +92,9 @@
|
||||
"reEnable": "Opnieuw inschakelen",
|
||||
"disable": "Uitschakelen",
|
||||
"conditionals": "Voorwaarden",
|
||||
"donate": "Doneer"
|
||||
"donate": "Doneer",
|
||||
"contactThrough": "Stuur bericht via:",
|
||||
"sendPIN": "Vraag de gebruiker om onderstaande pincode naar de bot te sturen."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "E-mailadres van {n} gewijzigd.",
|
||||
@@ -120,7 +122,7 @@
|
||||
"errorChangedEmailAddress": "Wijzigen van e-mailadres van {n} mislukt.",
|
||||
"errorFailureCheckLogs": "Mislukt (controleer console/logbestanden)",
|
||||
"errorPartialFailureCheckLogs": "Gedeeltelijke fout (controleer console/logbestanden)",
|
||||
"errorSendWelcomeEmail": "Versturen van welkomste-mail is mislukt (zie console/logs)",
|
||||
"errorSendWelcomeEmail": "Versturen van welkomstbericht is mislukt (zie console/logs)",
|
||||
"errorUserCreated": "Aanmaken van gebruiker {n} is mislukt.",
|
||||
"sentAnnouncement": "Aankondiging verzonden.",
|
||||
"saveEmail": "E-mail opgeslagen.",
|
||||
@@ -129,7 +131,9 @@
|
||||
"errorApplyUpdate": "Installatie van update mislukt, probeer handmatig.",
|
||||
"errorCheckUpdate": "Controleren op update mislukt.",
|
||||
"updateAvailable": "Er is een nieuwe update beschikbaar, kijk bij instellingen.",
|
||||
"noUpdatesAvailable": "Geen nieuwe updates beschikbaar."
|
||||
"noUpdatesAvailable": "Geen nieuwe updates beschikbaar.",
|
||||
"telegramVerified": "Telegram-account goedgekeurd.",
|
||||
"updateAppliedRefresh": "Update toegepast, ververs alsjeblieft."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"modifySettings": "Modificar configurações",
|
||||
"modifySettingsDescription": "Aplique as configurações de um perfil existente ou obtenha-as diretamente de um usuário.",
|
||||
"applyHomescreenLayout": "Aplicar layout na tela inicial",
|
||||
"sendDeleteNotificationEmail": "Enviar email de notificação",
|
||||
"sendDeleteNotificationEmail": "Enviar mensagem de notificação",
|
||||
"sendDeleteNotifiationExample": "Sua conta foi deletada.",
|
||||
"settingsRestartRequired": "Necessário reiniciar",
|
||||
"settingsRestartRequiredDescription": "É necessário reiniciar para aplicar algumas configurações alteradas. Deseja reiniciar agora ou mais tarde?",
|
||||
@@ -66,7 +66,7 @@
|
||||
"settingsRestart": "Reiniciar",
|
||||
"settingsRestarting": "Reiniciando…",
|
||||
"announce": "Anunciar",
|
||||
"subject": "Assunto do email",
|
||||
"subject": "Assunto",
|
||||
"message": "Mensagem",
|
||||
"markdownSupported": "Suporte a Markdown.",
|
||||
"customizeMessagesDescription": "Se não quiser usar os modelos de email do jfa-go, você pode criar o seu próprio usando o Markdown.",
|
||||
@@ -91,7 +91,10 @@
|
||||
"inviteMonths": "Meses",
|
||||
"reEnable": "Reativar",
|
||||
"disable": "Desativar",
|
||||
"conditionals": "Condicionais"
|
||||
"conditionals": "Condicionais",
|
||||
"donate": "Doar",
|
||||
"contactThrough": "Contato através:",
|
||||
"sendPIN": "Peça ao usuário para enviar o PIN abaixo para o bot."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Endereço de e-mail alterado de {n}.",
|
||||
@@ -120,7 +123,7 @@
|
||||
"errorFailureCheckLogs": "Falha (verificar console/logs)",
|
||||
"errorPartialFailureCheckLogs": "Falha parcial (verificar console/logs)",
|
||||
"errorUserCreated": "Falha ao criar o usuário {n}.",
|
||||
"errorSendWelcomeEmail": "Falha ao enviar e-mail de boas-vindas (verifique console/logs)",
|
||||
"errorSendWelcomeEmail": "Falha ao enviar mensagem de boas-vindas (verifique console/logs)",
|
||||
"sentAnnouncement": "Comunicado enviado.",
|
||||
"saveEmail": "Email salvo.",
|
||||
"errorSaveEmail": "Falha ao salvar o email.",
|
||||
@@ -128,7 +131,9 @@
|
||||
"errorApplyUpdate": "Falha ao aplicar a atualização, tente manualmente.",
|
||||
"updateAvailable": "Uma nova atualização está disponível, verifique as configurações.",
|
||||
"errorCheckUpdate": "Falha ao verificar atualizações.",
|
||||
"noUpdatesAvailable": "Nenhuma atualização disponível."
|
||||
"noUpdatesAvailable": "Nenhuma atualização disponível.",
|
||||
"telegramVerified": "Conta do Telegram verificada.",
|
||||
"updateAppliedRefresh": "Atualização instalada, atualize."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"theme": "Thema",
|
||||
"time24h": "24h-Format",
|
||||
"time12h": "12h-Format",
|
||||
"copied": "Kopiert"
|
||||
"copied": "Kopiert",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Kontakt über E-Mail",
|
||||
"contactTelegram": "Kontakt über Telegram"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"emailAddress": "Email Address",
|
||||
"name": "Name",
|
||||
"submit": "Submit",
|
||||
"send": "Send",
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"copy": "Copy",
|
||||
@@ -18,6 +19,7 @@
|
||||
"contactEmail": "Contact through Email",
|
||||
"contactTelegram": "Contact through Telegram",
|
||||
"linkDiscord": "Link Discord",
|
||||
"linkMatrix": "Link Matrix",
|
||||
"contactDiscord": "Contact through Discord",
|
||||
"theme": "Theme"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"time24h": "Temps 24h",
|
||||
"time12h": "Temps 12h",
|
||||
"theme": "Thème",
|
||||
"copied": "Copié"
|
||||
"copied": "Copié",
|
||||
"linkTelegram": "Lien Telegram",
|
||||
"contactEmail": "Contact par e-mail",
|
||||
"contactTelegram": "Contact par Telegram"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
"password": "Wachtwoord",
|
||||
"emailAddress": "E-mailadres",
|
||||
"submit": "Verstuur",
|
||||
"success": "Success",
|
||||
"success": "Succes",
|
||||
"error": "Fout",
|
||||
"copy": "Kopiëer",
|
||||
"theme": "Thema",
|
||||
"time24h": "24u-formaat",
|
||||
"time12h": "12u-formaat",
|
||||
"copied": "Gekopieerd"
|
||||
"copied": "Gekopieerd",
|
||||
"linkTelegram": "Koppel Telegram",
|
||||
"contactEmail": "Stuur e-mailbericht",
|
||||
"contactTelegram": "Stuur Telegram-bericht"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"theme": "Tema",
|
||||
"time24h": "Horário 24h",
|
||||
"time12h": "Horário 12h",
|
||||
"copied": "Copiado"
|
||||
"copied": "Copiado",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Contato por Email",
|
||||
"contactTelegram": "Contato pelo Telegram"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Deutsch (DE)"
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Wenn du das nicht warst, ignoriere bitte diese E-Mail.",
|
||||
"ifItWasNotYou": "Wenn du das nicht warst, ignoriere bitte dies.",
|
||||
"reason": "Grund",
|
||||
"helloUser": "Hallo {username},"
|
||||
},
|
||||
@@ -27,7 +27,8 @@
|
||||
"ifItWasYou": "Wenn du das warst, gib die PIN unten in die Eingabeaufforderung ein.",
|
||||
"codeExpiry": "Der Code wird am {date}, um {time} UTC ablaufen, was in {expiresInMinutes} ist.",
|
||||
"pin": "PIN",
|
||||
"name": "Passwortzurücksetzung"
|
||||
"name": "Passwortzurücksetzung",
|
||||
"ifItWasYouLink": "Wenn du das warst, klick auf den Link unten."
|
||||
},
|
||||
"userDeleted": {
|
||||
"title": "Dein Konto wurde gelöscht - Jellyfin",
|
||||
@@ -48,7 +49,8 @@
|
||||
"welcome": "Willkommen bei Jellyfin!",
|
||||
"youCanLoginWith": "Du kannst dich mit den mit den untenstehenden Zugangsdaten anmelden",
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Willkommens-E-Mail"
|
||||
"name": "Willkommen",
|
||||
"yourAccountWillExpire": "Dein Konto läuft am {date} ab."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
"title": "Bestätige deine E-Mail - Jellyfin",
|
||||
@@ -61,5 +63,15 @@
|
||||
"title": "Dein Konto ist abgelaufen - Jellyfin",
|
||||
"yourAccountHasExpired": "Dein Konto ist abgelaufen.",
|
||||
"contactTheAdmin": "Kontaktiere den Administrator für weitere Informationen."
|
||||
},
|
||||
"userDisabled": {
|
||||
"name": "Benutzer deaktiviert",
|
||||
"title": "Dein Konto wurde deaktiviert - Jellyfin",
|
||||
"yourAccountWasDisabled": "Dein Konto wurde deaktiviert."
|
||||
},
|
||||
"userEnabled": {
|
||||
"name": "Benutzer aktiviert",
|
||||
"title": "Dein Konto wurde wieder freigeschaltet - Jellyfin",
|
||||
"yourAccountWasEnabled": "Dein Konto wurde wieder aktiviert."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"author": "https://github.com/Cornichon420"
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Si ce n'était pas toi, tu peux ignorer ce mail.",
|
||||
"ifItWasNotYou": "Si ce n'était pas toi, tu peux ignorer ceci.",
|
||||
"reason": "Motif",
|
||||
"helloUser": "Salut {username},"
|
||||
},
|
||||
@@ -50,7 +50,7 @@
|
||||
"title": "Bienvenue sur Jellyfin",
|
||||
"welcome": "Bienvenue sur Jellyfin !",
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Courriel de bienvenue",
|
||||
"name": "Bienvenue",
|
||||
"yourAccountWillExpire": "Ton compte expirera le {date}."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Nederlands (NL)"
|
||||
},
|
||||
"strings": {
|
||||
"ifItWasNotYou": "Als jij dit niet was, negeer dan alsjeblieft deze email.",
|
||||
"ifItWasNotYou": "Als jij dit niet was, negeer dit dan alsjeblieft.",
|
||||
"reason": "Reden",
|
||||
"helloUser": "Hoi {username},"
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
"welcome": "Welkom bij Jellyfin!",
|
||||
"youCanLoginWith": "Je kunt inloggen met onderstaande gegevens",
|
||||
"jellyfinURL": "URL",
|
||||
"name": "Welkomste-mail",
|
||||
"name": "Welkom",
|
||||
"yourAccountWillExpire": "Je account verloopt op {date}."
|
||||
},
|
||||
"emailConfirmation": {
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"successContinueButton": "Weiter",
|
||||
"confirmationRequired": "E-Mail-Bestätigung erforderlich",
|
||||
"confirmationRequiredMessage": "Bitte überprüfe dein Posteingang und bestätige deine E-Mail-Adresse.",
|
||||
"yourAccountIsValidUntil": "Dein Konto wird bis zum {date} gültig sein."
|
||||
"yourAccountIsValidUntil": "Dein Konto wird bis zum {date} gültig sein.",
|
||||
"sendPIN": "Sende die untenstehende PIN an den Bot und komm dann hierher zurück, um dein Konto zu verbinden."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
@@ -43,6 +44,9 @@
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "Benutzer existiert bereits.",
|
||||
"errorInvalidCode": "Ungültiger Invite-Code."
|
||||
"errorInvalidCode": "Ungültiger Invite-Code.",
|
||||
"telegramVerified": "Telegram-Konto verifiziert.",
|
||||
"errorTelegramVerification": "Verifizierung von Telegram erforderlich.",
|
||||
"errorInvalidPIN": "Telegram PIN ist ungültig."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,17 @@
|
||||
"confirmationRequiredMessage": "Please check your email inbox to verify your address.",
|
||||
"yourAccountIsValidUntil": "Your account will be valid until {date}.",
|
||||
"sendPIN": "Send the PIN below to the bot, then come back here to link your account.",
|
||||
"sendPINDiscord": "Type {command} in {server_channel} on Discord, then send the PIN below via DM to the bot."
|
||||
"sendPINDiscord": "Type {command} in {server_channel} on Discord, then send the PIN below via DM to the bot.",
|
||||
"matrixEnterUser": "Enter your User ID, press submit, and a PIN will be sent to you. Enter it here to continue."
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "User already exists.",
|
||||
"errorInvalidCode": "Invalid invite code.",
|
||||
"errorTelegramVerification": "Telegram verification required.",
|
||||
"errorDiscordVerification": "Discord verification required.",
|
||||
"errorMatrixVerification": "Matrix verification required.",
|
||||
"errorInvalidPIN": "PIN is invalid.",
|
||||
"errorUnknown": "Unknown error.",
|
||||
"verified": "Account verified."
|
||||
},
|
||||
"validationStrings": {
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"successContinueButton": "Continuer",
|
||||
"confirmationRequired": "Confirmation de l'adresse e-mail requise",
|
||||
"confirmationRequiredMessage": "Veuillez vérifier votre boite de réception pour confirmer votre adresse e-mail.",
|
||||
"yourAccountIsValidUntil": "Votre compte sera valide jusqu'au {date}."
|
||||
"yourAccountIsValidUntil": "Votre compte sera valide jusqu'au {date}.",
|
||||
"sendPIN": "Envoyez le code PIN ci-dessous au bot, puis revenez ici pour lier votre compte."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
@@ -44,6 +45,9 @@
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "Utilisateur déjà existant.",
|
||||
"errorInvalidCode": "Code d’invitation non valide."
|
||||
"errorInvalidCode": "Code d’invitation non valide.",
|
||||
"errorTelegramVerification": "Vérification Telegram requise.",
|
||||
"errorInvalidPIN": "PIN Telegram invalide.",
|
||||
"telegramVerified": "Compte Telegram vérifié."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"successContinueButton": "Doorgaan",
|
||||
"confirmationRequired": "Bevestiging van e-mailadres verplicht",
|
||||
"confirmationRequiredMessage": "Controleer je e-mail inbox om je adres te bevestigen.",
|
||||
"yourAccountIsValidUntil": "Je account zal geldig zijn tot {date}."
|
||||
"yourAccountIsValidUntil": "Je account zal geldig zijn tot {date}.",
|
||||
"sendPIN": "Stuur onderstaande pincode naar de bot, en kom daarna hier terug om je account te koppelen."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
@@ -43,6 +44,9 @@
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "Gebruiker bestaat al.",
|
||||
"errorInvalidCode": "Ongeldige uitnodigingscode."
|
||||
"errorInvalidCode": "Ongeldige uitnodigingscode.",
|
||||
"telegramVerified": "Telegram-account goedgekeurd.",
|
||||
"errorTelegramVerification": "Telegram-verificatie nodig.",
|
||||
"errorInvalidPIN": "Telegram pincode is ongeldig."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,17 @@
|
||||
"passwordRequirementsHeader": "Requisitos da Senha",
|
||||
"successHeader": "Sucesso!",
|
||||
"successContinueButton": "Continuar",
|
||||
"confirmationRequired": "Necessária confirmação de e-mail",
|
||||
"confirmationRequired": "Confirmação por e-mail",
|
||||
"confirmationRequiredMessage": "Verifique sua caixa de email para finalizar o cadastro.",
|
||||
"yourAccountIsValidUntil": "Sua conta é válida até {date}."
|
||||
"yourAccountIsValidUntil": "Sua conta é válida até {date}.",
|
||||
"sendPIN": "Envie o PIN abaixo para o bot e volte aqui para vincular sua conta."
|
||||
},
|
||||
"notifications": {
|
||||
"errorUserExists": "Esse usuário já existe.",
|
||||
"errorInvalidCode": "Código do convite invalido."
|
||||
"errorInvalidCode": "Código do convite invalido.",
|
||||
"telegramVerified": "Conta do Telegram verificada.",
|
||||
"errorInvalidPIN": "O PIN do telegram é inválido.",
|
||||
"errorTelegramVerification": "Requer a verificação do telegram."
|
||||
},
|
||||
"validationStrings": {
|
||||
"length": {
|
||||
|
||||
13
lang/pwreset/de-DE.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Deutsch (DE)"
|
||||
},
|
||||
"strings": {
|
||||
"passwordReset": "Passwort zurücksetzen",
|
||||
"resetFailed": "Zurücksetzen des Passworts fehlgeschlagen",
|
||||
"tryAgain": "Bitte versuche es erneut.",
|
||||
"youCanLogin": "Du kannst dich nun mit dem unten stehenden Code als Passwort anmelden.",
|
||||
"youCanLoginOmbi": "Du kannst dich jetzt bei Jellyfin und Ombi mit dem unten stehenden Code als Passwort anmelden.",
|
||||
"changeYourPassword": "Achte darauf, dass du dein Passwort nach der Anmeldung änderst."
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,10 @@
|
||||
"title": "Passwortzurücksetzungen",
|
||||
"description": "Wenn ein Benutzer versucht sein Passwort zurückzusetzen, erstellt Jellyfin eine Datei namens 'passwordreset-*.json', welche eine PIN enthält. jfa-go liest die Datei und sendet die PIN an den Benutzer.",
|
||||
"pathToJellyfin": "Pfad zum Jellyfin-Konfigurationsverzeichnis",
|
||||
"pathToJellyfinNotice": "Wenn du nicht weißt, wo das ist, versuche dein Passwort in Jellyfin zurückzusetzen. Ein Popup mit '<Pfad zu Jellyfin>/passwordreset-*.json' wird angezeigt werden."
|
||||
"pathToJellyfinNotice": "Wenn du nicht weißt, wo das ist, versuche dein Passwort in Jellyfin zurückzusetzen. Ein Popup mit '<Pfad zu Jellyfin>/passwordreset-*.json' wird angezeigt werden.",
|
||||
"resetLinks": "Einen Link statt einer PIN senden",
|
||||
"resetLinksNotice": "Wenn die Ombi-Integration aktiviert ist, verwende dies, um zurückgesetzte Passwörter von Jellyfin mit Ombi zu synchronisieren.",
|
||||
"resetLinksLanguage": "Standardsprache für den Link zum Zurücksetzen des Passworts"
|
||||
},
|
||||
"passwordValidation": {
|
||||
"title": "Passwortüberprüfung",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
},
|
||||
"strings": {
|
||||
"startMessage": "Hi!\nEnter your Jellyfin PIN code here to verify your account.",
|
||||
"matrixStartMessage": "Hi\nEnter the below PIN in the Jellyfin sign-up page to verify your account.",
|
||||
"invalidPIN": "That PIN was invalid, try again.",
|
||||
"pinSuccess": "Success! You can now return to the sign-up page.",
|
||||
"languageMessage": "Note: See available languages with {command}, and set language with {command} <language code>."
|
||||
|
||||
59
main.go
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"mime"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
_ "github.com/hrfee/jfa-go/docs"
|
||||
"github.com/hrfee/jfa-go/logger"
|
||||
@@ -58,6 +60,8 @@ var temp = func() string {
|
||||
return temp
|
||||
}()
|
||||
|
||||
var logPath string = filepath.Join(temp, "jfa-go.log")
|
||||
|
||||
var serverTypes = map[string]string{
|
||||
"jellyfin": "Jellyfin",
|
||||
"emby": "Emby (experimental)",
|
||||
@@ -96,6 +100,7 @@ type appContext struct {
|
||||
email *Emailer
|
||||
telegram *TelegramDaemon
|
||||
discord *DiscordDaemon
|
||||
matrix *MatrixDaemon
|
||||
info, debug, err logger.Logger
|
||||
host string
|
||||
port int
|
||||
@@ -349,6 +354,10 @@ func start(asDaemon, firstCall bool) {
|
||||
if err := app.storage.loadDiscordUsers(); err != nil {
|
||||
app.err.Printf("Failed to load Discord users: %v", err)
|
||||
}
|
||||
app.storage.matrix_path = app.config.Section("files").Key("matrix_users").String()
|
||||
if err := app.storage.loadMatrixUsers(); err != nil {
|
||||
app.err.Printf("Failed to load Matrix users: %v", err)
|
||||
}
|
||||
|
||||
app.storage.profiles_path = app.config.Section("files").Key("user_profiles").String()
|
||||
app.storage.loadProfiles()
|
||||
@@ -586,6 +595,16 @@ func start(asDaemon, firstCall bool) {
|
||||
defer app.discord.Shutdown()
|
||||
}
|
||||
}
|
||||
if matrixEnabled {
|
||||
app.matrix, err = newMatrixDaemon(app)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to initialize Matrix daemon: %v", err)
|
||||
matrixEnabled = false
|
||||
} else {
|
||||
go app.matrix.run()
|
||||
defer app.matrix.Shutdown()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugMode = false
|
||||
address = "0.0.0.0:8056"
|
||||
@@ -622,6 +641,11 @@ func start(asDaemon, firstCall bool) {
|
||||
}
|
||||
}
|
||||
}()
|
||||
if firstRun {
|
||||
app.info.Printf("Loaded, visit %s to start.", address)
|
||||
} else {
|
||||
app.info.Printf("Loaded @ %s", address)
|
||||
}
|
||||
app.quit = make(chan os.Signal)
|
||||
signal.Notify(app.quit, os.Interrupt)
|
||||
go func() {
|
||||
@@ -703,10 +727,41 @@ func flagPassed(name string) (found bool) {
|
||||
// @tag.description Things that dont fit elsewhere.
|
||||
|
||||
func printVersion() {
|
||||
fmt.Println(info("jfa-go version: %s (%s)\n", hiwhite(version), white(commit)))
|
||||
tray := ""
|
||||
if TRAY {
|
||||
tray = " TrayIcon"
|
||||
}
|
||||
fmt.Println(info("jfa-go version: %s (%s)%s\n", hiwhite(version), white(commit), tray))
|
||||
}
|
||||
|
||||
func logOutput() func() {
|
||||
old := os.Stdout
|
||||
log.Printf("Logging to \"%s\"", logPath)
|
||||
f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return func() {}
|
||||
}
|
||||
writer := io.MultiWriter(old, f)
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout, os.Stderr = w, w
|
||||
log.SetOutput(writer)
|
||||
gin.DefaultWriter, gin.DefaultErrorWriter = writer, writer
|
||||
wExit := make(chan bool)
|
||||
go func() {
|
||||
io.Copy(writer, r)
|
||||
wExit <- true
|
||||
}()
|
||||
return func() {
|
||||
w.Close()
|
||||
<-wExit
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if TRAY {
|
||||
defer logOutput()()
|
||||
}
|
||||
printVersion()
|
||||
SOCK = filepath.Join(temp, SOCK)
|
||||
fmt.Println("Socket:", SOCK)
|
||||
@@ -774,7 +829,7 @@ You can then run:
|
||||
fmt.Print(info("systemctl --user stop jfa-go\n\n"))
|
||||
color.New(color.FgYellow).PrintFunc()("To restart: ")
|
||||
fmt.Print(info("systemctl --user stop jfa-go\n"))
|
||||
} else if TRAY && flagPassed("tray") {
|
||||
} else if TRAY {
|
||||
RunTray()
|
||||
} else {
|
||||
RESTART = make(chan bool, 1)
|
||||
|
||||
238
matrix.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/matrix-org/gomatrix"
|
||||
)
|
||||
|
||||
type MatrixDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
bot *gomatrix.Client
|
||||
userID string
|
||||
tokens map[string]UnverifiedUser // Map of tokens to users
|
||||
languages map[string]string // Map of roomIDs to language codes
|
||||
app *appContext
|
||||
}
|
||||
|
||||
type UnverifiedUser struct {
|
||||
Verified bool
|
||||
User *MatrixUser
|
||||
}
|
||||
|
||||
type MatrixUser struct {
|
||||
RoomID string
|
||||
UserID string
|
||||
Lang string
|
||||
Contact bool
|
||||
}
|
||||
|
||||
type MatrixIdentifier struct {
|
||||
User string `json:"user"`
|
||||
IdentType string `json:"type"`
|
||||
}
|
||||
|
||||
func (m MatrixIdentifier) Type() string { return m.IdentType }
|
||||
|
||||
var matrixFilter = gomatrix.Filter{
|
||||
Room: gomatrix.RoomFilter{
|
||||
Timeline: gomatrix.FilterPart{
|
||||
Types: []string{
|
||||
"m.room.message",
|
||||
"m.room.member",
|
||||
},
|
||||
},
|
||||
},
|
||||
EventFields: []string{
|
||||
"type",
|
||||
"event_id",
|
||||
"room_id",
|
||||
"state_key",
|
||||
"sender",
|
||||
"content.body",
|
||||
"content.membership",
|
||||
},
|
||||
}
|
||||
|
||||
func newMatrixDaemon(app *appContext) (d *MatrixDaemon, err error) {
|
||||
matrix := app.config.Section("matrix")
|
||||
homeserver := matrix.Key("homeserver").String()
|
||||
token := matrix.Key("token").String()
|
||||
d = &MatrixDaemon{
|
||||
ShutdownChannel: make(chan string),
|
||||
userID: matrix.Key("user_id").String(),
|
||||
tokens: map[string]UnverifiedUser{},
|
||||
languages: map[string]string{},
|
||||
app: app,
|
||||
}
|
||||
d.bot, err = gomatrix.NewClient(homeserver, d.userID, token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
filter, err := json.Marshal(matrixFilter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := d.bot.CreateFilter(filter)
|
||||
d.bot.Store.SaveFilterID(d.userID, resp.FilterID)
|
||||
for _, user := range app.storage.matrix {
|
||||
if user.Lang != "" {
|
||||
d.languages[user.RoomID] = user.Lang
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) generateAccessToken(homeserver, username, password string) (string, error) {
|
||||
req := &gomatrix.ReqLogin{
|
||||
Type: "m.login.password",
|
||||
Identifier: MatrixIdentifier{
|
||||
User: username,
|
||||
IdentType: "m.id.user",
|
||||
},
|
||||
Password: password,
|
||||
DeviceID: "jfa-go-" + commit,
|
||||
}
|
||||
bot, err := gomatrix.NewClient(homeserver, username, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := bot.Login(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.AccessToken, nil
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) run() {
|
||||
d.app.info.Println("Starting Matrix bot daemon")
|
||||
syncer := d.bot.Syncer.(*gomatrix.DefaultSyncer)
|
||||
syncer.OnEventType("m.room.message", d.handleMessage)
|
||||
// syncer.OnEventType("m.room.member", d.handleMembership)
|
||||
if err := d.bot.Sync(); err != nil {
|
||||
d.app.err.Printf("Matrix sync failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) Shutdown() {
|
||||
d.bot.StopSync()
|
||||
d.Stopped = true
|
||||
close(d.ShutdownChannel)
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) handleMessage(event *gomatrix.Event) {
|
||||
if event.Sender == d.userID {
|
||||
return
|
||||
}
|
||||
lang := "en-us"
|
||||
if l, ok := d.languages[event.RoomID]; ok {
|
||||
if _, ok := d.app.storage.lang.Telegram[l]; ok {
|
||||
lang = l
|
||||
}
|
||||
}
|
||||
sects := strings.Split(event.Content["body"].(string), " ")
|
||||
switch sects[0] {
|
||||
case "!lang":
|
||||
if len(sects) == 2 {
|
||||
d.commandLang(event, sects[1], lang)
|
||||
} else {
|
||||
d.commandLang(event, "", lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) commandLang(event *gomatrix.Event, code, lang string) {
|
||||
if code == "" {
|
||||
list := "!lang <lang>\n"
|
||||
for c := range d.app.storage.lang.Telegram {
|
||||
list += fmt.Sprintf("%s: %s\n", c, d.app.storage.lang.Telegram[c].Meta.Name)
|
||||
}
|
||||
_, err := d.bot.SendText(
|
||||
event.RoomID,
|
||||
list,
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Matrix: Failed to send message to \"%s\": %v", event.Sender, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, ok := d.app.storage.lang.Telegram[code]; !ok {
|
||||
return
|
||||
}
|
||||
d.languages[event.RoomID] = code
|
||||
if u, ok := d.app.storage.matrix[event.RoomID]; ok {
|
||||
u.Lang = code
|
||||
d.app.storage.matrix[event.RoomID] = u
|
||||
if err := d.app.storage.storeMatrixUsers(); err != nil {
|
||||
d.app.err.Printf("Matrix: Failed to store Matrix users: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) CreateRoom(userID string) (string, error) {
|
||||
room, err := d.bot.CreateRoom(&gomatrix.ReqCreateRoom{
|
||||
Visibility: "private",
|
||||
Invite: []string{userID},
|
||||
Topic: d.app.config.Section("matrix").Key("topic").String(),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return room.RoomID, nil
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) SendStart(userID string) (ok bool) {
|
||||
roomID, err := d.CreateRoom(userID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Failed to create room for user \"%s\": %v", userID, err)
|
||||
return
|
||||
}
|
||||
lang := "en-us"
|
||||
pin := genAuthToken()
|
||||
d.tokens[pin] = UnverifiedUser{
|
||||
false,
|
||||
&MatrixUser{
|
||||
RoomID: roomID,
|
||||
UserID: userID,
|
||||
Lang: lang,
|
||||
},
|
||||
}
|
||||
_, err = d.bot.SendText(
|
||||
roomID,
|
||||
d.app.storage.lang.Telegram[lang].Strings.get("matrixStartMessage")+"\n\n"+pin+"\n\n"+
|
||||
d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"}),
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Matrix: Failed to send welcome message to \"%s\": %v", userID, err)
|
||||
return
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func (d *MatrixDaemon) Send(message *Message, roomID ...string) (err error) {
|
||||
md := ""
|
||||
if message.Markdown != "" {
|
||||
// Convert images to links
|
||||
md = string(markdown.ToHTML([]byte(strings.ReplaceAll(message.Markdown, "![", "[")), nil, renderer))
|
||||
}
|
||||
for _, id := range roomID {
|
||||
if md != "" {
|
||||
_, err = d.bot.SendFormattedText(id, message.Text, md)
|
||||
} else {
|
||||
_, err = d.bot.SendText(id, message.Text)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// User enters ID on sign-up, a PIN is sent to them. They enter it on sign-up.
|
||||
|
||||
// Message the user first, to avoid E2EE by default
|
||||
24
models.go
@@ -19,6 +19,8 @@ type newUserDTO struct {
|
||||
TelegramContact bool `json:"telegram_contact"` // Whether or not to use telegram for notifications/pwrs
|
||||
DiscordPIN string `json:"discord_pin" example:"A1-B2-3C"` // Discord verification PIN (if used)
|
||||
DiscordContact bool `json:"discord_contact"` // Whether or not to use discord for notifications/pwrs
|
||||
MatrixPIN string `json:"matrix_pin" example:"A1-B2-3C"` // Matrix verification PIN (if used)
|
||||
MatrixContact bool `json:"matrix_contact"` // Whether or not to use matrix for notifications/pwrs
|
||||
}
|
||||
|
||||
type newUserResponse struct {
|
||||
@@ -137,6 +139,8 @@ type respUser struct {
|
||||
Discord string `json:"discord"` // Discord username (if known)
|
||||
DiscordID string `json:"discord_id"` // Discord user ID for creating links.
|
||||
NotifyThroughDiscord bool `json:"notify_discord"`
|
||||
Matrix string `json:"matrix"` // Matrix ID (if known)
|
||||
NotifyThroughMatrix bool `json:"notify_matrix"`
|
||||
}
|
||||
|
||||
type getUsersDTO struct {
|
||||
@@ -260,6 +264,7 @@ type SetContactMethodsDTO struct {
|
||||
Email bool `json:"email"`
|
||||
Discord bool `json:"discord"`
|
||||
Telegram bool `json:"telegram"`
|
||||
Matrix bool `json:"matrix"`
|
||||
}
|
||||
|
||||
type DiscordUserDTO struct {
|
||||
@@ -281,3 +286,22 @@ type DiscordInviteDTO struct {
|
||||
InviteURL string `json:"invite"`
|
||||
IconURL string `json:"icon"`
|
||||
}
|
||||
|
||||
type MatrixSendPINDTO struct {
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type MatrixCheckPINDTO struct {
|
||||
PIN string `json:"pin"`
|
||||
}
|
||||
|
||||
type MatrixConnectUserDTO struct {
|
||||
JellyfinID string `json:"jf_id"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type MatrixLoginDTO struct {
|
||||
Homeserver string `json:"homeserver"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
@@ -127,6 +127,11 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
router.GET(p+"/invite/:invCode/discord/invite", app.DiscordServerInvite)
|
||||
}
|
||||
}
|
||||
if matrixEnabled {
|
||||
router.GET(p+"/invite/:invCode/matrix/verified/:userID/:pin", app.MatrixCheckPIN)
|
||||
router.POST(p+"/invite/:invCode/matrix/user", app.MatrixSendPIN)
|
||||
router.POST(p+"/users/matrix", app.MatrixConnect)
|
||||
}
|
||||
}
|
||||
if *SWAGGER {
|
||||
app.info.Print(warning("\n\nWARNING: Swagger should not be used on a public instance.\n\n"))
|
||||
@@ -164,7 +169,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
api.GET(p+"/config", app.GetConfig)
|
||||
api.POST(p+"/config", app.ModifyConfig)
|
||||
api.POST(p+"/restart", app.restart)
|
||||
if telegramEnabled || discordEnabled {
|
||||
if telegramEnabled || discordEnabled || matrixEnabled {
|
||||
api.GET(p+"/telegram/pin", app.TelegramGetPin)
|
||||
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
|
||||
api.POST(p+"/users/telegram", app.TelegramAddUser)
|
||||
@@ -178,6 +183,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
|
||||
api.GET(p+"/ombi/users", app.OmbiUsers)
|
||||
api.POST(p+"/ombi/defaults", app.SetOmbiDefaults)
|
||||
}
|
||||
api.POST(p+"/matrix/login", app.MatrixLogin)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
39
storage.go
@@ -15,21 +15,22 @@ import (
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path string
|
||||
users map[string]time.Time
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
displayprefs, ombi_template map[string]interface{}
|
||||
emails map[string]EmailAddress
|
||||
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
|
||||
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
|
||||
customEmails customEmails
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
lang Lang
|
||||
invitesLock, usersLock sync.Mutex
|
||||
timePattern string
|
||||
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path, users_path, telegram_path, discord_path, matrix_path string
|
||||
users map[string]time.Time
|
||||
invites Invites
|
||||
profiles map[string]Profile
|
||||
defaultProfile string
|
||||
displayprefs, ombi_template map[string]interface{}
|
||||
emails map[string]EmailAddress
|
||||
telegram map[string]TelegramUser // Map of Jellyfin User IDs to telegram users.
|
||||
discord map[string]DiscordUser // Map of Jellyfin user IDs to discord users.
|
||||
matrix map[string]MatrixUser // Map of Jellyfin user IDs to Matrix users.
|
||||
customEmails customEmails
|
||||
policy mediabrowser.Policy
|
||||
configuration mediabrowser.Configuration
|
||||
lang Lang
|
||||
invitesLock, usersLock sync.Mutex
|
||||
}
|
||||
|
||||
type TelegramUser struct {
|
||||
@@ -790,6 +791,14 @@ func (st *Storage) storeDiscordUsers() error {
|
||||
return storeJSON(st.discord_path, st.discord)
|
||||
}
|
||||
|
||||
func (st *Storage) loadMatrixUsers() error {
|
||||
return loadJSON(st.matrix_path, &st.matrix)
|
||||
}
|
||||
|
||||
func (st *Storage) storeMatrixUsers() error {
|
||||
return storeJSON(st.matrix_path, st.matrix)
|
||||
}
|
||||
|
||||
func (st *Storage) loadCustomEmails() error {
|
||||
return loadJSON(st.customEmails_path, &st.customEmails)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func genAuthToken() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
pin := make([]rune, 8)
|
||||
for i := range pin {
|
||||
if i == 2 || i == 5 {
|
||||
if (i+1)%3 == 0 {
|
||||
pin[i] = '-'
|
||||
} else {
|
||||
pin[i] = runes[rand.Intn(len(runes))]
|
||||
@@ -146,6 +146,9 @@ func (t *TelegramDaemon) QuoteReply(upd *tg.Update, content string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var escapedChars = []string{"_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(", "\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>", "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|", "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!"}
|
||||
var escaper = strings.NewReplacer(escapedChars...)
|
||||
|
||||
// Send will send a telegram message to a list of chat IDs. message.text is used if no markdown is given.
|
||||
func (t *TelegramDaemon) Send(message *Message, ID ...int64) error {
|
||||
for _, id := range ID {
|
||||
@@ -153,7 +156,8 @@ func (t *TelegramDaemon) Send(message *Message, ID ...int64) error {
|
||||
if message.Markdown == "" {
|
||||
msg = tg.NewMessage(id, message.Text)
|
||||
} else {
|
||||
msg = tg.NewMessage(id, strings.ReplaceAll(message.Markdown, ".", "\\."))
|
||||
text := escaper.Replace(message.Markdown)
|
||||
msg = tg.NewMessage(id, text)
|
||||
msg.ParseMode = "MarkdownV2"
|
||||
}
|
||||
_, err := t.bot.Send(msg)
|
||||
|
||||
4
tray.go
@@ -9,6 +9,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
// "github.com/getlantern/systray"
|
||||
)
|
||||
|
||||
@@ -36,6 +37,7 @@ func onReady() {
|
||||
mStart := systray.AddMenuItem("Start", "Start jfa-go")
|
||||
mStop := systray.AddMenuItem("Stop", "Stop jfa-go")
|
||||
mRestart := systray.AddMenuItem("Restart", "Restart jfa-go")
|
||||
mOpenLogs := systray.AddMenuItem("Open logs", "Open jfa-go log file.")
|
||||
as := NewAutostart("jfa-go", "A user management system for Jellyfin", "Run on login", "Run jfa-go on user login.")
|
||||
mQuit := systray.AddMenuItem("Quit", "Quit jfa-go")
|
||||
|
||||
@@ -88,6 +90,8 @@ func onReady() {
|
||||
mStop.Enable()
|
||||
mRestart.Enable()
|
||||
}
|
||||
case <-mOpenLogs.ClickedCh:
|
||||
open.Start(logPath)
|
||||
case <-mQuit.ClickedCh:
|
||||
systray.Quit()
|
||||
// case <-mOnLogin.ClickedCh:
|
||||
|
||||
@@ -63,6 +63,8 @@ window.availableProfiles = window.availableProfiles || [];
|
||||
|
||||
window.modals.updateInfo = new Modal(document.getElementById("modal-update"));
|
||||
|
||||
window.modals.matrix = new Modal(document.getElementById("modal-matrix"));
|
||||
|
||||
if (window.telegramEnabled) {
|
||||
window.modals.telegram = new Modal(document.getElementById("modal-telegram"));
|
||||
}
|
||||
|
||||
77
ts/form.ts
@@ -1,6 +1,6 @@
|
||||
import { Modal } from "./modules/modal.js";
|
||||
import { notificationBox, whichAnimationEvent } from "./modules/common.js";
|
||||
import { _get, _post, toggleLoader, toDateString } from "./modules/common.js";
|
||||
import { _get, _post, toggleLoader, addLoader, removeLoader, toDateString } from "./modules/common.js";
|
||||
import { loadLangSelector } from "./modules/lang.js";
|
||||
|
||||
interface formWindow extends Window {
|
||||
@@ -9,6 +9,7 @@ interface formWindow extends Window {
|
||||
successModal: Modal;
|
||||
telegramModal: Modal;
|
||||
discordModal: Modal;
|
||||
matrixModal: Modal;
|
||||
confirmationModal: Modal
|
||||
code: string;
|
||||
messages: { [key: string]: string };
|
||||
@@ -20,6 +21,8 @@ interface formWindow extends Window {
|
||||
discordStartCommand: string;
|
||||
discordInviteLink: boolean;
|
||||
discordServerName: string;
|
||||
matrixRequired: boolean;
|
||||
matrixUserID: string;
|
||||
userExpiryEnabled: boolean;
|
||||
userExpiryMonths: number;
|
||||
userExpiryDays: number;
|
||||
@@ -150,6 +153,69 @@ if (window.discordEnabled) {
|
||||
};
|
||||
}
|
||||
|
||||
var matrixVerified = false;
|
||||
var matrixPIN = "";
|
||||
if (window.matrixEnabled) {
|
||||
window.matrixModal = new Modal(document.getElementById("modal-matrix"), window.matrixRequired);
|
||||
const matrixButton = document.getElementById("link-matrix") as HTMLSpanElement;
|
||||
matrixButton.onclick = window.matrixModal.show;
|
||||
const submitButton = document.getElementById("matrix-send") as HTMLSpanElement;
|
||||
const input = document.getElementById("matrix-userid") as HTMLInputElement;
|
||||
let userID = "";
|
||||
submitButton.onclick = () => {
|
||||
addLoader(submitButton);
|
||||
if (userID == "") {
|
||||
const send = {
|
||||
user_id: input.value
|
||||
};
|
||||
_post("/invite/" + window.code + "/matrix/user", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
removeLoader(submitButton);
|
||||
userID = input.value;
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("errorUnknown", window.messages["errorUnknown"]);
|
||||
window.matrixModal.close();
|
||||
return;
|
||||
}
|
||||
submitButton.classList.add("~positive");
|
||||
submitButton.classList.remove("~info");
|
||||
setTimeout(() => {
|
||||
submitButton.classList.add("~info");
|
||||
submitButton.classList.remove("~positive");
|
||||
}, 2000);
|
||||
input.placeholder = "PIN";
|
||||
input.value = "";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_get("/invite/" + window.code + "/matrix/verified/" + userID + "/" + input.value, null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
removeLoader(submitButton)
|
||||
const valid = req.response["success"] as boolean;
|
||||
if (valid) {
|
||||
window.matrixModal.close();
|
||||
window.notifications.customPositive("successVerified", "", window.messages["verified"]);
|
||||
matrixVerified = true;
|
||||
matrixPIN = input.value;
|
||||
matrixButton.classList.add("unfocused");
|
||||
document.getElementById("contact-via").classList.remove("unfocused");
|
||||
const radio = document.getElementById("contact-via-discord") as HTMLInputElement;
|
||||
radio.checked = true;
|
||||
} else {
|
||||
window.notifications.customError("errorInvalidPIN", window.messages["errorInvalidPIN"]);
|
||||
submitButton.classList.add("~critical");
|
||||
submitButton.classList.remove("~info");
|
||||
setTimeout(() => {
|
||||
submitButton.classList.add("~info");
|
||||
submitButton.classList.remove("~critical");
|
||||
}, 800);
|
||||
}
|
||||
}
|
||||
},);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (window.confirmation) {
|
||||
window.confirmationModal = new Modal(document.getElementById("modal-confirmation"), true);
|
||||
}
|
||||
@@ -229,6 +295,8 @@ interface sendDTO {
|
||||
telegram_contact?: boolean;
|
||||
discord_pin?: string;
|
||||
discord_contact?: boolean;
|
||||
matrix_pin?: string;
|
||||
matrix_contact?: boolean;
|
||||
}
|
||||
|
||||
const create = (event: SubmitEvent) => {
|
||||
@@ -254,6 +322,13 @@ const create = (event: SubmitEvent) => {
|
||||
send.discord_contact = true;
|
||||
}
|
||||
}
|
||||
if (matrixVerified) {
|
||||
send.matrix_pin = matrixPIN;
|
||||
const radio = document.getElementById("contact-via-matrix") as HTMLInputElement;
|
||||
if (radio.checked) {
|
||||
send.matrix_contact = true;
|
||||
}
|
||||
}
|
||||
_post("/newUser", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
let vals = req.response as respDTO;
|
||||
|
||||
@@ -18,6 +18,8 @@ interface User {
|
||||
discord: string;
|
||||
notify_discord: boolean;
|
||||
discord_id: string;
|
||||
matrix: string;
|
||||
notify_matrix: boolean;
|
||||
}
|
||||
|
||||
interface getPinResponse {
|
||||
@@ -44,13 +46,27 @@ class user implements User {
|
||||
private _discordUsername: string;
|
||||
private _discordID: string;
|
||||
private _notifyDiscord: boolean;
|
||||
private _matrix: HTMLTableDataCellElement;
|
||||
private _matrixID: string;
|
||||
private _notifyMatrix: boolean;
|
||||
private _expiry: HTMLTableDataCellElement;
|
||||
private _expiryUnix: number;
|
||||
private _lastActive: HTMLTableDataCellElement;
|
||||
private _lastActiveUnix: number;
|
||||
private _notifyDropdown: HTMLDivElement;
|
||||
id = "";
|
||||
private _selected: boolean;
|
||||
|
||||
private _lastNotifyMethod = (): string => {
|
||||
// Telegram, Matrix, Discord
|
||||
const telegram = this._telegramUsername && this._telegramUsername != "";
|
||||
const discord = this._discordUsername && this._discordUsername != "";
|
||||
const matrix = this._matrixID && this._matrixID != "";
|
||||
if (discord) return "discord";
|
||||
if (matrix) return "matrix";
|
||||
if (telegram) return "telegram";
|
||||
}
|
||||
|
||||
get selected(): boolean { return this._selected; }
|
||||
set selected(state: boolean) {
|
||||
this._selected = state;
|
||||
@@ -96,105 +112,179 @@ class user implements User {
|
||||
|
||||
get notify_email(): boolean { return this._notifyEmail; }
|
||||
set notify_email(s: boolean) {
|
||||
this._notifyEmail = s;
|
||||
if (window.telegramEnabled && this._telegramUsername != "") {
|
||||
const email = this._telegram.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
|
||||
if (email) {
|
||||
email.checked = s;
|
||||
if (this._notifyDropdown) {
|
||||
(this._notifyDropdown.querySelector(".accounts-contact-email") as HTMLInputElement).checked = s;
|
||||
}
|
||||
}
|
||||
|
||||
private _constructDropdown = (): HTMLDivElement => {
|
||||
const el = document.createElement("div") as HTMLDivElement;
|
||||
const telegram = this._telegramUsername != "";
|
||||
const discord = this._discordUsername != "";
|
||||
const matrix = this._matrixID != "";
|
||||
if (!telegram && !discord && !matrix) return;
|
||||
let innerHTML = `
|
||||
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
|
||||
<div class="dropdown manual">
|
||||
<div class="dropdown-display lg">
|
||||
<div class="card ~neutral !low">
|
||||
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
|
||||
<label class="row switch pb-1 mt-half">
|
||||
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
|
||||
</span>Email</span>
|
||||
</label>
|
||||
<div class="accounts-area-telegram">
|
||||
<label class="row switch pb-1">
|
||||
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
|
||||
<span>Telegram</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="accounts-area-discord">
|
||||
<label class="row switch pb-1">
|
||||
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
|
||||
<span>Discord</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="accounts-area-matrix">
|
||||
<label class="row switch pb-1">
|
||||
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-matrix">
|
||||
<span>Matrix</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
el.innerHTML = innerHTML;
|
||||
const button = el.querySelector("i");
|
||||
const dropdown = el.querySelector("div.dropdown") as HTMLDivElement;
|
||||
const checks = el.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||
for (let i = 0; i < checks.length; i++) {
|
||||
checks[i].onclick = () => this._setNotifyMethod();
|
||||
}
|
||||
|
||||
button.onclick = () => {
|
||||
dropdown.classList.add("selected");
|
||||
document.addEventListener("click", outerClickListener);
|
||||
};
|
||||
const outerClickListener = (event: Event) => {
|
||||
if (!(event.target instanceof HTMLElement && (el.contains(event.target) || button.contains(event.target)))) {
|
||||
dropdown.classList.remove("selected");
|
||||
document.removeEventListener("click", outerClickListener);
|
||||
}
|
||||
};
|
||||
return el;
|
||||
}
|
||||
|
||||
get matrix(): string { return this._matrixID; }
|
||||
set matrix(u: string) {
|
||||
if (!window.matrixEnabled) {
|
||||
this._notifyDropdown.querySelector(".accounts-area-matrix").classList.add("unfocused");
|
||||
return;
|
||||
}
|
||||
const lastNotifyMethod = this._lastNotifyMethod() == "matrix";
|
||||
this._matrixID = u;
|
||||
if (!u) {
|
||||
this._notifyDropdown.querySelector(".accounts-area-matrix").classList.add("unfocused");
|
||||
this._matrix.innerHTML = `
|
||||
<span class="chip btn !low">${window.lang.strings("add")}</span>
|
||||
<input type="text" class="input ~neutral !normal stealth-input unfocused" placeholder="@user:riot.im">
|
||||
`;
|
||||
(this._matrix.querySelector("span") as HTMLSpanElement).onclick = this._addMatrix;
|
||||
} else {
|
||||
this._notifyDropdown.querySelector(".accounts-area-matrix").classList.remove("unfocused");
|
||||
this._matrix.innerHTML = `
|
||||
<div class="table-inline">
|
||||
${u}
|
||||
</div>
|
||||
`;
|
||||
if (lastNotifyMethod) {
|
||||
(this._matrix.querySelector(".table-inline") as HTMLDivElement).appendChild(this._notifyDropdown);
|
||||
}
|
||||
}
|
||||
if (window.discordEnabled && this._discordUsername) {
|
||||
const email = this._discord.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
|
||||
email.checked = s;
|
||||
}
|
||||
|
||||
private _addMatrix = () => {
|
||||
const addButton = this._matrix.querySelector(".btn") as HTMLSpanElement;
|
||||
const icon = this._matrix.querySelector("i");
|
||||
const input = this._matrix.querySelector("input.stealth-input") as HTMLInputElement;
|
||||
if (addButton.classList.contains("chip")) {
|
||||
input.classList.remove("unfocused");
|
||||
addButton.innerHTML = `<i class="ri-check-line"></i>`;
|
||||
addButton.classList.remove("chip")
|
||||
if (icon) {
|
||||
icon.classList.add("unfocused");
|
||||
}
|
||||
} else {
|
||||
if (input.value.charAt(0) != "@" || !input.value.includes(":")) return;
|
||||
const send = {
|
||||
jf_id: this.id,
|
||||
user_id: input.value
|
||||
}
|
||||
_post("/users/matrix", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
document.dispatchEvent(new CustomEvent("accounts-reload"));
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("errorConnectMatrix", window.lang.notif("errorFailureCheckLogs"));
|
||||
return;
|
||||
}
|
||||
window.notifications.customSuccess("connectMatrix", window.lang.notif("accountConnected"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get notify_matrix(): boolean { return this._notifyMatrix; }
|
||||
set notify_matrix(s: boolean) {
|
||||
if (this._notifyDropdown) {
|
||||
(this._notifyDropdown.querySelector(".accounts-contact-matrix") as HTMLInputElement).checked = s;
|
||||
}
|
||||
}
|
||||
|
||||
get telegram(): string { return this._telegramUsername; }
|
||||
set telegram(u: string) {
|
||||
if (!window.telegramEnabled) return;
|
||||
if (!window.telegramEnabled) {
|
||||
this._notifyDropdown.querySelector(".accounts-area-telegram").classList.add("unfocused");
|
||||
return;
|
||||
}
|
||||
const lastNotifyMethod = this._lastNotifyMethod() == "telegram";
|
||||
this._telegramUsername = u;
|
||||
if (u == "") {
|
||||
this._telegram.innerHTML = `<span class="chip btn !low">Add</span>`;
|
||||
if (!u) {
|
||||
this._notifyDropdown.querySelector(".accounts-area-telegram").classList.add("unfocused");
|
||||
this._telegram.innerHTML = `<span class="chip btn !low">${window.lang.strings("add")}</span>`;
|
||||
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
|
||||
} else {
|
||||
let innerHTML = `
|
||||
this._notifyDropdown.querySelector(".accounts-area-telegram").classList.remove("unfocused");
|
||||
this._telegram.innerHTML = `
|
||||
<div class="table-inline">
|
||||
<a href="https://t.me/${u}" target="_blank">@${u}</a>
|
||||
</div>
|
||||
`;
|
||||
if (!window.discordEnabled || !this._discordUsername) {
|
||||
innerHTML += `
|
||||
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
|
||||
<div class="dropdown manual">
|
||||
<div class="dropdown-display lg">
|
||||
<div class="card ~neutral !low">
|
||||
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
|
||||
<label class="row switch pb-1 mt-half">
|
||||
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
|
||||
<span>Email</span>
|
||||
</label>
|
||||
<label class="row switch pb-1">
|
||||
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
|
||||
<span>Telegram</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
innerHTML += "</div>";
|
||||
this._telegram.innerHTML = innerHTML;
|
||||
if (!window.discordEnabled || !this._discordUsername) {
|
||||
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
|
||||
const button = this._telegram.querySelector("i");
|
||||
const dropdown = this._telegram.querySelector("div.dropdown") as HTMLDivElement;
|
||||
const checks = this._telegram.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||
for (let i = 0; i < checks.length; i++) {
|
||||
checks[i].onclick = () => this._setNotifyMethod("telegram");
|
||||
}
|
||||
|
||||
button.onclick = () => {
|
||||
dropdown.classList.add("selected");
|
||||
document.addEventListener("click", outerClickListener);
|
||||
};
|
||||
const outerClickListener = (event: Event) => {
|
||||
if (!(event.target instanceof HTMLElement && (this._telegram.contains(event.target) || button.contains(event.target)))) {
|
||||
dropdown.classList.remove("selected");
|
||||
document.removeEventListener("click", outerClickListener);
|
||||
}
|
||||
};
|
||||
if (lastNotifyMethod) {
|
||||
(this._telegram.querySelector(".table-inline") as HTMLDivElement).appendChild(this._notifyDropdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get notify_telegram(): boolean { return this._notifyTelegram; }
|
||||
set notify_telegram(s: boolean) {
|
||||
if (!window.telegramEnabled || !this._telegramUsername) return;
|
||||
this._notifyTelegram = s;
|
||||
const telegram = this._telegram.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
|
||||
if (telegram) {
|
||||
telegram.checked = s;
|
||||
}
|
||||
if (window.discordEnabled && this._discordUsername) {
|
||||
const telegram = this._discord.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
|
||||
telegram.checked = s;
|
||||
if (this._notifyDropdown) {
|
||||
(this._notifyDropdown.querySelector(".accounts-contact-telegram") as HTMLInputElement).checked = s;
|
||||
}
|
||||
}
|
||||
|
||||
private _setNotifyMethod = (mode: string = "telegram") => {
|
||||
let el: HTMLElement;
|
||||
if (mode == "telegram") { el = this._telegram }
|
||||
else if (mode == "discord") { el = this._discord }
|
||||
const email = el.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
|
||||
private _setNotifyMethod = () => {
|
||||
const email = this._notifyDropdown.getElementsByClassName("accounts-contact-email")[0] as HTMLInputElement;
|
||||
let send = {
|
||||
id: this.id,
|
||||
email: email.checked
|
||||
}
|
||||
if (window.telegramEnabled && this._telegramUsername) {
|
||||
const telegram = el.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
|
||||
const telegram = this._notifyDropdown.getElementsByClassName("accounts-contact-telegram")[0] as HTMLInputElement;
|
||||
send["telegram"] = telegram.checked;
|
||||
}
|
||||
if (window.discordEnabled && this._discordUsername) {
|
||||
const discord = el.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
|
||||
const discord = this._notifyDropdown.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
|
||||
send["discord"] = discord.checked;
|
||||
}
|
||||
_post("/users/contact", send, (req: XMLHttpRequest) => {
|
||||
@@ -219,62 +309,26 @@ class user implements User {
|
||||
|
||||
get discord(): string { return this._discordUsername; }
|
||||
set discord(u: string) {
|
||||
if (!window.discordEnabled) return;
|
||||
if (!window.discordEnabled) {
|
||||
this._notifyDropdown.querySelector(".accounts-area-discord").classList.add("unfocused");
|
||||
return;
|
||||
}
|
||||
const lastNotifyMethod = this._lastNotifyMethod() == "discord";
|
||||
this._discordUsername = u;
|
||||
if (u == "") {
|
||||
if (!u) {
|
||||
this._discord.innerHTML = `<span class="chip btn !low">Add</span>`;
|
||||
(this._discord.querySelector("span") as HTMLSpanElement).onclick = () => addDiscord(this.id);
|
||||
this._notifyDropdown.querySelector(".accounts-area-discord").classList.add("unfocused");
|
||||
} else {
|
||||
let innerHTML = `
|
||||
this._notifyDropdown.querySelector(".accounts-area-discord").classList.remove("unfocused");
|
||||
this._discord.innerHTML = `
|
||||
<div class="table-inline">
|
||||
<a href="https://discord.com/users/${this._discordID}" class="discord-link" target="_blank">${u}</a>
|
||||
<i class="icon ri-settings-2-line ml-half dropdown-button"></i>
|
||||
<div class="dropdown manual">
|
||||
<div class="dropdown-display lg">
|
||||
<div class="card ~neutral !low">
|
||||
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
|
||||
<label class="row switch pb-1 mt-half">
|
||||
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email">
|
||||
<span>Email</span>
|
||||
</label>
|
||||
<label class="row switch pb-1">
|
||||
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord">
|
||||
<span>Discord</span>
|
||||
</label>
|
||||
`;
|
||||
if (window.telegramEnabled && this._telegramUsername != "") {
|
||||
innerHTML += `
|
||||
<label class="row switch pb-1">
|
||||
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram">
|
||||
<span>Telegram</span>
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
innerHTML += `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this._discord.innerHTML = innerHTML;
|
||||
// Javascript is necessary as including the button inside the dropdown would make it too wide to display next to the username.
|
||||
const button = this._discord.querySelector("i");
|
||||
const dropdown = this._discord.querySelector("div.dropdown") as HTMLDivElement;
|
||||
const checks = this._discord.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
|
||||
for (let i = 0; i < checks.length; i++) {
|
||||
checks[i].onclick = () => this._setNotifyMethod("discord");
|
||||
if (lastNotifyMethod) {
|
||||
(this._discord.querySelector(".table-inline") as HTMLDivElement).appendChild(this._notifyDropdown);
|
||||
}
|
||||
|
||||
button.onclick = () => {
|
||||
dropdown.classList.add("selected");
|
||||
document.addEventListener("click", outerClickListener);
|
||||
};
|
||||
const outerClickListener = (event: Event) => {
|
||||
if (!(event.target instanceof HTMLElement && (this._discord.contains(event.target) || button.contains(event.target)))) {
|
||||
dropdown.classList.remove("selected");
|
||||
document.removeEventListener("click", outerClickListener);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,13 +342,8 @@ class user implements User {
|
||||
|
||||
get notify_discord(): boolean { return this._notifyDiscord; }
|
||||
set notify_discord(s: boolean) {
|
||||
if (!window.discordEnabled || !this._discordUsername) return;
|
||||
this._notifyDiscord = s;
|
||||
const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
|
||||
discord.checked = s;
|
||||
if (window.telegramEnabled && this._telegramUsername != "") {
|
||||
const discord = this._discord.getElementsByClassName("accounts-contact-discord")[0] as HTMLInputElement;
|
||||
discord.checked = s;
|
||||
if (this._notifyDropdown) {
|
||||
(this._notifyDropdown.querySelector(".accounts-contact-discord") as HTMLInputElement).checked = s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +382,11 @@ class user implements User {
|
||||
<td class="accounts-telegram"></td>
|
||||
`;
|
||||
}
|
||||
if (window.matrixEnabled) {
|
||||
innerHTML += `
|
||||
<td class="accounts-matrix"></td>
|
||||
`;
|
||||
}
|
||||
if (window.discordEnabled) {
|
||||
innerHTML += `
|
||||
<td class="accounts-discord"></td>
|
||||
@@ -352,10 +406,13 @@ class user implements User {
|
||||
this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement;
|
||||
this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement;
|
||||
this._discord = this._row.querySelector(".accounts-discord") as HTMLTableDataCellElement;
|
||||
this._matrix = this._row.querySelector(".accounts-matrix") as HTMLTableDataCellElement;
|
||||
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
|
||||
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
|
||||
this._check.onchange = () => { this.selected = this._check.checked; }
|
||||
|
||||
this._notifyDropdown = this._constructDropdown();
|
||||
|
||||
const toggleStealthInput = () => {
|
||||
if (this._emailEditButton.classList.contains("ri-edit-line")) {
|
||||
this._email.innerHTML = emailEditor;
|
||||
@@ -458,14 +515,20 @@ class user implements User {
|
||||
this.id = user.id;
|
||||
this.name = user.name;
|
||||
this.email = user.email || "";
|
||||
// Little hack to get settings cogs to appear on first load
|
||||
this._discordUsername = user.discord;
|
||||
this._telegramUsername = user.telegram;
|
||||
this._matrixID = user.matrix;
|
||||
this.discord = user.discord;
|
||||
this.telegram = user.telegram;
|
||||
this.matrix = user.matrix;
|
||||
this.last_active = user.last_active;
|
||||
this.admin = user.admin;
|
||||
this.disabled = user.disabled;
|
||||
this.expiry = user.expiry;
|
||||
this.notify_discord = user.notify_discord;
|
||||
this.notify_telegram = user.notify_telegram;
|
||||
this.notify_matrix = user.notify_matrix;
|
||||
this.notify_email = user.notify_email;
|
||||
this.discord_id = user.discord_id;
|
||||
}
|
||||
|
||||
@@ -105,7 +105,11 @@ export class notificationBox implements NotificationBox {
|
||||
private _error = (message: string): HTMLElement => {
|
||||
const noti = document.createElement('aside');
|
||||
noti.classList.add("aside", "~critical", "!normal", "mt-half", "notification-error");
|
||||
noti.innerHTML = `<strong>${window.lang.strings("error")}:</strong> ${message}`;
|
||||
let error = "";
|
||||
if (window.lang) {
|
||||
error = window.lang.strings("error") + ":"
|
||||
}
|
||||
noti.innerHTML = `<strong>${error}</strong> ${message}`;
|
||||
const closeButton = document.createElement('span') as HTMLSpanElement;
|
||||
closeButton.classList.add("button", "~critical", "!low", "ml-1");
|
||||
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { _get, _post, toggleLoader } from "../modules/common.js";
|
||||
import { _get, _post, toggleLoader, addLoader, removeLoader } from "../modules/common.js";
|
||||
import { Marked } from "@ts-stack/markdown";
|
||||
import { stripMarkdown } from "../modules/stripmd.js";
|
||||
|
||||
@@ -666,6 +666,40 @@ export class settingsList {
|
||||
}
|
||||
}
|
||||
|
||||
private _addMatrix = () => {
|
||||
// Modify the login modal, why not
|
||||
const modal = document.getElementById("form-matrix") as HTMLFormElement;
|
||||
modal.onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
const button = modal.querySelector("span.submit") as HTMLSpanElement;
|
||||
addLoader(button);
|
||||
let send = {
|
||||
homeserver: (document.getElementById("matrix-homeserver") as HTMLInputElement).value,
|
||||
username: (document.getElementById("matrix-user") as HTMLInputElement).value,
|
||||
password: (document.getElementById("matrix-password") as HTMLInputElement).value
|
||||
}
|
||||
_post("/matrix/login", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
removeLoader(button);
|
||||
if (req.status == 400) {
|
||||
window.notifications.customError("errorUnknown", window.lang.notif(req.response["error"] as string));
|
||||
return;
|
||||
} else if (req.status == 401) {
|
||||
window.notifications.customError("errorUnauthorized", req.response["error"] as string);
|
||||
return;
|
||||
} else if (req.status == 500) {
|
||||
window.notifications.customError("errorAddMatrix", window.lang.notif("errorFailureCheckLogs"));
|
||||
return;
|
||||
}
|
||||
window.modals.matrix.close();
|
||||
_post("/restart", null, () => {});
|
||||
window.location.reload();
|
||||
}
|
||||
}, true);
|
||||
};
|
||||
window.modals.matrix.show();
|
||||
}
|
||||
|
||||
reload = () => _get("/config", null, (req: XMLHttpRequest) => {
|
||||
if (req.readyState == 4) {
|
||||
if (req.status != 200) {
|
||||
@@ -698,6 +732,17 @@ export class settingsList {
|
||||
icon.onclick = () => window.updater.checkForUpdates(window.modals.updateInfo.show);
|
||||
}
|
||||
this.addSection(name, settings.sections[name], icon);
|
||||
} else if (name == "matrix" && !window.matrixEnabled) {
|
||||
const addButton = document.createElement("div");
|
||||
addButton.classList.add("tooltip", "left");
|
||||
addButton.innerHTML = `
|
||||
<span class="button ~neutral !normal">+</span>
|
||||
<span class="content sm">
|
||||
${window.lang.strings("linkMatrix")}
|
||||
</span>
|
||||
`;
|
||||
(addButton.querySelector("span.button") as HTMLSpanElement).onclick = this._addMatrix;
|
||||
this.addSection(name, settings.sections[name], addButton);
|
||||
} else {
|
||||
this.addSection(name, settings.sections[name]);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ declare interface Window {
|
||||
emailEnabled: boolean;
|
||||
telegramEnabled: boolean;
|
||||
discordEnabled: boolean;
|
||||
matrixEnabled: boolean;
|
||||
ombiEnabled: boolean;
|
||||
usernameEnabled: boolean;
|
||||
token: string;
|
||||
@@ -103,6 +104,7 @@ declare interface Modals {
|
||||
updateInfo: Modal;
|
||||
telegram: Modal;
|
||||
discord: Modal;
|
||||
matrix: Modal;
|
||||
}
|
||||
|
||||
interface Invite {
|
||||
|
||||
176
updater.go
@@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -347,7 +347,11 @@ func getBuildName() string {
|
||||
if arch == "" {
|
||||
return ""
|
||||
}
|
||||
return operatingSystem + "_" + arch
|
||||
tray := ""
|
||||
if TRAY {
|
||||
tray = "TrayIcon_"
|
||||
}
|
||||
return tray + operatingSystem + "_" + arch
|
||||
}
|
||||
|
||||
func (ud *Updater) downloadInternal(assets *[]GHAsset, tag Tag) (applyUpdate ApplyUpdate, status int, err error) {
|
||||
@@ -392,69 +396,129 @@ func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
gz, err := gzip.NewReader(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
status = -1
|
||||
return
|
||||
}
|
||||
defer gz.Close()
|
||||
tarReader := tar.NewReader(gz)
|
||||
var header *tar.Header
|
||||
for {
|
||||
header, err = tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
zp, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
||||
if err != nil {
|
||||
status = -1
|
||||
return
|
||||
}
|
||||
for _, zf := range zp.File {
|
||||
if zf.Name != ud.binary {
|
||||
continue
|
||||
}
|
||||
var file string
|
||||
file, err = os.Executable()
|
||||
if err != nil {
|
||||
status = -1
|
||||
return
|
||||
}
|
||||
switch header.Typeflag {
|
||||
case tar.TypeReg:
|
||||
// Search only for file named ud.binary
|
||||
if header.Name == ud.binary {
|
||||
var file string
|
||||
file, err = os.Executable()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var path string
|
||||
path, err = filepath.EvalSymlinks(file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var info fs.FileInfo
|
||||
info, err = os.Stat(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mode := info.Mode()
|
||||
var f *os.File
|
||||
f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, tarReader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
applyUpdate = func() error {
|
||||
oldName := path + "-" + version + "-" + commit
|
||||
err := os.Rename(path, oldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Rename(path+"_", path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Remove(oldName)
|
||||
}
|
||||
return
|
||||
}
|
||||
var path string
|
||||
path, err = filepath.EvalSymlinks(file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var info fs.FileInfo
|
||||
info, err = os.Stat(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mode := info.Mode()
|
||||
var unzippedFile io.ReadCloser
|
||||
unzippedFile, err = zf.Open()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer unzippedFile.Close()
|
||||
var f *os.File
|
||||
f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, unzippedFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
applyUpdate = func() error {
|
||||
oldName := path + "-" + version + "-" + commit
|
||||
err := os.Rename(path, oldName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Rename(path+"_", path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Remove(oldName)
|
||||
}
|
||||
return
|
||||
}
|
||||
// gz, err := gzip.NewReader(resp.Body)
|
||||
// if err != nil {
|
||||
// status = -1
|
||||
// return
|
||||
// }
|
||||
// defer gz.Close()
|
||||
// tarReader := tar.NewReader(gz)
|
||||
// var header *tar.Header
|
||||
// for {
|
||||
// header, err = tarReader.Next()
|
||||
// if err == io.EOF {
|
||||
// break
|
||||
// }
|
||||
// if err != nil {
|
||||
// status = -1
|
||||
// return
|
||||
// }
|
||||
// switch header.Typeflag {
|
||||
// case tar.TypeReg:
|
||||
// // Search only for file named ud.binary
|
||||
// if header.Name == ud.binary {
|
||||
// var file string
|
||||
// file, err = os.Executable()
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// var path string
|
||||
// path, err = filepath.EvalSymlinks(file)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// var info fs.FileInfo
|
||||
// info, err = os.Stat(path)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// mode := info.Mode()
|
||||
// var f *os.File
|
||||
// f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// defer f.Close()
|
||||
// _, err = io.Copy(f, tarReader)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// applyUpdate = func() error {
|
||||
// oldName := path + "-" + version + "-" + commit
|
||||
// err := os.Rename(path, oldName)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// err = os.Rename(path+"_", path)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return os.Remove(oldName)
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
err = errors.New("Couldn't find file: " + ud.binary)
|
||||
return
|
||||
}
|
||||
|
||||
6
views.go
@@ -123,6 +123,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
|
||||
"email_enabled": emailEnabled,
|
||||
"telegram_enabled": telegramEnabled,
|
||||
"discord_enabled": discordEnabled,
|
||||
"matrix_enabled": matrixEnabled,
|
||||
"notifications": notificationsEnabled,
|
||||
"version": version,
|
||||
"commit": commit,
|
||||
@@ -287,6 +288,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
"langName": lang,
|
||||
"telegramEnabled": telegramEnabled,
|
||||
"discordEnabled": discordEnabled,
|
||||
"matrixEnabled": matrixEnabled,
|
||||
}
|
||||
if telegramEnabled {
|
||||
data["telegramPIN"] = app.telegram.NewAuthToken()
|
||||
@@ -294,6 +296,10 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
||||
data["telegramURL"] = app.telegram.link
|
||||
data["telegramRequired"] = app.config.Section("telegram").Key("required").MustBool(false)
|
||||
}
|
||||
if matrixEnabled {
|
||||
data["matrixRequired"] = app.config.Section("matrix").Key("required").MustBool(false)
|
||||
data["matrixUser"] = app.matrix.userID
|
||||
}
|
||||
if discordEnabled {
|
||||
data["discordPIN"] = app.discord.NewAuthToken()
|
||||
data["discordUsername"] = app.discord.username
|
||||
|
||||