Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add listusers command #854

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
159 changes: 145 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,133 @@ 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) => ({ mxid, displayName: memberProfiles[mxid].display_name }));

members.push(...userProfiles);
} catch (e) {
errorMessages.push(`Couldn't get members from ${chanMxid}`);
}
}));

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

if (!this.config.bridge.disablePresence) {

await Promise.all(members.map(async (member) => {
try {
const presence = await this.bridge.botClient.getPresenceStatusFor(member.mxid);
member.presence = presence;
} catch (e) {
errorMessages.push(`Couldn't get presence for ${member.mxid}`);
}
}));
}

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

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

/** 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} Matrix users in ${channel.toString()} may not necessarily be in the other bridged channels in the server.\n`;

for (let i = 0; i < length; i++) {
const { mxid, displayName, presence } = members[i];
let line = "• ";
line += (displayName) ? `${displayName} (${mxid})` : mxid;

if (!this.config.bridge.disablePresence) {
const state = 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;
return message + `\n_and ${formatter.format(remaining)} others…_`;
}

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