@@ -18,25 +18,29 @@ import { DiscordBot } from "./bot";
1818import * as Discord from "better-discord.js" ;
1919import { Util , ICommandActions , ICommandParameters , CommandPermissonCheck } from "./util" ;
2020import { Log } from "./log" ;
21- import { Appservice } from "matrix-bot-sdk" ;
21+ import { Appservice , Presence } from "matrix-bot-sdk" ;
22+ import { DiscordBridgeConfig } from './config' ;
2223
2324const log = new Log ( "DiscordCommandHandler" ) ;
2425
2526export class DiscordCommandHandler {
2627 constructor (
2728 private bridge : Appservice ,
2829 private discord : DiscordBot ,
30+ private config : DiscordBridgeConfig ,
2931 ) { }
3032
33+ /**
34+ * @param msg Message to process.
35+ * @returns The message the bot replied with.
36+ */
3137 public async Process ( msg : Discord . Message ) {
3238 const chan = msg . channel as Discord . TextChannel ;
3339 if ( ! chan . guild ) {
34- await msg . channel . send ( "**ERROR:** only available for guild channels" ) ;
35- return ;
40+ return await msg . channel . send ( "**ERROR:** only available for guild channels" ) ;
3641 }
3742 if ( ! msg . member ) {
38- await msg . channel . send ( "**ERROR:** could not determine message member" ) ;
39- return ;
43+ return await msg . channel . send ( "**ERROR:** could not determine message member" ) ;
4044 }
4145
4246 const discordMember = msg . member ;
@@ -50,15 +54,15 @@ export class DiscordCommandHandler {
5054 permission : "MANAGE_WEBHOOKS" ,
5155 run : async ( ) => {
5256 if ( await this . discord . Provisioner . MarkApproved ( chan , discordMember , true ) ) {
53- return "Thanks for your response! The matrix bridge has been approved." ;
57+ return "Thanks for your response! The Matrix bridge has been approved." ;
5458 } else {
5559 return "Thanks for your response, however" +
5660 " it has arrived after the deadline - sorry!" ;
5761 }
5862 } ,
5963 } ,
6064 ban : {
61- description : "Bans a user on the matrix side" ,
65+ description : "Bans a user on the Matrix side" ,
6266 params : [ "name" ] ,
6367 permission : "BAN_MEMBERS" ,
6468 run : this . ModerationActionGenerator ( chan , "ban" ) ,
@@ -69,36 +73,42 @@ export class DiscordCommandHandler {
6973 permission : "MANAGE_WEBHOOKS" ,
7074 run : async ( ) => {
7175 if ( await this . discord . Provisioner . MarkApproved ( chan , discordMember , false ) ) {
72- return "Thanks for your response! The matrix bridge has been declined." ;
76+ return "Thanks for your response! The Matrix bridge has been declined." ;
7377 } else {
7478 return "Thanks for your response, however" +
7579 " it has arrived after the deadline - sorry!" ;
7680 }
7781 } ,
7882 } ,
7983 kick : {
80- description : "Kicks a user on the matrix side" ,
84+ description : "Kicks a user on the Matrix side" ,
8185 params : [ "name" ] ,
8286 permission : "KICK_MEMBERS" ,
8387 run : this . ModerationActionGenerator ( chan , "kick" ) ,
8488 } ,
8589 unban : {
86- description : "Unbans a user on the matrix side" ,
90+ description : "Unbans a user on the Matrix side" ,
8791 params : [ "name" ] ,
8892 permission : "BAN_MEMBERS" ,
8993 run : this . ModerationActionGenerator ( chan , "unban" ) ,
9094 } ,
9195 unbridge : {
92- description : "Unbridge matrix rooms from this channel" ,
96+ description : "Unbridge Matrix rooms from this channel" ,
9397 params : [ ] ,
9498 permission : [ "MANAGE_WEBHOOKS" , "MANAGE_CHANNELS" ] ,
9599 run : async ( ) => this . UnbridgeChannel ( chan ) ,
96100 } ,
101+ listusers : {
102+ description : "List users on the Matrix side of the bridge" ,
103+ params : [ ] ,
104+ permission : [ ] ,
105+ run : async ( ) => this . ListMatrixMembers ( chan )
106+ }
97107 } ;
98108
99109 const parameters : ICommandParameters = {
100110 name : {
101- description : "The display name or mxid of a matrix user" ,
111+ description : "The display name or mxid of a Matrix user" ,
102112 get : async ( name ) => {
103113 const channelMxids = await this . discord . ChannelSyncroniser . GetRoomIdsFromChannel ( msg . channel ) ;
104114 const mxUserId = await Util . GetMxidFromName ( intent , name , channelMxids ) ;
@@ -115,7 +125,7 @@ export class DiscordCommandHandler {
115125 } ;
116126
117127 const reply = await Util . ParseCommand ( "!matrix" , msg . content , actions , parameters , permissionCheck ) ;
118- await msg . channel . send ( reply ) ;
128+ return await msg . channel . send ( reply ) ;
119129 }
120130
121131 private ModerationActionGenerator ( discordChannel : Discord . TextChannel , funcKey : "kick" | "ban" | "unban" ) {
@@ -156,12 +166,150 @@ export class DiscordCommandHandler {
156166 return "This channel has been unbridged" ;
157167 } catch ( err ) {
158168 if ( err . message === "Channel is not bridged" ) {
159- return "This channel is not bridged to a plumbed matrix room" ;
169+ return "This channel is not bridged to a plumbed Matrix room" ;
160170 }
161171 log . error ( "Error while unbridging room " + channel . id ) ;
162172 log . error ( err ) ;
163173 return "There was an error unbridging this room. " +
164174 "Please try again later or contact the bridge operator." ;
165175 }
166176 }
177+
178+ private async ListMatrixMembers ( channel : Discord . TextChannel ) : Promise < string > {
179+ const chanMxids = await this . discord . ChannelSyncroniser . GetRoomIdsFromChannel ( channel ) ;
180+ const members : {
181+ mxid : string ;
182+ displayName ?: string ;
183+ presence ?: Presence ;
184+ } [ ] = [ ] ;
185+ const errorMessages : string [ ] = [ ] ;
186+
187+ await Promise . all ( chanMxids . map ( async ( chanMxid ) => {
188+ const { underlyingClient } = this . bridge . botIntent ;
189+
190+ try {
191+ const memberProfiles = await underlyingClient . getJoinedRoomMembersWithProfiles ( chanMxid ) ;
192+ const userProfiles = Object . keys ( memberProfiles )
193+ . filter ( ( mxid ) => ! this . bridge . isNamespacedUser ( mxid ) )
194+ . map ( ( mxid ) => {
195+ return {
196+ mxid,
197+ displayName : memberProfiles [ mxid ] . display_name
198+ } ;
199+ } ) ;
200+
201+ members . push ( ...userProfiles ) ;
202+ } catch ( e ) {
203+ errorMessages . push ( `Couldn't get members from ${ chanMxid } ` ) ;
204+ }
205+ } ) ) ;
206+
207+ if ( errorMessages . length ) {
208+ const errorMessage = errorMessages . join ( '\n' ) ;
209+ throw Error ( errorMessage ) ;
210+ }
211+
212+ if ( ! this . config . bridge . disablePresence ) {
213+ await Promise . all ( members . map ( async ( member ) => {
214+ const { botClient } = this . bridge ;
215+ try {
216+ const presence = await botClient . getPresenceStatusFor ( member . mxid ) ;
217+ member . presence = presence ;
218+ return presence ;
219+ } catch ( e ) {
220+ errorMessages . push ( `Couldn't get presence for ${ member . mxid } ` ) ;
221+ }
222+ } ) ) ;
223+ }
224+
225+ if ( errorMessages . length ) {
226+ const errorMessage = errorMessages . join ( '\n' ) ;
227+ throw Error ( errorMessage ) ;
228+ }
229+
230+ const length = members . length ;
231+ const formatter = new Intl . NumberFormat ( 'en-US' ) ;
232+ const formattedTotalMembers = formatter . format ( length ) ;
233+ let userCount : string ;
234+
235+ if ( length === 1 ) {
236+ userCount = `is **1** user` ;
237+ } else {
238+ userCount = `are **${ formattedTotalMembers } ** users` ;
239+ }
240+
241+ const userCountMessage = `There ${ userCount } on the Matrix side.` ;
242+
243+ if ( length === 0 ) {
244+ return userCountMessage ;
245+ }
246+
247+ members . sort ( ( a , b ) => {
248+ const aPresenceState = a . presence ?. state ?? "unknown" ;
249+ const bPresenceState = b . presence ?. state ?? "unknown" ;
250+
251+ if ( aPresenceState === bPresenceState ) {
252+ const aDisplayName = a . displayName ;
253+ const bDisplayName = b . displayName ;
254+
255+ if ( aDisplayName === bDisplayName ) {
256+ return a . mxid . localeCompare ( b . mxid ) ;
257+ }
258+
259+ if ( ! aDisplayName ) {
260+ return 1 ;
261+ }
262+
263+ if ( ! bDisplayName ) {
264+ return - 1 ;
265+ }
266+
267+ return aDisplayName . localeCompare ( bDisplayName , 'en' , { sensitivity : "base" } ) ;
268+ }
269+
270+ const presenseOrdinal = {
271+ "online" : 0 ,
272+ "unavailable" : 1 ,
273+ "offline" : 2 ,
274+ "unknown" : 3
275+ } ;
276+
277+ return presenseOrdinal [ aPresenceState ] - presenseOrdinal [ bPresenceState ] ;
278+ } ) ;
279+
280+ const disclaimer = `Matrix users in ${ channel . toString ( ) } may not necessarily be in the other bridged channels in the server.` ;
281+ /** Reserve characters for the worst-case "and x others…" line at the end if there are too many members. */
282+ const reservedChars = `\n_and ${ formattedTotalMembers } others…_` . length ;
283+
284+ let message = `${ userCountMessage } ${ disclaimer } \n` ;
285+
286+ for ( let i = 0 ; i < length ; i ++ ) {
287+ const member = members [ i ] ;
288+ const hasDisplayName = ! ! member . displayName ;
289+ let line = "• " ;
290+
291+ if ( hasDisplayName ) {
292+ line += `${ member . displayName } (${ member . mxid } )` ;
293+ } else {
294+ line += member . mxid ;
295+ }
296+
297+ if ( ! this . config . bridge . disablePresence ) {
298+ const state = member . presence ?. state ?? "unknown" ;
299+ // Use Discord terminology for Away
300+ const stateDisplay = ( state === "unavailable" ) ? "idle" : state ;
301+ line += ` - ${ stateDisplay . charAt ( 0 ) . toUpperCase ( ) + stateDisplay . slice ( 1 ) } ` ;
302+ }
303+
304+ if ( 2000 - message . length - reservedChars < line . length ) {
305+ const remaining = length - i ;
306+ message += `\n_and ${ formatter . format ( remaining ) } others…_` ;
307+ return message ;
308+ }
309+
310+ message += `\n${ line } ` ;
311+ }
312+
313+ return message ;
314+ }
167315}
0 commit comments