diff --git a/changelog.d/854.feature b/changelog.d/854.feature new file mode 100644 index 00000000..6413d949 --- /dev/null +++ b/changelog.d/854.feature @@ -0,0 +1 @@ +Add listusers command to Discord bot to list the users on the Matrix side. Thanks to @SethFalco! diff --git a/src/bot.ts b/src/bot.ts index 8bc73d41..81ea7bc3 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -144,7 +144,7 @@ export class DiscordBot { this.mxEventProcessor = new MatrixEventProcessor( new MatrixEventProcessorOpts(config, bridge, this, store), ); - this.discordCommandHandler = new DiscordCommandHandler(bridge, this); + this.discordCommandHandler = new DiscordCommandHandler(bridge, this, config); // init vars this.sentMessages = []; this.discordMessageQueue = {}; diff --git a/src/discordcommandhandler.ts b/src/discordcommandhandler.ts index f1d5fda0..5616ce9a 100644 --- a/src/discordcommandhandler.ts +++ b/src/discordcommandhandler.ts @@ -18,7 +18,8 @@ import { DiscordBot } from "./bot"; import * as Discord from "better-discord.js"; import { Util, ICommandActions, ICommandParameters, CommandPermissonCheck } from "./util"; import { Log } from "./log"; -import { Appservice } from "matrix-bot-sdk"; +import { Appservice, Presence } from "matrix-bot-sdk"; +import { DiscordBridgeConfig } from './config'; const log = new Log("DiscordCommandHandler"); @@ -26,6 +27,7 @@ export class DiscordCommandHandler { constructor( private bridge: Appservice, private discord: DiscordBot, + private config: DiscordBridgeConfig, ) { } public async Process(msg: Discord.Message) { @@ -50,7 +52,7 @@ export class DiscordCommandHandler { permission: "MANAGE_WEBHOOKS", run: async () => { if (await this.discord.Provisioner.MarkApproved(chan, discordMember, true)) { - return "Thanks for your response! The matrix bridge has been approved."; + return "Thanks for your response! The Matrix bridge has been approved."; } else { return "Thanks for your response, however" + " it has arrived after the deadline - sorry!"; @@ -58,7 +60,7 @@ export class DiscordCommandHandler { }, }, ban: { - description: "Bans a user on the matrix side", + description: "Bans a user on the Matrix side", params: ["name"], permission: "BAN_MEMBERS", run: this.ModerationActionGenerator(chan, "ban"), @@ -69,7 +71,7 @@ export class DiscordCommandHandler { permission: "MANAGE_WEBHOOKS", run: async () => { if (await this.discord.Provisioner.MarkApproved(chan, discordMember, false)) { - return "Thanks for your response! The matrix bridge has been declined."; + return "Thanks for your response! The Matrix bridge has been declined."; } else { return "Thanks for your response, however" + " it has arrived after the deadline - sorry!"; @@ -77,28 +79,34 @@ export class DiscordCommandHandler { }, }, kick: { - description: "Kicks a user on the matrix side", + description: "Kicks a user on the Matrix side", params: ["name"], permission: "KICK_MEMBERS", run: this.ModerationActionGenerator(chan, "kick"), }, unban: { - description: "Unbans a user on the matrix side", + description: "Unbans a user on the Matrix side", params: ["name"], permission: "BAN_MEMBERS", run: this.ModerationActionGenerator(chan, "unban"), }, unbridge: { - description: "Unbridge matrix rooms from this channel", + description: "Unbridge Matrix rooms from this channel", params: [], permission: ["MANAGE_WEBHOOKS", "MANAGE_CHANNELS"], run: async () => this.UnbridgeChannel(chan), }, + listusers: { + description: "List users on the Matrix side of the bridge", + params: [], + permission: [], + run: async () => this.ListMatrixMembers(chan) + } }; const parameters: ICommandParameters = { name: { - description: "The display name or mxid of a matrix user", + description: "The display name or mxid of a Matrix user", get: async (name) => { const channelMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(msg.channel); const mxUserId = await Util.GetMxidFromName(intent, name, channelMxids); @@ -156,7 +164,7 @@ export class DiscordCommandHandler { return "This channel has been unbridged"; } catch (err) { if (err.message === "Channel is not bridged") { - return "This channel is not bridged to a plumbed matrix room"; + return "This channel is not bridged to a plumbed Matrix room"; } log.error("Error while unbridging room " + channel.id); log.error(err); @@ -164,4 +172,98 @@ export class DiscordCommandHandler { "Please try again later or contact the bridge operator."; } } + + private async ListMatrixMembers(channel: Discord.TextChannel): Promise { + const chanMxids = await this.discord.ChannelSyncroniser.GetRoomIdsFromChannel(channel); + const members: { + mxid: string; + displayName?: string; + presence?: Presence; + }[] = []; + const errorMessages: string[] = []; + + await Promise.all(chanMxids.map(async (chanMxid) => { + const { underlyingClient } = this.bridge.botIntent; + + try { + const memberProfiles = await underlyingClient.getJoinedRoomMembersWithProfiles(chanMxid); + const userProfiles = Object.keys(memberProfiles) + .filter((mxid) => !this.bridge.isNamespacedUser(mxid)) + .map((mxid) => { + return { + mxid, + displayName: memberProfiles[mxid].display_name + }; + }); + + members.push(...userProfiles); + } catch (e) { + errorMessages.push(`Couldn't get members from ${chanMxid}`); + } + })); + + if (errorMessages.length) { + const errorMessage = errorMessages.join('\n'); + throw Error(errorMessage); + } + + if (!this.config.bridge.disablePresence) { + await Promise.all(members.map(async (member) => { + const { botClient } = this.bridge; + const presence = await botClient.getPresenceStatusFor(member.mxid); + member.presence = presence; + return presence; + })); + } + + const length = members.length; + const formatter = new Intl.NumberFormat('en-US'); + const formattedTotalMembers = formatter.format(length); + let userCount: string; + + if (length === 1) { + userCount = `is **1** user`; + } else { + userCount = `are **${formattedTotalMembers}** users`; + } + + const userCountMessage = `There ${userCount} on the Matrix side.`; + + if (length === 0) { + return userCountMessage; + } + + const disclaimer = `Matrix users in ${channel.toString()} may not necessarily be in the other bridged channels in the server.`; + /** Reserve characters for the worst-case "and x others…" line at the end if there are too many members. */ + const reservedChars = `\n_and ${formattedTotalMembers} others…_`.length; + + let message = `${userCountMessage} ${disclaimer}\n`; + + for (let i = 0; i < length; i++) { + const member = members[i]; + const hasDisplayName = !!member.displayName; + let line = "• "; + + if (hasDisplayName) { + line += `${member.displayName} (${member.mxid})`; + } else { + line += member.mxid; + } + + if (member.presence) { + const { state } = member.presence; + line += ` - ${state.charAt(0).toUpperCase() + state.slice(1)}`; + } + + if (2000 - message.length - reservedChars < line.length) { + const remaining = length - i; + message += `\n_and ${formatter.format(remaining)} others…_`; + return message; + } + + message += `\n${line}`; + } + + return message; + } }