mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	Update commands (#2325)
* kick-ban all members by servername * Add command for deleting multiple messages * remove console logs and improve ban command description * improve commands description * add server acl command * fix code highlight not working after editing in dev tools
This commit is contained in:
		
							parent
							
								
									13f1d53191
								
							
						
					
					
						commit
						87e97eab88
					
				
					 5 changed files with 339 additions and 45 deletions
				
			
		| 
						 | 
				
			
			@ -24,7 +24,7 @@ export const TextViewerContent = forwardRef<HTMLPreElement, TextViewerContentPro
 | 
			
		|||
    >
 | 
			
		||||
      <ErrorBoundary fallback={<code>{text}</code>}>
 | 
			
		||||
        <Suspense fallback={<code>{text}</code>}>
 | 
			
		||||
          <ReactPrism>{(codeRef) => <code ref={codeRef}>{text}</code>}</ReactPrism>
 | 
			
		||||
          <ReactPrism key={text}>{(codeRef) => <code ref={codeRef}>{text}</code>}</ReactPrism>
 | 
			
		||||
        </Suspense>
 | 
			
		||||
      </ErrorBoundary>
 | 
			
		||||
    </Text>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import React, { KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, useMemo } from 'react';
 | 
			
		||||
import { Editor } from 'slate';
 | 
			
		||||
import { Box, MenuItem, Text } from 'folds';
 | 
			
		||||
import { Box, config, MenuItem, Text } from 'folds';
 | 
			
		||||
import { Room } from 'matrix-js-sdk';
 | 
			
		||||
import { Command, useCommands } from '../../hooks/useCommands';
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -75,9 +75,6 @@ export function CommandAutocomplete({
 | 
			
		|||
      headerContent={
 | 
			
		||||
        <Box grow="Yes" direction="Row" gap="200" justifyContent="SpaceBetween">
 | 
			
		||||
          <Text size="L400">Commands</Text>
 | 
			
		||||
          <Text size="T200" priority="300" truncate>
 | 
			
		||||
            Begin your message with command
 | 
			
		||||
          </Text>
 | 
			
		||||
        </Box>
 | 
			
		||||
      }
 | 
			
		||||
      requestClose={requestClose}
 | 
			
		||||
| 
						 | 
				
			
			@ -87,17 +84,22 @@ export function CommandAutocomplete({
 | 
			
		|||
          key={commandName}
 | 
			
		||||
          as="button"
 | 
			
		||||
          radii="300"
 | 
			
		||||
          style={{ height: 'unset' }}
 | 
			
		||||
          onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
 | 
			
		||||
            onTabPress(evt, () => handleAutocomplete(commandName))
 | 
			
		||||
          }
 | 
			
		||||
          onClick={() => handleAutocomplete(commandName)}
 | 
			
		||||
        >
 | 
			
		||||
          <Box grow="Yes" direction="Row" gap="200" justifyContent="SpaceBetween">
 | 
			
		||||
            <Box shrink="No">
 | 
			
		||||
              <Text style={{ flexGrow: 1 }} size="B400" truncate>
 | 
			
		||||
                {`/${commandName}`}
 | 
			
		||||
              </Text>
 | 
			
		||||
            </Box>
 | 
			
		||||
          <Box
 | 
			
		||||
            style={{ padding: `${config.space.S300} 0` }}
 | 
			
		||||
            grow="Yes"
 | 
			
		||||
            direction="Column"
 | 
			
		||||
            gap="100"
 | 
			
		||||
            justifyContent="SpaceBetween"
 | 
			
		||||
          >
 | 
			
		||||
            <Text style={{ flexGrow: 1 }} size="B400" truncate>
 | 
			
		||||
              {`/${commandName}`}
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Text truncate priority="300" size="T200">
 | 
			
		||||
              {commands[commandName].description}
 | 
			
		||||
            </Text>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,34 +1,127 @@
 | 
			
		|||
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.split(' ');
 | 
			
		||||
  const users = rawIds.filter((id) => isUserId(id));
 | 
			
		||||
  return {
 | 
			
		||||
    users,
 | 
			
		||||
    reason,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
  const content = payload.slice(0, flagMatch.index);
 | 
			
		||||
  const flags = 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>;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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]
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -125,3 +125,9 @@ export const suffixRename = (name: string, validator: (newName: string) => boole
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export const replaceSpaceWithDash = (str: string): string => str.replace(/ /g, '-');
 | 
			
		||||
 | 
			
		||||
export const splitWithSpace = (content: string): string[] => {
 | 
			
		||||
  const trimmedContent = content.trim();
 | 
			
		||||
  if (trimmedContent === '') return [];
 | 
			
		||||
  return trimmedContent.split(' ');
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,11 +13,16 @@ import {
 | 
			
		|||
  UploadProgress,
 | 
			
		||||
  UploadResponse,
 | 
			
		||||
} from 'matrix-js-sdk';
 | 
			
		||||
import to from 'await-to-js';
 | 
			
		||||
import { IImageInfo, IThumbnailContent, IVideoInfo } from '../../types/matrix/common';
 | 
			
		||||
import { AccountDataEvent } from '../../types/matrix/accountData';
 | 
			
		||||
import { getStateEvent } from './room';
 | 
			
		||||
import { StateEvent } from '../../types/matrix/room';
 | 
			
		||||
 | 
			
		||||
const DOMAIN_REGEX = /\b(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\b/;
 | 
			
		||||
 | 
			
		||||
export const isServerName = (serverName: string): boolean => DOMAIN_REGEX.test(serverName);
 | 
			
		||||
 | 
			
		||||
export const matchMxId = (id: string): RegExpMatchArray | null => id.match(/^([@!$+#])(.+):(\S+)$/);
 | 
			
		||||
 | 
			
		||||
export const validMxId = (id: string): boolean => !!matchMxId(id);
 | 
			
		||||
| 
						 | 
				
			
			@ -292,3 +297,35 @@ export const downloadEncryptedMedia = async (
 | 
			
		|||
 | 
			
		||||
  return decryptedContent;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const rateLimitedActions = async <T, R = void>(
 | 
			
		||||
  data: T[],
 | 
			
		||||
  callback: (item: T) => Promise<R>,
 | 
			
		||||
  maxRetryCount?: number
 | 
			
		||||
) => {
 | 
			
		||||
  let retryCount = 0;
 | 
			
		||||
  const performAction = async (dataItem: T) => {
 | 
			
		||||
    const [err] = await to<R, MatrixError>(callback(dataItem));
 | 
			
		||||
 | 
			
		||||
    if (err?.httpStatus === 429) {
 | 
			
		||||
      if (retryCount === maxRetryCount) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const waitMS = err.getRetryAfterMs() ?? 200;
 | 
			
		||||
      await new Promise((resolve) => {
 | 
			
		||||
        setTimeout(resolve, waitMS);
 | 
			
		||||
      });
 | 
			
		||||
      retryCount += 1;
 | 
			
		||||
 | 
			
		||||
      await performAction(dataItem);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < data.length; i += 1) {
 | 
			
		||||
    const dataItem = data[i];
 | 
			
		||||
    retryCount = 0;
 | 
			
		||||
    // eslint-disable-next-line no-await-in-loop
 | 
			
		||||
    await performAction(dataItem);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue