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 {