Skip to content

Commit

Permalink
feat: add listusers command
Browse files Browse the repository at this point in the history
Signed-off-by: Seth Falco <[email protected]>
  • Loading branch information
SethFalco committed Oct 9, 2022
1 parent ac5180e commit bd15968
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 18 deletions.
1 change: 1 addition & 0 deletions changelog.d/854.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add listusers command to Discord bot to list the users on the Matrix side. Thanks to @SethFalco!
2 changes: 1 addition & 1 deletion src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down
176 changes: 162 additions & 14 deletions src/discordcommandhandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,29 @@ 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");

export class DiscordCommandHandler {
constructor(
private bridge: Appservice,
private discord: DiscordBot,
private config: DiscordBridgeConfig,
) { }

/**
* @param msg Message to process.
* @returns The message the bot replied with.
*/
public async Process(msg: Discord.Message) {
const chan = msg.channel as Discord.TextChannel;
if (!chan.guild) {
await msg.channel.send("**ERROR:** only available for guild channels");
return;
return await msg.channel.send("**ERROR:** only available for guild channels");
}
if (!msg.member) {
await msg.channel.send("**ERROR:** could not determine message member");
return;
return await msg.channel.send("**ERROR:** could not determine message member");
}

const discordMember = msg.member;
Expand All @@ -50,15 +54,15 @@ 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!";
}
},
},
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"),
Expand All @@ -69,36 +73,42 @@ 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!";
}
},
},
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);
Expand All @@ -115,7 +125,7 @@ export class DiscordCommandHandler {
};

const reply = await Util.ParseCommand("!matrix", msg.content, actions, parameters, permissionCheck);
await msg.channel.send(reply);
return await msg.channel.send(reply);
}

private ModerationActionGenerator(discordChannel: Discord.TextChannel, funcKey: "kick"|"ban"|"unban") {
Expand Down Expand Up @@ -156,12 +166,150 @@ 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);
return "There was an error unbridging this room. " +
"Please try again later or contact the bridge operator.";
}
}

private async ListMatrixMembers(channel: Discord.TextChannel): Promise<string> {
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;
try {
const presence = await botClient.getPresenceStatusFor(member.mxid);
member.presence = presence;
return presence;
} catch (e) {
errorMessages.push(`Couldn't get presence for ${member.mxid}`);
}
}));
}

if (errorMessages.length) {
const errorMessage = errorMessages.join('\n');
throw Error(errorMessage);
}

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;
}

members.sort((a, b) => {
const aPresenceState = a.presence?.state ?? "unknown";
const bPresenceState = b.presence?.state ?? "unknown";

if (aPresenceState === bPresenceState) {
const aDisplayName = a.displayName;
const bDisplayName = b.displayName;

if (aDisplayName === bDisplayName) {
return a.mxid.localeCompare(b.mxid);
}

if (!aDisplayName) {
return 1;
}

if (!bDisplayName) {
return -1;
}

return aDisplayName.localeCompare(bDisplayName, 'en', { sensitivity: "base" });
}

const presenseOrdinal = {
"online": 0,
"unavailable": 1,
"offline": 2,
"unknown": 3
};

return presenseOrdinal[aPresenceState] - presenseOrdinal[bPresenceState];
});

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 (!this.config.bridge.disablePresence) {
const state = member.presence?.state ?? "unknown";
// Use Discord terminology for Away
const stateDisplay = (state === "unavailable") ? "idle" : state;
line += ` - ${stateDisplay.charAt(0).toUpperCase() + stateDisplay.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;
}
}
7 changes: 7 additions & 0 deletions test/mocks/appservicemock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ class MatrixClientMock extends AppserviceMockBase {
super();
}

public getPresenceStatusFor(userId: string) {
this.funcCalled("getPresenceStatusFor", userId);
return {
state: "online"
}
}

public banUser(roomId: string, userId: string) {
this.funcCalled("banUser", roomId, userId);
}
Expand Down
4 changes: 4 additions & 0 deletions test/mocks/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export class MockChannel {
public permissionsFor(member: MockMember) {
return new Permissions(Permissions.FLAGS.MANAGE_WEBHOOKS as PermissionResolvable);
}

public toString(): string {
return `<#${this.id}>`;
}
}

export class MockTextChannel extends TextChannel {
Expand Down
4 changes: 4 additions & 0 deletions test/mocks/guild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ export class MockGuild {
public _mockAddMember(member: MockMember) {
this.members.cache.set(member.id, member);
}

public toString(): string {
return `<#${this.id}>`;
}
}
1 change: 0 additions & 1 deletion test/structures/test_lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ limitations under the License.

import { expect } from "chai";
import { Lock } from "../../src/structures/lock";
import { Util } from "../../src/util";

const LOCKTIMEOUT = 300;

Expand Down
Loading

0 comments on commit bd15968

Please sign in to comment.