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

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 (68) hide show
  1. package/package.json +1 -1
  2. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/package.json +1 -1
  3. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/accounts.ts +10 -10
  4. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/actions.ts +1 -1
  5. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/attachments.ts +1 -1
  6. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/format.ts +7 -7
  7. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group-actions.ts +1 -1
  8. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/index.ts +10 -10
  9. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/station.ts +2 -2
  10. package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-transcribe.ts +6 -6
  11. package/runtime/node_modules/@metro-labs/mcp/package.json +2 -2
  12. package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +14 -8
  13. package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +17 -10
  14. package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -2
  15. package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +6 -6
  16. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +6 -6
  17. package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +6 -6
  18. package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +2 -2
  19. package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +4 -4
  20. package/runtime/node_modules/@metro-labs/telegram/package.json +5 -2
  21. package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +47 -86
  22. package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +210 -251
  23. package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +66 -107
  24. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/client.ts +4 -4
  25. package/runtime/node_modules/@metro-labs/telegram/src/format.ts +110 -141
  26. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/group.ts +1 -1
  27. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/history.ts +1 -1
  28. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/inbound.ts +6 -6
  29. package/runtime/node_modules/@metro-labs/telegram/src/index.ts +35 -92
  30. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/login.ts +20 -20
  31. package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +65 -151
  32. package/runtime/node_modules/@metro-labs/telegram/src/members.ts +60 -32
  33. package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/reactions.ts +1 -1
  34. package/runtime/node_modules/@metro-labs/telegram/src/station.ts +8 -1
  35. package/runtime/node_modules/@metro-labs/telegram/src/types.ts +6 -47
  36. package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -3
  37. package/runtime/node_modules/@metro-labs/{telegram-user → telegram-bot}/package.json +3 -6
  38. package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
  39. package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
  40. package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
  41. package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
  42. package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
  43. package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
  44. package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
  45. package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
  46. package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
  47. package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
  48. package/runtime/runtime.json +1 -1
  49. package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +0 -81
  50. package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +0 -254
  51. package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +0 -87
  52. package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +0 -141
  53. package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +0 -46
  54. package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +0 -80
  55. package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +0 -75
  56. package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +0 -24
  57. package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +0 -7
  58. package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +0 -1
  59. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/api-base.ts +0 -0
  60. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group.ts +0 -0
  61. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/members.ts +0 -0
  62. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/send-files.ts +0 -0
  63. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/verify.ts +0 -0
  64. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-speak.ts +0 -0
  65. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice.ts +0 -0
  66. /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/wire.ts +0 -0
  67. /package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/normalize.ts +0 -0
  68. /package/runtime/node_modules/@metro-labs/{telegram → telegram-bot}/src/verify.ts +0 -0
@@ -3,102 +3,43 @@ import { join } from 'node:path';
3
3
  import {
4
4
  makeAccountStore,
5
5
  resolveAccountId,
6
+ type Die,
6
7
  } from '@metro-labs/mcp/stations/account-store';
7
- import { Line } from '@metro-labs/mcp/lines';
8
+ import type { UserAccount } from './types.js';
8
9
 
9
10
  const ACCOUNTS_FILE =
10
11
  process.env.TELEGRAM_ACCOUNTS_FILE ??
11
12
  join(homedir(), '.metro', 'telegram-accounts.json');
12
13
 
