diff --git a/api.go b/api.go
index 4c1ccb2..d21645b 100644
--- a/api.go
+++ b/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
diff --git a/config.go b/config.go
index d605ef5..e0e9ba3 100644
--- a/config.go
+++ b/config.go
@@ -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
}
diff --git a/config/config-base.json b/config/config-base.json
index 36bad28..d14f886 100644
--- a/config/config-base.json
+++ b/config/config-base.json
@@ -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,
diff --git a/email.go b/email.go
index d541a04..88a8bdb 100644
--- a/email.go
+++ b/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 {
diff --git a/go.mod b/go.mod
index ec77101..4dfa77f 100644
--- a/go.mod
+++ b/go.mod
@@ -38,6 +38,7 @@ 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
diff --git a/go.sum b/go.sum
index 9a577fc..f96fe96 100644
--- a/go.sum
+++ b/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=
diff --git a/html/admin.html b/html/admin.html
index 31db2b2..a486455 100644
--- a/html/admin.html
+++ b/html/admin.html
@@ -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 @@
{{ end }}
+
@@ -545,6 +559,9 @@
{{ if .telegram_enabled }}
Telegram |
{{ end }}
+ {{ if .matrix_enabled }}
+ Matrix |
+ {{ end }}
{{ if .discord_enabled }}
Discord |
{{ end }}
diff --git a/html/form-base.html b/html/form-base.html
index 855253b..be95c3e 100644
--- a/html/form-base.html
+++ b/html/form-base.html
@@ -22,6 +22,9 @@
window.discordPIN = "{{ .discordPIN }}";
window.discordInviteLink = {{ .discordInviteLink }};
window.discordServerName = "{{ .discordServerName }}";
+ window.matrixEnabled = {{ .matrixEnabled }};
+ window.matrixRequired = {{ .matrixRequired }};
+ window.matrixUserID = "{{ .matrixUser }}";
{{ end }}
diff --git a/html/form.html b/html/form.html
index 6b785d0..fecaccd 100644
--- a/html/form.html
+++ b/html/form.html
@@ -48,6 +48,24 @@
{{ end }}
+ {{ if .matrixEnabled }}
+
+
+
{{ .strings.linkMatrix }}
+
{{ .strings.matrixEnterUser }}
+
+
+
+
+
+
+
+ {{ .matrixUser }}
+
+
{{ .strings.submit }}
+
+
+ {{ end }}
@@ -84,7 +102,10 @@
{{ if .discordEnabled }}
{{ .strings.linkDiscord }}
{{ end }}
- {{ if or (.telegramEnabled) (.discordEnabled) }}
+ {{ if .matrixEnabled }}
+ {{ .strings.linkMatrix }}
+ {{ end }}
+ {{ if or (.telegramEnabled) (or .discordEnabled .matrixEnabled) }}
{{ end }}
+ {{ if .matrixEnabled }}
+
+ {{ end }}
{{ end }}
diff --git a/images/matrix/1.png b/images/matrix/1.png
new file mode 100644
index 0000000..8e1ae81
Binary files /dev/null and b/images/matrix/1.png differ
diff --git a/images/matrix/2.png b/images/matrix/2.png
new file mode 100644
index 0000000..0bf9bba
Binary files /dev/null and b/images/matrix/2.png differ
diff --git a/images/matrix/3.png b/images/matrix/3.png
new file mode 100644
index 0000000..126b4e5
Binary files /dev/null and b/images/matrix/3.png differ
diff --git a/images/matrix/4.png b/images/matrix/4.png
new file mode 100644
index 0000000..c941c5e
Binary files /dev/null and b/images/matrix/4.png differ
diff --git a/lang/admin/en-us.json b/lang/admin/en-us.json
index 3936b8c..1c53ded 100644
--- a/lang/admin/en-us.json
+++ b/lang/admin/en-us.json
@@ -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}.",
diff --git a/lang/common/en-us.json b/lang/common/en-us.json
index 3e9ef53..5f8ec44 100644
--- a/lang/common/en-us.json
+++ b/lang/common/en-us.json
@@ -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"
}
diff --git a/lang/form/en-us.json b/lang/form/en-us.json
index 6726c0b..7b85cfc 100644
--- a/lang/form/en-us.json
+++ b/lang/form/en-us.json
@@ -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": {
diff --git a/lang/telegram/en-us.json b/lang/telegram/en-us.json
index 642399f..e93aed7 100644
--- a/lang/telegram/en-us.json
+++ b/lang/telegram/en-us.json
@@ -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} ."
diff --git a/main.go b/main.go
index fa4749d..afaecc0 100644
--- a/main.go
+++ b/main.go
@@ -100,6 +100,7 @@ type appContext struct {
email *Emailer
telegram *TelegramDaemon
discord *DiscordDaemon
+ matrix *MatrixDaemon
info, debug, err logger.Logger
host string
port int
@@ -353,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()
@@ -590,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"
diff --git a/matrix.go b/matrix.go
new file mode 100644
index 0000000..58cf55b
--- /dev/null
+++ b/matrix.go
@@ -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 \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
diff --git a/models.go b/models.go
index e14030d..1c7b9cc 100644
--- a/models.go
+++ b/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"`
+}
diff --git a/router.go b/router.go
index a717d49..295f7eb 100644
--- a/router.go
+++ b/router.go
@@ -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)
+
}
}
diff --git a/storage.go b/storage.go
index c32fb75..ae2022a 100644
--- a/storage.go
+++ b/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)
}
diff --git a/telegram.go b/telegram.go
index 4cca16f..654d05b 100644
--- a/telegram.go
+++ b/telegram.go
@@ -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))]
diff --git a/ts/admin.ts b/ts/admin.ts
index edfb009..6efaa4e 100644
--- a/ts/admin.ts
+++ b/ts/admin.ts
@@ -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"));
}
diff --git a/ts/form.ts b/ts/form.ts
index 94d3874..b3b0815 100644
--- a/ts/form.ts
+++ b/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;
diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts
index b00ef97..17d8e06 100644
--- a/ts/modules/accounts.ts
+++ b/ts/modules/accounts.ts
@@ -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 = `
+
+
+ `;
+ el.innerHTML = innerHTML;
+ const button = el.querySelector("i");
+ const dropdown = el.querySelector("div.dropdown") as HTMLDivElement;
+ const checks = el.querySelectorAll("input") as NodeListOf;
+ 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 = `
+ ${window.lang.strings("add")}
+
+ `;
+ (this._matrix.querySelector("span") as HTMLSpanElement).onclick = this._addMatrix;
+ } else {
+ this._notifyDropdown.querySelector(".accounts-area-matrix").classList.remove("unfocused");
+ this._matrix.innerHTML = `
+
+ ${u}
+
+ `;
+ 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 = ``;
+ 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 = `Add`;
+ if (!u) {
+ this._notifyDropdown.querySelector(".accounts-area-telegram").classList.add("unfocused");
+ this._telegram.innerHTML = `${window.lang.strings("add")}`;
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
} else {
- let innerHTML = `
+ this._notifyDropdown.querySelector(".accounts-area-telegram").classList.remove("unfocused");
+ this._telegram.innerHTML = `
`;
- if (!window.discordEnabled || !this._discordUsername) {
- innerHTML += `
-
-
- `;
- }
- innerHTML += "";
- 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;
- 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 = `Add`;
(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 = `
`;
- 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;
- 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 {
|
`;
}
+ if (window.matrixEnabled) {
+ innerHTML += `
+ |
+ `;
+ }
if (window.discordEnabled) {
innerHTML += `
|
@@ -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;
}
diff --git a/ts/modules/common.ts b/ts/modules/common.ts
index f761a4c..05b6bcb 100644
--- a/ts/modules/common.ts
+++ b/ts/modules/common.ts
@@ -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 = `${window.lang.strings("error")}: ${message}`;
+ let error = "";
+ if (window.lang) {
+ error = window.lang.strings("error") + ":"
+ }
+ noti.innerHTML = `${error} ${message}`;
const closeButton = document.createElement('span') as HTMLSpanElement;
closeButton.classList.add("button", "~critical", "!low", "ml-1");
closeButton.innerHTML = ``;
diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts
index 6348154..8a639aa 100644
--- a/ts/modules/settings.ts
+++ b/ts/modules/settings.ts
@@ -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 = `
+ +
+
+ ${window.lang.strings("linkMatrix")}
+
+ `;
+ (addButton.querySelector("span.button") as HTMLSpanElement).onclick = this._addMatrix;
+ this.addSection(name, settings.sections[name], addButton);
} else {
this.addSection(name, settings.sections[name]);
}
diff --git a/ts/typings/d.ts b/ts/typings/d.ts
index eb5be55..c06785e 100644
--- a/ts/typings/d.ts
+++ b/ts/typings/d.ts
@@ -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 {
diff --git a/views.go b/views.go
index 31c69ed..b8efab0 100644
--- a/views.go
+++ b/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,
@@ -295,6 +296,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
"langName": lang,
"telegramEnabled": telegramEnabled,
"discordEnabled": discordEnabled,
+ "matrixEnabled": matrixEnabled,
}
if telegramEnabled {
data["telegramPIN"] = app.telegram.NewAuthToken()
@@ -302,6 +304,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