@stage-labs/metro 0.1.0-beta.21 → 0.1.0-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/cli.js +18 -12
  2. package/dist/control.js +68 -12
  3. package/dist/prompt.js +51 -0
  4. package/package.json +1 -1
  5. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/package.json +1 -1
  6. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/accounts.ts +10 -10
  7. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/actions.ts +1 -1
  8. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/attachments.ts +1 -1
  9. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/format.ts +7 -7
  10. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group-actions.ts +1 -1
  11. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/index.ts +10 -10
  12. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/station.ts +2 -2
  13. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-transcribe.ts +6 -6
  14. package/runtime/node_modules/@metro-labs/mcp/package.json +2 -2
  15. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +14 -8
  16. package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +2 -4
  17. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +49 -27
  18. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -2
  19. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +6 -6
  20. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +6 -6
  21. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +6 -6
  22. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +2 -2
  23. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +4 -4
  24. package/runtime/node_modules/@metro-labs/telegram/package.json +5 -2
  25. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +47 -86
  26. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +210 -251
  27. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +66 -107
  28. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/client.ts +4 -4
  29. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +110 -141
  30. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/group.ts +1 -1
  31. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/history.ts +1 -1
  32. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/inbound.ts +6 -6
  33. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +35 -92
  34. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/login.ts +20 -20
  35. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +65 -151
  36. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +60 -32
  37. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/reactions.ts +1 -1
  38. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +8 -1
  39. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +6 -47
  40. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -3
  41. package/runtime/node_modules/@metro-labs/{telegram-user → telegram-bot}/package.json +3 -6
  42. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  43. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  44. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  45. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  46. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  47. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  48. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  49. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  50. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  51. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  52. package/runtime/runtime.json +1 -1
  53. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +0 -81
  54. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +0 -254
  55. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +0 -87
  56. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +0 -141
  57. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +0 -46
  58. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +0 -80
  59. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +0 -75
  60. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +0 -24
  61. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +0 -7
  62. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +0 -1
  63. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/api-base.ts +0 -0
  64. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group.ts +0 -0
  65. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/members.ts +0 -0
  66. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/send-files.ts +0 -0
  67. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/verify.ts +0 -0
  68. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-speak.ts +0 -0
  69. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice.ts +0 -0
  70. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/wire.ts +0 -0
  71. /package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/normalize.ts +0 -0
  72. /package/runtime/node_modules/@metro-labs/{telegram → telegram-bot}/src/verify.ts +0 -0
@@ -1,295 +1,254 @@
1
- import { accountFor, accounts, tg, targetOf } from './accounts.js';
2
- import { respond } from './wire.js';
1
+ import { TrainError } from '@metro-labs/mcp/train-error';
3
2
  import { errMsg } from '@metro-labs/mcp/log';
4
- import { normalizeTelegram } from '@metro-labs/mcp/stations/messaging-normalize';
5
- import {
6
- adminMemberList,
7
- inaccessibleMemberList,
8
- type TgChatMember,
9
- } from './members.js';
10
3
  import {
11
4
  makeStation,
5
+ respond,
12
6
  type CallMsg,
13
7
  type StationHandler,
14
8
  } from '@metro-labs/mcp/stations/station-runtime';
15
- import { mediaKindOf } from './attachments.js';
16
- import { assertContentLength } from '@metro-labs/mcp/stations/attachments';
9
+ import { accountFor, accounts, targetOf } from './accounts.js';
10
+ import type { UserClient } from './client.js';
11
+ import { normalizeTelegramUser } from './normalize.js';
17
12
  import {
18
- emitOutbound,
19
- finishSend,
20
- media,
21
- MEDIA_METHOD_FIELD,
22
- sendDice,
23
- sendLocation,
24
- sendMedia,
13
+ sendAttachments,
14
+ type CanonicalAttachment,
25
15
  } from './media-actions.js';
16
+ import { clampLimit, shapeHistory } from './history.js';
17
+ import { fetchMembers, isRestricted, restrictedMemberList } from './members.js';
18
+ import {
19
+ groupAddMembers,
20
+ groupCreate,
21
+ groupInviteLink,
22
+ groupRemoveMembers,
23
+ } from './group.js';
24
+
25
+ type Args = Record<string, unknown>;
26
+
27
+ interface Resolved {
28
+ accountId: string;
29
+ client: UserClient;
30
+ chatId: number;
31
+ topicId?: number;
32
+ }
26
33
 
34
+ type ClientFor = (accountId: string) => UserClient;
27
35
 
36
+ function str(v: unknown): string | undefined {
37
+ return typeof v === 'string' ? v : undefined;
38
+ }
28
39
 
