diff --git a/api.go b/api.go
index 72b354e..d21645b 100644
--- a/api.go
+++ b/api.go
@@ -1550,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
@@ -1575,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) {
@@ -2367,6 +2372,42 @@ func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
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
diff --git a/html/admin.html b/html/admin.html
index 7591ed4..a486455 100644
--- a/html/admin.html
+++ b/html/admin.html
@@ -341,6 +341,19 @@
{{ end }}
+
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/matrix.go b/matrix.go
index 0eb8095..58cf55b 100644
--- a/matrix.go
+++ b/matrix.go
@@ -31,6 +31,13 @@ type MatrixUser struct {
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{
@@ -80,6 +87,27 @@ func newMatrixDaemon(app *appContext) (d *MatrixDaemon, err error) {
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)
diff --git a/models.go b/models.go
index 07f2d95..1c7b9cc 100644
--- a/models.go
+++ b/models.go
@@ -299,3 +299,9 @@ 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 d8b61fd..295f7eb 100644
--- a/router.go
+++ b/router.go
@@ -169,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)
@@ -183,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/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/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 785db4d..c06785e 100644
--- a/ts/typings/d.ts
+++ b/ts/typings/d.ts
@@ -104,6 +104,7 @@ declare interface Modals {
updateInfo: Modal;
telegram: Modal;
discord: Modal;
+ matrix: Modal;
}
interface Invite {