diff --git a/examples/login.yaml b/examples/login.yaml index f2339cf..6f434da 100644 --- a/examples/login.yaml +++ b/examples/login.yaml @@ -33,8 +33,17 @@ prefix: '.' authorized-users: - '@bob:example.org' +# Deactivated user PFP +# This is the profile image (in mxc:// format) that will be set to user profiles before deactivation. if left blank it will simply be removed +# Recommended is blank or `mxc://matrix.org/LaVsSRIBaLkOwvehbwDxEDio` +deactivatedpfp: '' + +# Deactivated user DisplayName +# Display name to set to any deactivated user. Unsure what will happen if left blank. +# Recommended is `Deactivated User` +deactivateddn: 'Deactivated User' # Location of dendrite.yaml # the interface steals database information from the dendrite.yaml configuration file to work # make sure whatever user is running the interface has permissions to open the dendrite.yaml file -dendriteyaml: '/opt/dendrite/dendrite.yaml' +dendriteyaml: '/opt/dendrite/dendrite.yaml' \ No newline at end of file diff --git a/index.js b/index.js index 6337778..0fb18fc 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,7 @@ +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + //Import dependencies import { AutojoinRoomsMixin, MatrixClient, SimpleFsStorageProvider } from "matrix-bot-sdk"; import fs from "fs"; @@ -14,6 +18,8 @@ let adminRoom = loginParsed["administration-room"]; const prefix = loginParsed["prefix"] const authorizedUsers = loginParsed["authorized-users"]; const dendriteyaml = loginParsed["dendriteyaml"] +const deactivatedpfp = loginParsed["deactivatedpfp"] +const deactivateddn = loginParsed["deactivateddn"] //if the interface config does not supply a path if (!dendriteyaml){ @@ -45,7 +51,6 @@ if(!dendriteconfig["global"]){ process.exit(1) } - //the bot sync something idk bro it was here in the example so i dont touch it ;-; const storage = new SimpleFsStorageProvider("bot.json"); @@ -181,6 +186,65 @@ async function makeDendriteReq (reqType, command, arg1, arg2, body) { } +async function makeUserReq (reqType, command, arg1, arg2, userToken, body, ) { + + //base url guaranteed to always be there + //Dendrite only accepts requests from localhost + let url = "http://localhost:" + port + "/_matrix/client/v3/" + command + + //if there is a first argument add it + if (arg1) url += ("/" + arg1) + + //if there is a second argument add it + if (arg2) url += ("/" + arg2) + + //if body is supplied, stringify it to send in http request + let bodyStr = null + if (body) bodyStr = JSON.stringify(body) + + try { + + //make the request and return whatever the promise resolves to + var response = await (await fetch(url, { + method: reqType, + headers: { + "Authorization": "Bearer " + userToken, + "Content-Type": "application/json" + }, + body:bodyStr + })).json() + + //.catch + } catch (e) { + client.sendHtmlNotice(adminRoom, ("❌ | could not make "+ url + " request with error\n
" + e + "
")) + } + + //.then + client.sendHtmlNotice(adminRoom, ("Ran "+ url + " with response
" + JSON.stringify(response) + "
")) + + return response + +} + +async function resetUserPwd (localpart, password, logout){ + + let userMxid = "@" + localpart + ":" + server + + if (!password) password = generateSecureOneTimeCode(35) + + makeDendriteReq("POST", "resetPassword", userMxid, null, { + password:password, + logout_devices:logout + }) + + return (password) + +} + +async function evacuateUser(mxid){ + makeDendriteReq("POST", "evacuateUser", mxid) +} + async function resetUserPwd (localpart, password, logout){ let userMxid = "@" + localpart + ":" + server @@ -336,6 +400,83 @@ commandHandlers.set("passwd", async ({contentByWords, event}) => { }) +commandHandlers.set("deactivate", async ({contentByWords, event}) => { + + //first argument provided + let user = contentByWords[1] + if(!user) { + + client.sendHtmlNotice(adminRoom, ("❌ | no user indicated.")) + + return; + } + + //remove the @ no matter if its a mxid or localpart + //user may mistakenly provide @localpart or localpart:server.tld and that is okay + // .substring(1) just removes the first char + if(user.startsWith('@')) user = user.substring(1) + + //decides if its a mxid or localpart + if(user.includes(":")){ + + //if its not a local user we cant do anything + if(!user.endsWith(":" + server)){ + + client.sendHtmlNotice(adminRoom, ("❌ | " + contentByWords[1] + " does not appear to be a valid user ID.")) + + return; + } + + //we want only the localpart + //while there are normal restrictions on user account chars, @ and : are the only characters that truly cannot be allowed + //it is possible for admins to modify dendrite to remove those restrictions, and this interface need not restrict to that needlessly + user = user.split(":")[0] + + } + + //reset the password as to lock out the user + let newpwd = await resetUserPwd(user, null, true) + + //idk some race conditions, this makes it work more reliably so sure + await delay(1000) + + //make login request + let response = await makeUserReq("POST", "login", null, null, null, { + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": user, + }, + "password": newpwd, + }) + + let userToken = response["access_token"] + + //no token means no successful login + if (!userToken) { + + client.sendNotice(adminRoom, "❌ | unable to log in. This may just be a momentary error.") + + return; + } + + let userMxid = "@" + user + ":" + server + + //sanatize pfp and displayname + await makeUserReq("PUT", "profile", userMxid, "avatar_url", userToken, {"avatar_url":deactivatedpfp}) + await makeUserReq("PUT", "profile", userMxid, "displayname", userToken, {"displayname":deactivateddn}) + + //deactivate the account + await makeUserReq("POST", "account", "deactivate", null, userToken, { + "auth": { + "type": "m.login.password", + "user": user, + "password": newpwd, + }, + }) + +}) + //data structure to hold various handlers let eventHandlers = new Map() diff --git a/package-lock.json b/package-lock.json index 94197a6..77b9527 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,17 +88,17 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/node": { - "version": "20.10.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", - "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", + "version": "20.11.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.4.tgz", + "integrity": "sha512-6I0fMH8Aoy2lOejL3s4LhyIYX34DPwY8bl5xlNjBvUEk8OHrcuzsFt+Ied4LvJihbtXPM+8zUqdydfIti86v9g==", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/qs": { - "version": "6.9.10", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", - "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==" + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==" }, "node_modules/@types/range-parser": { "version": "1.2.7", @@ -229,9 +229,9 @@ } }, "node_modules/async-lock": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz", - "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" }, "node_modules/asynckit": { "version": "0.4.0", @@ -1317,9 +1317,9 @@ } }, "node_modules/postcss": { - "version": "8.4.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", - "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", "funding": [ { "type": "opencollective", @@ -1578,14 +1578,15 @@ } }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", "dependencies": { "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.1" }, "engines": { "node": ">= 0.4"