atagen

malice

public
Star0
Watch0
db526ad90ce25d51e848525fa1f3b04eee4496d3
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/commands.ts

e4c64c36f105atagen2026-10-24 07:09 UTC1
import type { Person } from './types';
e4c64c36f105atagen2026-10-24 07:09 UTC2
e4c64c36f105atagen2026-10-24 07:09 UTC3
export interface ParsedCommand {
e4c64c36f105atagen2026-10-24 07:09 UTC4
  name: string;
e4c64c36f105atagen2026-10-24 07:09 UTC5
  args: string;
e4c64c36f105atagen2026-10-24 07:09 UTC6
}
e4c64c36f105atagen2026-10-24 07:09 UTC7
e4c64c36f105atagen2026-10-24 07:09 UTC8
export const COMMANDS = [
e4c64c36f105atagen2026-10-24 07:09 UTC9
  'me',
7cca1d5628d1atagen2026-10-24 07:36 UTC10
  'notice',
132eeceeb362atagen2026-10-25 03:34 UTC11
  'sticker',
e4c64c36f105atagen2026-10-24 07:09 UTC12
  'msg',
e4c64c36f105atagen2026-10-24 07:09 UTC13
  'join',
e4c64c36f105atagen2026-10-24 07:09 UTC14
  'leave',
e4c64c36f105atagen2026-10-24 07:09 UTC15
  'invite',
e4c64c36f105atagen2026-10-24 07:09 UTC16
  'disinvite',
e4c64c36f105atagen2026-10-24 07:09 UTC17
  'kick',
e4c64c36f105atagen2026-10-24 07:09 UTC18
  'ban',
e4c64c36f105atagen2026-10-24 07:09 UTC19
  'unban',
e4c64c36f105atagen2026-10-24 07:09 UTC20
  'powerlevel',
7cca1d5628d1atagen2026-10-24 07:36 UTC21
  'mute',
7cca1d5628d1atagen2026-10-24 07:36 UTC22
  'unmute',
7cca1d5628d1atagen2026-10-24 07:36 UTC23
  'ignore',
7cca1d5628d1atagen2026-10-24 07:36 UTC24
  'unignore',
e4c64c36f105atagen2026-10-24 07:09 UTC25
  'delete',
7cca1d5628d1atagen2026-10-24 07:36 UTC26
  'roomnick',
e4c64c36f105atagen2026-10-24 07:09 UTC27
  'globalnick',
7cca1d5628d1atagen2026-10-24 07:36 UTC28
  'roomavatar',
e4c64c36f105atagen2026-10-24 07:09 UTC29
  'globalavatar'
e4c64c36f105atagen2026-10-24 07:09 UTC30
] as const;
e4c64c36f105atagen2026-10-24 07:09 UTC31
export type CommandName = (typeof COMMANDS)[number];
e4c64c36f105atagen2026-10-24 07:09 UTC32
e4c64c36f105atagen2026-10-24 07:09 UTC33
/** A leading `/word`, or null for plain text. `//text` sends `/text` as a message. */
e4c64c36f105atagen2026-10-24 07:09 UTC34
export function parseCommand(text: string): ParsedCommand | null {
e4c64c36f105atagen2026-10-24 07:09 UTC35
  const match = /^\/([a-z]+)(?:\s+([\s\S]*))?$/i.exec(text.trim());
e4c64c36f105atagen2026-10-24 07:09 UTC36
  if (!match) return null;
e4c64c36f105atagen2026-10-24 07:09 UTC37
  return { name: match[1].toLowerCase(), args: (match[2] ?? '').trim() };
e4c64c36f105atagen2026-10-24 07:09 UTC38
}
e4c64c36f105atagen2026-10-24 07:09 UTC39
e4c64c36f105atagen2026-10-24 07:09 UTC40
export function isKnownCommand(name: string): name is CommandName {
e4c64c36f105atagen2026-10-24 07:09 UTC41
  return (COMMANDS as readonly string[]).includes(name);
e4c64c36f105atagen2026-10-24 07:09 UTC42
}
e4c64c36f105atagen2026-10-24 07:09 UTC43
e4c64c36f105atagen2026-10-24 07:09 UTC44
export function unescapeCommand(text: string): string {
e4c64c36f105atagen2026-10-24 07:09 UTC45
  return text.startsWith('//') ? text.slice(1) : text;
e4c64c36f105atagen2026-10-24 07:09 UTC46
}
e4c64c36f105atagen2026-10-24 07:09 UTC47
e4c64c36f105atagen2026-10-24 07:09 UTC48
export type Target = { user_id: string; rest: string } | { error: string };
e4c64c36f105atagen2026-10-24 07:09 UTC49
e4c64c36f105atagen2026-10-24 07:09 UTC50
/** The user an argument string starts with, by matrix id or by a known display name. */
e4c64c36f105atagen2026-10-24 07:09 UTC51
export function splitTarget(args: string, people: Person[]): Target {
e4c64c36f105atagen2026-10-24 07:09 UTC52
  if (!args) return { error: 'Name someone' };
e4c64c36f105atagen2026-10-24 07:09 UTC53
  if (args.startsWith('@')) {
e4c64c36f105atagen2026-10-24 07:09 UTC54
    const [id, ...rest] = args.split(/\s+/);
e4c64c36f105atagen2026-10-24 07:09 UTC55
    if (/^@[^:\s]+:\S+$/.test(id)) return { user_id: id, rest: rest.join(' ') };
e4c64c36f105atagen2026-10-24 07:09 UTC56
  }
e4c64c36f105atagen2026-10-24 07:09 UTC57
  const lower = args.toLowerCase();
e4c64c36f105atagen2026-10-24 07:09 UTC58
  let best: Person[] = [];
e4c64c36f105atagen2026-10-24 07:09 UTC59
  let bestLength = 0;
e4c64c36f105atagen2026-10-24 07:09 UTC60
  for (const person of people) {
e4c64c36f105atagen2026-10-24 07:09 UTC61
    const name = person.name.toLowerCase();
e4c64c36f105atagen2026-10-24 07:09 UTC62
    if (!name || name.length < bestLength) continue;
e4c64c36f105atagen2026-10-24 07:09 UTC63
    if (lower !== name && !lower.startsWith(`${name} `)) continue;
e4c64c36f105atagen2026-10-24 07:09 UTC64
    if (name.length > bestLength) {
e4c64c36f105atagen2026-10-24 07:09 UTC65
      best = [person];
e4c64c36f105atagen2026-10-24 07:09 UTC66
      bestLength = name.length;
e4c64c36f105atagen2026-10-24 07:09 UTC67
    } else {
e4c64c36f105atagen2026-10-24 07:09 UTC68
      best.push(person);
e4c64c36f105atagen2026-10-24 07:09 UTC69
    }
e4c64c36f105atagen2026-10-24 07:09 UTC70
  }
e4c64c36f105atagen2026-10-24 07:09 UTC71
  const ids = [...new Set(best.map((p) => p.user_id))];
e4c64c36f105atagen2026-10-24 07:09 UTC72
  if (ids.length === 0) return { error: `No one here is called ${args.split(/\s+/)[0]}` };
e4c64c36f105atagen2026-10-24 07:09 UTC73
  if (ids.length > 1) return { error: `Several people are called ${best[0].name}, use their @id` };
e4c64c36f105atagen2026-10-24 07:09 UTC74
  return { user_id: ids[0], rest: args.slice(bestLength).trim() };
e4c64c36f105atagen2026-10-24 07:09 UTC75
}
05a48e9e7942atagen2026-10-24 09:32 UTC76
05a48e9e7942atagen2026-10-24 09:32 UTC77
export const COMMAND_USAGE: Record<CommandName, string> = {
05a48e9e7942atagen2026-10-24 09:32 UTC78
  me: '<action>',
05a48e9e7942atagen2026-10-24 09:32 UTC79
  notice: '<text>',
132eeceeb362atagen2026-10-25 03:34 UTC80
  sticker: '<sticker name>',
05a48e9e7942atagen2026-10-24 09:32 UTC81
  msg: '<user> [message]',
05a48e9e7942atagen2026-10-24 09:32 UTC82
  join: '<room>',
05a48e9e7942atagen2026-10-24 09:32 UTC83
  leave: '',
05a48e9e7942atagen2026-10-24 09:32 UTC84
  invite: '<user>',
05a48e9e7942atagen2026-10-24 09:32 UTC85
  disinvite: '<user> [reason]',
05a48e9e7942atagen2026-10-24 09:32 UTC86
  kick: '<user> [reason]',
05a48e9e7942atagen2026-10-24 09:32 UTC87
  ban: '<user> [reason]',
05a48e9e7942atagen2026-10-24 09:32 UTC88
  unban: '<user> [reason]',
05a48e9e7942atagen2026-10-24 09:32 UTC89
  powerlevel: '<user> <level>',
05a48e9e7942atagen2026-10-24 09:32 UTC90
  mute: '<user>',
05a48e9e7942atagen2026-10-24 09:32 UTC91
  unmute: '<user>',
05a48e9e7942atagen2026-10-24 09:32 UTC92
  ignore: '<user>',
05a48e9e7942atagen2026-10-24 09:32 UTC93
  unignore: '<user>',
05a48e9e7942atagen2026-10-24 09:32 UTC94
  delete: '[reason]',
05a48e9e7942atagen2026-10-24 09:32 UTC95
  roomnick: '[name]',
05a48e9e7942atagen2026-10-24 09:32 UTC96
  globalnick: '<name>',
05a48e9e7942atagen2026-10-24 09:32 UTC97
  roomavatar: '[mxc]',
05a48e9e7942atagen2026-10-24 09:32 UTC98
  globalavatar: '[mxc]'
05a48e9e7942atagen2026-10-24 09:32 UTC99
};
05a48e9e7942atagen2026-10-24 09:32 UTC100
05a48e9e7942atagen2026-10-24 09:32 UTC101
/** The command name being typed when the caret sits in a leading `/word`. */
05a48e9e7942atagen2026-10-24 09:32 UTC102
export function commandQueryAt(text: string, caret: number): string | null {
05a48e9e7942atagen2026-10-24 09:32 UTC103
  const match = /^\/([a-z]*)$/i.exec(text.slice(0, caret));
05a48e9e7942atagen2026-10-24 09:32 UTC104
  return match ? match[1].toLowerCase() : null;
05a48e9e7942atagen2026-10-24 09:32 UTC105
}
05a48e9e7942atagen2026-10-24 09:32 UTC106
132eeceeb362atagen2026-10-25 03:34 UTC107
export function matchCommands(query: string): CommandName[] {
132eeceeb362atagen2026-10-25 03:34 UTC108
  const q = query.toLowerCase();
132eeceeb362atagen2026-10-25 03:34 UTC109
  return COMMANDS.flatMap((name, order) => {
132eeceeb362atagen2026-10-25 03:34 UTC110
    if (name.startsWith(q)) return [{ name, score: -1, order }];
132eeceeb362atagen2026-10-25 03:34 UTC111
    let previous = -1;
132eeceeb362atagen2026-10-25 03:34 UTC112
    let score = 0;
132eeceeb362atagen2026-10-25 03:34 UTC113
    for (const letter of q) {
132eeceeb362atagen2026-10-25 03:34 UTC114
      const index = name.indexOf(letter, previous + 1);
132eeceeb362atagen2026-10-25 03:34 UTC115
      if (index < 0) return [];
132eeceeb362atagen2026-10-25 03:34 UTC116
      score += index - previous - 1;
132eeceeb362atagen2026-10-25 03:34 UTC117
      previous = index;
132eeceeb362atagen2026-10-25 03:34 UTC118
    }
132eeceeb362atagen2026-10-25 03:34 UTC119
    return [{ name, score, order }];
132eeceeb362atagen2026-10-25 03:34 UTC120
  }).sort((a, b) => a.score - b.score || a.order - b.order).map(({ name }) => name);
05a48e9e7942atagen2026-10-24 09:32 UTC121
}