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"