@@ -1,35 +1,128 @@
import { MatrixClient , Room } from 'matrix-js-sdk' ;
import { Direction , IContextResponse , MatrixClient , Method , Room , RoomMember } from 'matrix-js-sdk' ;
import { RoomServerAclEventContent } from 'matrix-js-sdk/lib/types' ;
import { useMemo } from 'react' ;
import { getDMRoomFor , isRoomAlias , isRoomId , isUserId } from '../utils/matrix' ;
import {
getDMRoomFor ,
isRoomAlias ,
isRoomId ,
isServerName ,
isUserId ,
rateLimitedActions ,
} from '../utils/matrix' ;
import { hasDevices } from '../../util/matrixUtil' ;
import * as roomActions from '../../client/action/room' ;
import { useRoomNavigate } from './useRoomNavigate' ;
import { Membership , StateEvent } from '../../types/matrix/room' ;
import { getStateEvent } from '../utils/room' ;
import { splitWithSpace } from '../utils/common' ;
export const SHRUG = '¯\\_(ツ)_/¯' ;
export const TABLEFLIP = '(╯°□°)╯︵ ┻━┻' ;
export const UNFLIP = '┬─┬ノ( º_ºノ )' ;
export function parseUsersAndReason ( payload : string ) : {
users : string [ ] ;
reason? : string ;
} {
let reason : string | undefined ;
let ids : string = payload ;
const FLAG_PAT = '(?:^|\\s)-(\\w+)\\b' ;
const FLAG_REG = new RegExp ( FLAG_PAT ) ;
const FLAG_REG_G = new RegExp ( FLAG_PAT , 'g' ) ;
const reasonMatch = payload . match ( /\s-r\s/ ) ;
if ( reasonMatch ) {
ids = payload . slice ( 0 , reasonMatch . index ) ;
reason = payload . slice ( ( reasonMatch . index ? ? 0 ) + reasonMatch [ 0 ] . length ) ;
if ( reason . trim ( ) === '' ) reason = undefined ;
export const splitPayloadContentAndFlags = ( payload : string ) : [ string , string | undefined ] = > {
const flagMatch = payload . match ( FLAG_REG ) ;
if ( ! flagMatch ) {
return [ payload , undefined ] ;
}
const rawIds = ids . sp lit ( ' ' ) ;
const user s = rawIds . filter ( ( id ) = > isUserId ( id ) ) ;
return {
users ,
reason ,
const content = payload . slice ( 0 , flagMatch . index ) ;
const flag s = payload . slice ( flagMatch . index ) ;
return [ content , flags ] ;
} ;
export const parseFlags = ( flags : string | undefined ) : Record < string , string | undefined > = > {
const result : Record < string , string > = { } ;
if ( ! flags ) return result ;
const matches : { key : string ; index : number ; match : string } [ ] = [ ] ;
for ( let match = FLAG_REG_G . exec ( flags ) ; match !== null ; match = FLAG_REG_G . exec ( flags ) ) {
matches . push ( { key : match [ 1 ] , index : match.index , match : match [ 0 ] } ) ;
}
for ( let i = 0 ; i < matches . length ; i += 1 ) {
const { key , match } = matches [ i ] ;
const start = matches [ i ] . index + match . length ;
const end = i + 1 < matches . length ? matches [ i + 1 ] . index : flags.length ;
const value = flags . slice ( start , end ) . trim ( ) ;
result [ key ] = value ;
}
return result ;
} ;
export const parseUsers = ( payload : string ) : string [ ] = > {
const users : string [ ] = [ ] ;
splitWithSpace ( payload ) . forEach ( ( item ) = > {
if ( isUserId ( item ) ) {
users . push ( item ) ;
}
} ) ;
return users ;
} ;
export const parseServers = ( payload : string ) : string [ ] = > {
const servers : string [ ] = [ ] ;
splitWithSpace ( payload ) . forEach ( ( item ) = > {
if ( isServerName ( item ) ) {
servers . push ( item ) ;
}
} ) ;
return servers ;
} ;
const getServerMembers = ( room : Room , server : string ) : RoomMember [ ] = > {
const members : RoomMember [ ] = room
. getMembers ( )
. filter ( ( member ) = > member . userId . endsWith ( ` : ${ server } ` ) ) ;
return members ;
} ;
export const parseTimestampFlag = ( input : string ) : number | undefined = > {
const match = input . match ( /^(\d+(?:\.\d+)?)([dhms])$/ ) ; // supports floats like 1.5d
if ( ! match ) {
return undefined ;
}
const value = parseFloat ( match [ 1 ] ) ; // supports decimal values
const unit = match [ 2 ] ;
const now = Date . now ( ) ; // in milliseconds
let delta = 0 ;
switch ( unit ) {
case 'd' :
delta = value * 24 * 60 * 60 * 1000 ;
break ;
case 'h' :
delta = value * 60 * 60 * 1000 ;
break ;
case 'm' :
delta = value * 60 * 1000 ;
break ;
case 's' :
delta = value * 1000 ;
break ;
default :
return undefined ;
}
const timestamp = now - delta ;
return timestamp ;
} ;
export type CommandExe = ( payload : string ) = > Promise < void > ;
export enum Command {
@@ -52,6 +145,8 @@ export enum Command {
ConvertToRoom = 'converttoroom' ,
TableFlip = 'tableflip' ,
UnFlip = 'unflip' ,
Delete = 'delete' ,
Acl = 'acl' ,
}
export type CommandContent = {
@@ -96,7 +191,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
name : Command.StartDm ,
description : 'Start direct message with user. Example: /startdm userId1' ,
exe : async ( payload ) = > {
const rawIds = payload . split ( ' ' ) ;
const rawIds = splitWithSpace ( payload ) ;
const userIds = rawIds . filter ( ( id ) = > isUserId ( id ) && id !== mx . getUserId ( ) ) ;
if ( userIds . length === 0 ) return ;
if ( userIds . length === 1 ) {
@@ -106,7 +201,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
return ;
}
}
const devices = await Promise . all ( userIds . map ( uid = > hasDevices ( mx , uid ) ) ) ;
const devices = await Promise . all ( userIds . map ( ( uid ) = > hasDevices ( mx , uid ) ) ) ;
const isEncrypt = devices . every ( ( hasDevice ) = > hasDevice ) ;
const result = await roomActions . createDM ( mx , userIds , isEncrypt ) ;
navigateRoom ( result . room_id ) ;
@@ -116,7 +211,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
name : Command.Join ,
description : 'Join room with address. Example: /join address1 address2' ,
exe : async ( payload ) = > {
const rawIds = payload . split ( ' ' ) ;
const rawIds = splitWithSpace ( payload ) ;
const roomIds = rawIds . filter (
( idOrAlias ) = > isRoomId ( idOrAlias ) || isRoomAlias ( idOrAlias )
) ;
@@ -131,7 +226,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
mx . leave ( room . roomId ) ;
return ;
}
const rawIds = payload . split ( ' ' ) ;
const rawIds = splitWithSpace ( payload ) ;
const roomIds = rawIds . filter ( ( id ) = > isRoomId ( id ) ) ;
roomIds . map ( ( id ) = > mx . leave ( id ) ) ;
} ,
@@ -140,7 +235,10 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
name : Command.Invite ,
description : 'Invite user to room. Example: /invite userId1 userId2 [-r reason]' ,
exe : async ( payload ) = > {
const { users , reason } = parseUsersAndReason ( payload ) ;
const [ content , flags ] = splitPayloadContentAndFlags ( payload ) ;
const users = parseUsers ( content ) ;
const flagToContent = parseFlags ( flags ) ;
const reason = flagToContent . r ;
users . map ( ( id ) = > mx . invite ( room . roomId , id , reason ) ) ;
} ,
} ,
@@ -148,7 +246,10 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
name : Command.DisInvite ,
description : 'Disinvite user to room. Example: /disinvite userId1 userId2 [-r reason]' ,
exe : async ( payload ) = > {
const { users , reason } = parseUsersAndReason ( payload ) ;
const [ content , flags ] = splitPayloadContentAndFlags ( payload ) ;
const users = parseUsers ( content ) ;
const flagToContent = parseFlags ( flags ) ;
const reason = flagToContent . r ;
users . map ( ( id ) = > mx . kick ( room . roomId , id , reason ) ) ;
} ,
} ,
@@ -156,23 +257,53 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
name : Command.Kick ,
description : 'Kick user from room. Example: /kick userId1 userId2 [-r reason]' ,
exe : async ( payload ) = > {
const { users , reason } = parseUsersAndReason ( payload ) ;
users . map ( ( id ) = > mx . kick ( room . roomId , id , reason ) ) ;
const [ content , flags ] = splitPayloadContentAndFlags ( payload ) ;
const users = parseUsers ( content ) ;
const servers = parseServers ( content ) ;
const flagToContent = parseFlags ( flags ) ;
const reason = flagToContent . r ;
const serverMembers = servers ? . flatMap ( ( server ) = > getServerMembers ( room , server ) ) ;
const serverUsers = serverMembers
? . filter ( ( m ) = > m . membership !== Membership . Ban )
. map ( ( m ) = > m . userId ) ;
if ( Array . isArray ( serverUsers ) ) {
serverUsers . forEach ( ( user ) = > {
if ( ! users . includes ( user ) ) users . push ( user ) ;
} ) ;
}
rateLimitedActions ( users , ( id ) = > mx . kick ( room . roomId , id , reason ) ) ;
} ,
} ,
[ Command . Ban ] : {
name : Command.Ban ,
description : 'Ban user from room. Example: /ban userId1 userId2 [-r reason]' ,
description : 'Ban user from room. Example: /ban userId1 userId2 servername [-r reason]' ,
exe : async ( payload ) = > {
const { users , reason } = parseUsersAndReason ( payload ) ;
users . map ( ( id ) = > mx . ban ( room . roomId , id , reason ) ) ;
const [ content , flags ] = splitPayloadContentAndFlags ( payload ) ;
const users = parseUsers ( content ) ;
const servers = parseServers ( content ) ;
const flagToContent = parseFlags ( flags ) ;
const reason = flagToContent . r ;
const serverMembers = servers ? . flatMap ( ( server ) = > getServerMembers ( room , server ) ) ;
const serverUsers = serverMembers ? . map ( ( m ) = > m . userId ) ;
if ( Array . isArray ( serverUsers ) ) {
serverUsers . forEach ( ( user ) = > {
if ( ! users . includes ( user ) ) users . push ( user ) ;
} ) ;
}
rateLimitedActions ( users , ( id ) = > mx . ban ( room . roomId , id , reason ) ) ;
} ,
} ,
[ Command . UnBan ] : {
name : Command.UnBan ,
description : 'Unban user from room. Example: /unban userId1 userId2' ,
exe : async ( payload ) = > {
const rawIds = payload . split ( ' ' ) ;
const rawIds = splitWithSpace ( payload ) ;
const users = rawIds . filter ( ( id ) = > isUserId ( id ) ) ;
users . map ( ( id ) = > mx . unban ( room . roomId , id ) ) ;
} ,
@@ -181,7 +312,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
name : Command.Ignore ,
description : 'Ignore user. Example: /ignore userId1 userId2' ,
exe : async ( payload ) = > {
const rawIds = payload . split ( ' ' ) ;
const rawIds = splitWithSpace ( payload ) ;
const userIds = rawIds . filter ( ( id ) = > isUserId ( id ) ) ;
if ( userIds . length > 0 ) roomActions . ignore ( mx , userIds ) ;
} ,
@@ -190,7 +321,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
name : Command.UnIgnore ,
description : 'Unignore user. Example: /unignore userId1 userId2' ,
exe : async ( payload ) = > {
const rawIds = payload . split ( ' ' ) ;
const rawIds = splitWithSpace ( payload ) ;
const userIds = rawIds . filter ( ( id ) = > isUserId ( id ) ) ;
if ( userIds . length > 0 ) roomActions . unignore ( mx , userIds ) ;
} ,
@@ -227,6 +358,124 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
roomActions . convertToRoom ( mx , room . roomId ) ;
} ,
} ,
[ Command . Delete ] : {
name : Command.Delete ,
description :
'Delete messages from users. Example: /delete userId1 servername -past 1d|2h|5m|30s [-t m.room.message] [-r spam]' ,
exe : async ( payload ) = > {
const [ content , flags ] = splitPayloadContentAndFlags ( payload ) ;
const users = parseUsers ( content ) ;
const servers = parseServers ( content ) ;
const flagToContent = parseFlags ( flags ) ;
const reason = flagToContent . r ;
const pastContent = flagToContent . past ? ? '' ;
const msgTypeContent = flagToContent . t ;
const messageTypes : string [ ] = msgTypeContent ? splitWithSpace ( msgTypeContent ) : [ ] ;
const ts = parseTimestampFlag ( pastContent ) ;
if ( ! ts ) return ;
const serverMembers = servers ? . flatMap ( ( server ) = > getServerMembers ( room , server ) ) ;
const serverUsers = serverMembers ? . map ( ( m ) = > m . userId ) ;
if ( Array . isArray ( serverUsers ) ) {
serverUsers . forEach ( ( user ) = > {
if ( ! users . includes ( user ) ) users . push ( user ) ;
} ) ;
}
const result = await mx . timestampToEvent ( room . roomId , ts , Direction . Forward ) ;
const startEventId = result . event_id ;
const path = ` /rooms/ ${ encodeURIComponent ( room . roomId ) } /context/ ${ encodeURIComponent (
startEventId
) } ` ;
const eventContext = await mx . http . authedRequest < IContextResponse > ( Method . Get , path , {
limit : 0 ,
} ) ;
let token : string | undefined = eventContext . start ;
while ( token ) {
// eslint-disable-next-line no-await-in-loop
const response = await mx . createMessagesRequest (
room . roomId ,
token ,
20 ,
Direction . Forward ,
undefined
) ;
const { end , chunk } = response ;
// remove until the latest event;
token = end ;
const eventsToDelete = chunk . filter (
( roomEvent ) = >
( messageTypes . length > 0 ? messageTypes . includes ( roomEvent . type ) : true ) &&
users . includes ( roomEvent . sender ) &&
roomEvent . unsigned ? . redacted_because === undefined
) ;
const eventIds = eventsToDelete . map ( ( roomEvent ) = > roomEvent . event_id ) ;
// eslint-disable-next-line no-await-in-loop
await rateLimitedActions ( eventIds , ( eventId ) = >
mx . redactEvent ( room . roomId , eventId , undefined , { reason } )
) ;
}
} ,
} ,
[ Command . Acl ] : {
name : Command.Acl ,
description :
'Manage server access control list. Example /acl [-a servername1] [-d servername2] [-ra servername1] [-rd servername2]' ,
exe : async ( payload ) = > {
const [ , flags ] = splitPayloadContentAndFlags ( payload ) ;
const flagToContent = parseFlags ( flags ) ;
const allowFlag = flagToContent . a ;
const denyFlag = flagToContent . d ;
const removeAllowFlag = flagToContent . ra ;
const removeDenyFlag = flagToContent . rd ;
const allowList = allowFlag ? splitWithSpace ( allowFlag ) : [ ] ;
const denyList = denyFlag ? splitWithSpace ( denyFlag ) : [ ] ;
const removeAllowList = removeAllowFlag ? splitWithSpace ( removeAllowFlag ) : [ ] ;
const removeDenyList = removeDenyFlag ? splitWithSpace ( removeDenyFlag ) : [ ] ;
const serverAcl = getStateEvent (
room ,
StateEvent . RoomServerAcl
) ? . getContent < RoomServerAclEventContent > ( ) ;
const aclContent : RoomServerAclEventContent = {
allow : serverAcl?.allow ? [ . . . serverAcl . allow ] : [ ] ,
allow_ip_literals : serverAcl?.allow_ip_literals ,
deny : serverAcl?.deny ? [ . . . serverAcl . deny ] : [ ] ,
} ;
allowList . forEach ( ( servername ) = > {
if ( ! Array . isArray ( aclContent . allow ) || aclContent . allow . includes ( servername ) ) return ;
aclContent . allow . push ( servername ) ;
} ) ;
denyList . forEach ( ( servername ) = > {
if ( ! Array . isArray ( aclContent . deny ) || aclContent . deny . includes ( servername ) ) return ;
aclContent . deny . push ( servername ) ;
} ) ;
aclContent . allow = aclContent . allow ? . filter (
( servername ) = > ! removeAllowList . includes ( servername )
) ;
aclContent . deny = aclContent . deny ? . filter (
( servername ) = > ! removeDenyList . includes ( servername )
) ;
aclContent . allow ? . sort ( ) ;
aclContent . deny ? . sort ( ) ;
await mx . sendStateEvent ( room . roomId , StateEvent . RoomServerAcl as any , aclContent ) ;
} ,
} ,
} ) ,
[ mx , room , navigateRoom ]
) ;