13
- export interface AccountConfig {
14
- id: string;
15
- token: string;
16
- owner?: string;
14
+ const isSignedInt = (s: string): boolean => /^-?\d+$/.test(s);
15
+ const isTopic = (s: string): boolean => /^\d+$/.test(s);
16
+
17
+ function validateAccount(a: UserAccount, seen: Set<string>, die: Die): void {
18
+ if (!a.id) die('account missing id');
19
+ if (!a.session || typeof a.session !== 'string')
20
+ die(`account '${a.id}' missing session`);
21
+ if (!Number.isInteger(a.apiId) || (a.apiId ?? 0) <= 0)
22
+ die(`account '${a.id}' missing apiId`);
23
+ if (!a.apiHash || typeof a.apiHash !== 'string')
24
+ die(`account '${a.id}' missing apiHash`);
25
+ if (seen.has(a.id)) die(`duplicate account id '${a.id}'`);
26
+ seen.add(a.id);
17
27
  }
18
28
 
19
- export const { loadAccounts } = makeAccountStore<AccountConfig>({
29
+ export const { loadAccounts } = makeAccountStore<UserAccount>({
20
30
  prefix: 'telegram',
21
31
  file: ACCOUNTS_FILE,
22
32
  allowlistEnv: ['TELEGRAM_ONLY_ACCOUNTS', 'TELEGRAM_ACCOUNTS'],
23
33
  validate(raw, die) {
24
- const seenId = new Set<string>();
25
- const seenTok = new Set<string>();
26
- for (const a of raw) {
27
- if (!a.id) die('account missing id');
28
- if (!a.token || typeof a.token !== 'string')
29
- die(`account '${a.id}' missing token`);
30
- if (seenId.has(a.id)) die(`duplicate account id '${a.id}'`);
31
- if (seenTok.has(a.token))
32
- die(
33
- `account '${a.id}' reuses a token used by another account (409 on getUpdates)`,
34
- );
35
- seenId.add(a.id);
36
- seenTok.add(a.token);
37
- }
34
+ const seen = new Set<string>();
35
+ for (const a of raw) validateAccount(a, seen, die);
38
36
  },
39
37
  });
40
38
 
41
- export interface Account {
42
- cfg: AccountConfig;
43
- api: string;
44
- fileApi: string;
45
- offset: number;
46
- }
47
- export const accounts = new Map<string, Account>();
48
-
49
- export async function tg<T>(
50
- accountId: string,
51
- method: string,
52
- body: unknown,
53
- timeoutMs = 30_000,
54
- ): Promise<T> {
55
- const acct = accounts.get(accountId);
56
- if (!acct) throw new Error(`unknown account '${accountId}'`);
57
- const res = await fetch(`${acct.api}/${method}`, {
58
- method: 'POST',
59
- headers: { 'Content-Type': 'application/json' },
60
- body: JSON.stringify(body),
61
- signal: AbortSignal.timeout(timeoutMs),
62
- });
63
- const json = (await res.json()) as {
64
- ok: boolean;
65
- description?: string;
66
- result?: T;
67
- };
68
- if (!json.ok)
69
- throw new Error(`telegram ${method}: ${json.description ?? 'unknown'}`);
70
- return json.result as T;
71
- }
72
-
73
- export async function tgForm<T>(
74
- accountId: string,
75
- method: string,
76
- form: FormData,
77
- timeoutMs = 60_000,
78
- ): Promise<T> {
79
- const acct = accounts.get(accountId);
80
- if (!acct) throw new Error(`unknown account '${accountId}'`);
81
- const res = await fetch(`${acct.api}/${method}`, {
82
- method: 'POST',
83
- body: form,
84
- signal: AbortSignal.timeout(timeoutMs),
85
- });
86
- const json = (await res.json()) as {
87
- ok: boolean;
88
- description?: string;
89
- result?: T;
90
- };
91
- if (!json.ok)
92
- throw new Error(`telegram ${method}: ${json.description ?? 'unknown'}`);
93
- return json.result as T;
94
- }
39
+ export const accounts = new Map<string, UserAccount>();
95
40
 
96
41
  export function accountFor(args: { account?: string; line?: string }): string {
97
- return resolveAccountId(
98
- accounts,
99
- args,
100
- (line) => Line.parseTelegram(line)?.accountId,
101
- );
42
+ return resolveAccountId(accounts, args, (line) => targetOf(line)?.accountId);
102
43
  }
103
44
 
104
45
  export function lineOf(
@@ -110,11 +51,31 @@ export function lineOf(
110
51
  return `metro://telegram/${accountId}/${tail}`;
111
52
  }
112
53
 
113
- export function targetOf(
114
- line: string,
115
- accountOverride?: string,
116
- ): { accountId: string; chatId: number; topicId?: number } {
117
- const parsed = Line.parseTelegram(line);
118
- if (!parsed) throw new Error(`bad telegram line: ${line}`);
119
- return { ...parsed, accountId: accountOverride ?? parsed.accountId };
54
+ interface Target {
55
+ accountId: string;
56
+ chatId: number;
57
+ topicId?: number;
58
+ }
59
+
60
+ function splitScoped(path: string[]): { accountId: string; rest: string[] } {
61
+ const first = path[0];
62
+ if (path.length >= 2 && first !== undefined && !isSignedInt(first))
63
+ return { accountId: first, rest: path.slice(1) };
64
+ return { accountId: 'default', rest: path };
65
+ }
66
+
67
+ export function targetOf(line: string): Target | undefined {
68
+ const prefix = 'metro://telegram/';
69
+ if (!line.startsWith(prefix)) return undefined;
70
+ const path = line.slice(prefix.length).split('/').filter(Boolean);
71
+ const { accountId, rest } = splitScoped(path);
72
+ const [chatId, topicId] = rest;
73
+ if (rest.length < 1 || rest.length > 2 || chatId === undefined) return undefined;
74
+ if (!isSignedInt(chatId)) return undefined;
75
+ if (topicId !== undefined && !isTopic(topicId)) return undefined;
76
+ return {
77
+ accountId,
78
+ chatId: Number(chatId),
79
+ ...(topicId !== undefined ? { topicId: Number(topicId) } : {}),
80
+ };
120
81
  }
@@ -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
+ }