40
+ function lineOfArgs(args: Args): string {
41
+ const line = str(args.line);
42
+ if (!line) throw new TrainError('bad_request', 'missing line');
43
+ return line;
44
+ }
29
45
 
30
- export type { CallMsg };
46
+ function resolve(args: Args, clientFor: ClientFor): Resolved {
47
+ const line = lineOfArgs(args);
48
+ const target = targetOf(line);
49
+ if (!target) throw new TrainError('bad_request', `bad line '${line}'`);
50
+ const accountId = accountFor({ account: str(args.account), line });
51
+ return {
52
+ accountId,
53
+ client: clientFor(accountId),
54
+ chatId: target.chatId,
55
+ ...(target.topicId !== undefined ? { topicId: target.topicId } : {}),
56
+ };
57
+ }
31
58
 
32
- const meCache = new Map<string, { id: number; username: string | null }>();
33
- async function getMe(
34
- accountId: string,
35
- ): Promise<{ id: number; username: string | null } | null> {
36
- const cached = meCache.get(accountId);
37
- if (cached) return cached;
59
+ async function guard<T>(run: () => Promise<T>): Promise<T> {
38
60
  try {
39
- const me = await tg<{ id: number; username?: string }>(
40
- accountId,
41
- 'getMe',
42
- {},
43
- 10_000,
44
- );
45
- const v = { id: me.id, username: me.username ?? null };
46
- meCache.set(accountId, v);
47
- return v;
48
- } catch {
49
- return null;
61
+ return await run();
62
+ } catch (e) {
63
+ const msg = errMsg(e);
64
+ if (/FLOOD_WAIT|SLOWMODE_WAIT/i.test(msg))
65
+ throw new TrainError('rate_limited', msg);
66
+ throw new TrainError('telegram_call', msg);
50
67
  }
51
68
  }
52
69
 
53
- async function listAccounts(id: string): Promise<void> {
54
- const list = await Promise.all(
55
- [...accounts.values()].map(async (a) => {
56
- const me = await getMe(a.cfg.id);
57
- const username = me?.username ?? null;
58
- return {
59
- id: a.cfg.id,
60
- handle: username === null ? null : `@${username}`,
61
- url: username === null ? null : `https://t.me/${username}`,
62
- owner: a.cfg.owner ?? null,
63
- botId: me?.id ?? null,
64
- username,
65
- };
66
- }),
67
- );
68
- respond(id, { result: { accounts: list } });
70
+ function attachmentsOf(args: Args): CanonicalAttachment[] {
71
+ const raw = args.attachments;
72
+ return Array.isArray(raw) ? (raw as CanonicalAttachment[]) : [];
69
73
  }
70
74
 
71
- interface SendArgs {
72
- line: string;
73
- text: string;
74
- replyTo?: string;
75
- parseMode?: string;
76
- buttons?: { text: string; url: string }[][];
77
- account?: string;
78
- attachments?: {
79
- kind?: string;
80
- path?: string;
81
- url?: string;
82
- mime?: string;
83
- name?: string;
84
- }[];
75
+ function makeSend(clientFor: ClientFor): StationHandler {
76
+ return async (id, args) => {
77
+ const { accountId, client, chatId, topicId } = resolve(args, clientFor);
78
+ const text = str(args.text) ?? '';
79
+ const replyTo = str(args.replyTo);
80
+ const explicitReplyTo = replyTo ? Number(replyTo) : undefined;
81
+ const replyParam = explicitReplyTo ?? topicId;
82
+ const attachments = attachmentsOf(args);
83
+ if (attachments.length) {
84
+ const sent = await guard(() =>
85
+ sendAttachments({ client, chatId, replyTo: replyParam }, attachments, text),
86
+ );
87
+ respond(id, {
88
+ result: {
89
+ messageId: String(sent.message.id),
90
+ account: accountId,
91
+ attachments: sent.delivered,
92
+ },
93
+ });
94
+ return;
95
+ }
96
+ const peer = await guard(() => client.tg.resolvePeer(chatId));
97
+ const sent = await guard(() =>
98
+ client.tg.sendText(
99
+ peer,
100
+ text,
101
+ replyParam !== undefined ? { replyTo: replyParam } : undefined,
102
+ ),
103
+ );
104
+ respond(id, {
105
+ result: { messageId: String(sent.id), account: accountId },
106
+ });
107
+ };
85
108
  }
86
109
 
87
-
88
- type WireAttachment = NonNullable<SendArgs['attachments']>[number];
89
-
90
- const attachmentSrc = (att: WireAttachment): string | undefined =>
91
- att.path ?? att.url;
92
-
93
- const attachmentKind = (att: WireAttachment): string =>
94
- mediaKindOf(att.kind, att.mime, attachmentSrc(att) ?? att.name);
95
-
96
- async function sendOneAttachment(
97
- a: SendArgs,
98
- att: WireAttachment,
99
- caption: string | undefined,
100
- ): Promise<{ accountId: string; message_id: number; kind: string }> {
101
- const kind = attachmentKind(att);
102
- const mf = MEDIA_METHOD_FIELD[kind];
103
- if (!mf) throw new Error(`telegram cannot send a '${kind}' attachment`);
104
- const sent = await sendMedia(mf.method, mf.field, {
105
- line: a.line,
106
- path: attachmentSrc(att),
107
- caption,
108
- replyTo: a.replyTo,
109
- parseMode: a.parseMode,
110
- account: a.account,
111
- name: att.name,
112
- });
113
- return { ...sent, kind };
110
+ function makeReact(clientFor: ClientFor): StationHandler {
111
+ return async (id, args) => {
112
+ const { accountId, client, chatId } = resolve(args, clientFor);
113
+ const message = Number(str(args.messageId));
114
+ const raw = str(args.emoji);
115
+ const emoji = raw === undefined || raw === '' ? null : raw;
116
+ await guard(() =>
117
+ client.tg.sendReaction({ chatId, message, emoji }),
118
+ );
119
+ respond(id, { result: { ok: true, account: accountId } });
120
+ };
114
121
  }
115
122
 
116
- const captionLabel = (caption: string | undefined, kind: string): string =>
117
- caption !== undefined && caption !== '' ? caption : `[${kind}]`;
123
+ function makeEdit(clientFor: ClientFor): StationHandler {
124
+ return async (id, args) => {
125
+ const { accountId, client, chatId } = resolve(args, clientFor);
126
+ const message = Number(str(args.messageId));
127
+ const text = str(args.text) ?? '';
128
+ await guard(() => client.tg.editMessage({ chatId, message, text }));
129
+ respond(id, { result: { ok: true, account: accountId } });
130
+ };
131
+ }
118
132
 
119
- async function sendAttachments(id: string, a: SendArgs): Promise<void> {
120
- const { line, text, replyTo, attachments = [] } = a;
121
- let last: { accountId: string; message_id: number } | undefined;
122
- const delivered: string[] = [];
123
- for (let i = 0; i < attachments.length; i++) {
124
- const att = attachments[i];
125
- if (!att) continue;
126
- const caption = i === 0 ? text : undefined;
127
- const sent = await sendOneAttachment(a, att, caption);
128
- last = sent;
129
- delivered.push(sent.kind);
130
- emitOutbound(
131
- sent.accountId,
132
- line,
133
- String(sent.message_id),
134
- captionLabel(caption, sent.kind),
135
- replyTo,
133
+ function makeDelete(clientFor: ClientFor): StationHandler {
134
+ return async (id, args) => {
135
+ const { accountId, client, chatId } = resolve(args, clientFor);
136
+ const message = Number(str(args.messageId));
137
+ await guard(() =>
138
+ client.tg.deleteMessagesById(chatId, [message], { revoke: true }),
136
139
  );
137
- }
138
- if (!last) throw new Error('no attachments were sent');
139
- respond(id, {
140
- result: {
141
- messageId: String(last.message_id),
142
- account: last.accountId,
143
- attachments: delivered,
144
- },
145
- });
140
+ respond(id, { result: { ok: true, account: accountId } });
141
+ };
146
142
  }
147
143
 
148
- async function send(id: string, args: Record<string, unknown>): Promise<void> {
149
- const a = args as unknown as SendArgs;
150
- if (a.attachments?.length) {
151
- await sendAttachments(id, a);
152
- return;
153
- }
154
- const { line, text, replyTo, parseMode, buttons, account } = a;
155
- const { accountId, chatId, topicId } = targetOf(line, account);
156
- const body: Record<string, unknown> = { chat_id: chatId, text };
157
- if (topicId !== undefined) body.message_thread_id = topicId;
158
- if (replyTo) body.reply_parameters = { message_id: Number(replyTo) };
159
- if (parseMode) body.parse_mode = parseMode;
160
- if (buttons) body.reply_markup = { inline_keyboard: buttons };
161
- const sent = await tg<{ message_id: number }>(accountId, 'sendMessage', body);
162
- finishSend(id, accountId, line, String(sent.message_id), text, replyTo);
144
+ function makeRead(clientFor: ClientFor): StationHandler {
145
+ return async (id, args) => {
146
+ const { accountId, client, chatId } = resolve(args, clientFor);
147
+ const limit = clampLimit(args.limit);
148
+ const before = str(args.before);
149
+ const beforeId = before ? Number(before) : undefined;
150
+ const offset =
151
+ beforeId !== undefined && Number.isFinite(beforeId)
152
+ ? { offset: { id: beforeId, date: 0 } }
153
+ : {};
154
+ const page = await guard(() =>
155
+ client.tg.getHistory(chatId, { limit, ...offset }),
156
+ );
157
+ respond(id, { result: shapeHistory(accountId, chatId, page) });
158
+ };
163
159
  }
164
160
 
165
- async function react(id: string, args: Record<string, unknown>): Promise<void> {
166
- const { line, messageId, emoji, account } = args as {
167
- line: string;
168
- messageId: string;
169
- emoji: string;
170
- account?: string;
161
+ function makeListMembers(clientFor: ClientFor): StationHandler {
162
+ return async (id, args) => {
163
+ const { client, chatId } = resolve(args, clientFor);
164
+ const limit = typeof args.limit === 'number' ? args.limit : undefined;
165
+ try {
166
+ respond(id, { result: await fetchMembers(client, chatId, limit) });
167
+ } catch (e) {
168
+ const msg = errMsg(e);
169
+ if (isRestricted(msg)) {
170
+ respond(id, { result: restrictedMemberList(msg) });
171
+ return;
172
+ }
173
+ throw e;
174
+ }
171
175
  };
172
- const { accountId, chatId } = targetOf(line, account);
173
- await tg(accountId, 'setMessageReaction', {
174
- chat_id: chatId,
175
- message_id: Number(messageId),
176
- reaction: emoji ? [{ type: 'emoji', emoji }] : [],
177
- });
178
- respond(id, { result: { ok: true, account: accountId } });
179
176
  }
180
177
 
181
- async function edit(id: string, args: Record<string, unknown>): Promise<void> {
182
- const { line, messageId, text, parseMode, account } = args as {
183
- line: string;
184
- messageId: string;
185
- text: string;
186
- parseMode?: string;
187
- account?: string;
178
+ function makeGroupCreate(clientFor: ClientFor): StationHandler {
179
+ return async (id, args) => {
180
+ const accountId = accountFor({ account: str(args.account) });
181
+ const client = clientFor(accountId);
182
+ respond(id, {
183
+ result: await guard(() => groupCreate(client, accountId, args)),
184
+ });
188
185
  };
189
- const { accountId, chatId } = targetOf(line, account);
190
- await tg(accountId, 'editMessageText', {
191
- chat_id: chatId,
192
- message_id: Number(messageId),
193
- text,
194
- parse_mode: parseMode,
195
- });
196
- respond(id, { result: { ok: true, account: accountId } });
197
186
  }
198
187
 
199
- async function remove(id: string, args: Record<string, unknown>): Promise<void> {
200
- const { line, messageId, account } = args as {
201
- line: string;
202
- messageId: string;
203
- account?: string;
188
+ function makeGroupAdd(clientFor: ClientFor): StationHandler {
189
+ return async (id, args) => {
190
+ const { client, chatId } = resolve(args, clientFor);
191
+ const line = lineOfArgs(args);
192
+ respond(id, {
193
+ result: await guard(() => groupAddMembers(client, chatId, line, args)),
194
+ });
204
195
  };
205
- const { accountId, chatId } = targetOf(line, account);
206
- await tg(accountId, 'deleteMessage', {
207
- chat_id: chatId,
208
- message_id: Number(messageId),
209
- });
210
- respond(id, { result: { ok: true, account: accountId } });
211
196
  }
212
197
 
213
-
214
-
215
- async function download(id: string, args: Record<string, unknown>): Promise<void> {
216
- const {
217
- fileId,
218
- outDir = '/tmp',
219
- account,
220
- } = args as { fileId: string; outDir?: string; account?: string };
221
- const accountId = accountFor({ account });
222
- const acct = accounts.get(accountId);
223
- if (!acct) throw new Error(`unknown account '${accountId}'`);
224
- const meta = await tg<{ file_path: string }>(accountId, 'getFile', {
225
- file_id: fileId,
226
- });
227
- const res = await fetch(`${acct.fileApi}/${meta.file_path}`, {
228
- signal: AbortSignal.timeout(60_000),
229
- });
230
- if (!res.ok) throw new Error(`telegram download ${res.status}`);
231
- assertContentLength(res.headers.get('content-length'));
232
- const data = await res.arrayBuffer();
233
- const filename = meta.file_path.split('/').pop() ?? `${fileId}.bin`;
234
- const path = `${outDir}/${Date.now()}-${filename}`;
235
- await Bun.write(path, data);
236
- respond(id, {
237
- result: { path, fileSize: data.byteLength, account: accountId },
238
- });
198
+ function makeGroupRemove(clientFor: ClientFor): StationHandler {
199
+ return async (id, args) => {
200
+ const { client, chatId } = resolve(args, clientFor);
201
+ const line = lineOfArgs(args);
202
+ respond(id, {
203
+ result: await guard(() => groupRemoveMembers(client, chatId, line, args)),
204
+ });
205
+ };
239
206
  }
240
207
 
241
- async function listMembers(
242
- id: string,
243
- args: Record<string, unknown>,
244
- ): Promise<void> {
245
- const { line, account } = args as { line: string; account?: string };
246
- const { accountId, chatId } = targetOf(line, account);
247
- let admins: TgChatMember[];
248
- try {
249
- admins = await tg<TgChatMember[]>(accountId, 'getChatAdministrators', {
250
- chat_id: chatId,
251
- });
252
- } catch (e) {
253
- respond(id, { result: inaccessibleMemberList(errMsg(e)) });
254
- return;
255
- }
256
- let total: number | null = null;
257
- try {
258
- total = await tg<number>(accountId, 'getChatMemberCount', {
259
- chat_id: chatId,
208
+ function makeGroupInvite(clientFor: ClientFor): StationHandler {
209
+ return async (id, args) => {
210
+ const { client, chatId } = resolve(args, clientFor);
211
+ const line = lineOfArgs(args);
212
+ respond(id, {
213
+ result: await guard(() => groupInviteLink(client, chatId, line)),
260
214
  });
261
- } catch {
262
- total = null;
263
- }
264
- respond(id, { result: adminMemberList(admins, total) });
215
+ };
265
216
  }
266
217
 
267
- const HANDLERS: Record<string, StationHandler> = {
268
- accounts: (id) => listAccounts(id),
269
- send,
270
- react,
271
- edit,
272
- delete: remove,
273
- listMembers,
274
- send_photo: (id, args) =>
275
- media(id, 'sendPhoto', 'photo', ((args.caption as string) ?? '') + ' [image]', args),
276
- send_document: (id, args) =>
277
- media(id, 'sendDocument', 'document', ((args.caption as string) ?? '') + ' [file]', args),
278
- send_voice: (id, args) => media(id, 'sendVoice', 'voice', '[voice]', args),
279
- send_sticker: (id, args) => media(id, 'sendSticker', 'sticker', '[sticker]', args),
280
- send_dice: sendDice,
281
- send_location: sendLocation,
282
- download,
283
- };
218
+ function makeAccounts(clientFor: ClientFor): StationHandler {
219
+ return (id) => {
220
+ const list = [...accounts.values()].map((a) => {
221
+ const me = clientFor(a.id).self();
222
+ const username = me?.username ?? null;
223
+ return {
224
+ id: a.id,
225
+ handle: username === null ? me?.phone ?? null : `@${username}`,
226
+ url: username === null ? null : `https://t.me/${username}`,
227
+ owner: a.owner ?? null,
228
+ };
229
+ });
230
+ respond(id, { result: { accounts: list } });
231
+ return Promise.resolve();
232
+ };
233
+ }
284
234
 
285
- export const handleCall = makeStation({
286
- handlers: HANDLERS,
287
- normalize: normalizeTelegram,
288
- preDispatch: (id, action) => {
289
- if (action === 'read') {
290
- respond(id, { error: "unsupported verb 'read' on telegram" });
291
- return true;
292
- }
293
- return false;
294
- },
295
- });
235
+ export function makeHandleCall(
236
+ clientFor: ClientFor,
237
+ ): (msg: CallMsg) => Promise<void> {
238
+ return makeStation({
239
+ handlers: {
240
+ accounts: makeAccounts(clientFor),
241
+ send: makeSend(clientFor),
242
+ react: makeReact(clientFor),
243
+ edit: makeEdit(clientFor),
244
+ delete: makeDelete(clientFor),
245
+ read: makeRead(clientFor),
246
+ listMembers: makeListMembers(clientFor),
247
+ groupCreate: makeGroupCreate(clientFor),
248
+ groupAddMembers: makeGroupAdd(clientFor),
249
+ groupRemoveMembers: makeGroupRemove(clientFor),
250
+ groupInviteLink: makeGroupInvite(clientFor),
251
+ },
252
+ normalize: normalizeTelegramUser,
253
+ });
254
+ }