atagen

malice

public
Star0
Watch0
e590609b63434b8b595b383f8124dea45191bd6e
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/src/lib/commands.tsTypescript121 lines3.5 KB
1
import type { Person } from './types';
2
3
export interface ParsedCommand {
4
  name: string;
5
  args: string;
6
}
7
8
export const COMMANDS = [
9
  'me',
10
  'notice',
11
  'sticker',
12
  'msg',
13
  'join',
14
  'leave',
15
  'invite',
16
  'disinvite',
17
  'kick',
18
  'ban',
19
  'unban',
20
  'powerlevel',
21
  'mute',
22
  'unmute',
23
  'ignore',
24
  'unignore',
25
  'delete',
26
  'roomnick',
27
  'globalnick',
28
  'roomavatar',
29
  'globalavatar'
30
] as const;
31
export type CommandName = (typeof COMMANDS)[number];
32
33
/** A leading `/word`, or null for plain text. `//text` sends `/text` as a message. */
34
export function parseCommand(text: string): ParsedCommand | null {
35
  const match = /^\/([a-z]+)(?:\s+([\s\S]*))?$/i.exec(text.trim());
36
  if (!match) return null;
37
  return { name: match[1].toLowerCase(), args: (match[2] ?? '').trim() };
38
}
39
40
export function isKnownCommand(name: string): name is CommandName {
41
  return (COMMANDS as readonly string[]).includes(name);
42
}
43
44
export function unescapeCommand(text: string): string {
45
  return text.startsWith('//') ? text.slice(1) : text;
46
}
47
48
export type Target = { user_id: string; rest: string } | { error: string };
49
50
/** The user an argument string starts with, by matrix id or by a known display name. */
51
export function splitTarget(args: string, people: Person[]): Target {
52
  if (!args) return { error: 'Name someone' };
53
  if (args.startsWith('@')) {
54
    const [id, ...rest] = args.split(/\s+/);
55
    if (/^@[^:\s]+:\S+$/.test(id)) return { user_id: id, rest: rest.join(' ') };
56
  }
57
  const lower = args.toLowerCase();
58
  let best: Person[] = [];
59
  let bestLength = 0;
60
  for (const person of people) {
61
    const name = person.name.toLowerCase();
62
    if (!name || name.length < bestLength) continue;
63
    if (lower !== name && !lower.startsWith(`${name} `)) continue;
64
    if (name.length > bestLength) {
65
      best = [person];
66
      bestLength = name.length;
67
    } else {
68
      best.push(person);
69
    }
70
  }
71
  const ids = [...new Set(best.map((p) => p.user_id))];
72
  if (ids.length === 0) return { error: `No one here is called ${args.split(/\s+/)[0]}` };
73
  if (ids.length > 1) return { error: `Several people are called ${best[0].name}, use their @id` };
74
  return { user_id: ids[0], rest: args.slice(bestLength).trim() };
75
}
76
77
export const COMMAND_USAGE: Record<CommandName, string> = {
78
  me: '<action>',
79
  notice: '<text>',
80
  sticker: '<sticker name>',
81
  msg: '<user> [message]',
82
  join: '<room>',
83
  leave: '',
84
  invite: '<user>',
85
  disinvite: '<user> [reason]',
86
  kick: '<user> [reason]',
87
  ban: '<user> [reason]',
88
  unban: '<user> [reason]',
89
  powerlevel: '<user> <level>',
90
  mute: '<user>',
91
  unmute: '<user>',
92
  ignore: '<user>',
93
  unignore: '<user>',
94
  delete: '[reason]',
95
  roomnick: '[name]',
96
  globalnick: '<name>',
97
  roomavatar: '[mxc]',
98
  globalavatar: '[mxc]'
99
};
100
101
/** The command name being typed when the caret sits in a leading `/word`. */
102
export function commandQueryAt(text: string, caret: number): string | null {
103
  const match = /^\/([a-z]*)$/i.exec(text.slice(0, caret));
104
  return match ? match[1].toLowerCase() : null;
105
}
106
107
export function matchCommands(query: string): CommandName[] {
108
  const q = query.toLowerCase();
109
  return COMMANDS.flatMap((name, order) => {
110
    if (name.startsWith(q)) return [{ name, score: -1, order }];
111
    let previous = -1;
112
    let score = 0;
113
    for (const letter of q) {
114
      const index = name.indexOf(letter, previous + 1);
115
      if (index < 0) return [];
116
      score += index - previous - 1;
117
      previous = index;
118
    }
119
    return [{ name, score, order }];
120
  }).sort((a, b) => a.score - b.score || a.order - b.order).map(({ name }) => name);
121
}