@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,103 +1,46 @@
1
+ import { TrainError } from '@metro-labs/mcp/train-error';
1
2
  import { errMsg } from '@metro-labs/mcp/log';
2
- import { accounts, loadAccounts, tg, type Account } from './accounts.js';
3
- import { emit } from './wire.js';
4
- import {
5
- emitInbound,
6
- envelope,
7
- reactionCountEnvelope,
8
- reactionEnvelope,
9
- saveMediaAndEmit,
10
- type TgMsg,
11
- type TgReaction,
12
- type TgReactionCount,
13
- } from './format.js';
14
3
  import { readCalls } from '@metro-labs/mcp/trains/protocol';
15
- import { handleCall } from './actions.js';
4
+ import {
5
+ accounts,
6
+ loadAccounts,
7
+ accountFor,
8
+ lineOf,
9
+ targetOf,
10
+ } from './accounts.js';
11
+ import { createClient, type UserClient } from './client.js';
12
+ import { startInbound } from './inbound.js';
13
+ import { makeHandleCall } from './actions.js';
16
14
 
17
- readCalls('telegram', handleCall);
15
+ const clients = new Map<string, UserClient>();
18
16
 
19
- interface Update {
20
- update_id: number;
21
- message?: TgMsg;
22
- message_reaction?: TgReaction;
23
- message_reaction_count?: TgReactionCount;
24
- }
25
-
26
- function handleUpdate(id: string, u: Update): void {
27
- if (u.message && !u.message.from?.is_bot) {
28
- const env = envelope(id, u.message);
29
- emitInbound(emit, id, env);
30
- saveMediaAndEmit(emit, id, u.message, env.id as string);
31
- }
32
- if (u.message_reaction) {
33
- const env = reactionEnvelope(id, u.message_reaction);
34
- if (env) emitInbound(emit, id, env);
35
- }
36
- if (u.message_reaction_count) {
37
- const env = reactionCountEnvelope(id, u.message_reaction_count);
38
- if (env) emitInbound(emit, id, env);
17
+ function clientFor(accountId: string): UserClient {
18
+ let client = clients.get(accountId);
19
+ if (!client) {
20
+ const account = accounts.get(accountId);
21
+ if (!account)
22
+ throw new TrainError('not_implemented', `unknown account '${accountId}'`);
23
+ client = createClient(account);
24
+ clients.set(accountId, client);
39
25
  }
26
+ return client;
40
27
  }
41
28
 
42
- async function runAccount(acct: Account): Promise<void> {
43
- const { id } = acct.cfg;
44
- try {
45
- await tg(id, 'deleteWebhook', { drop_pending_updates: false });
46
- } catch (err) {
47
- process.stderr.write(
48
- `telegram[${id}] deleteWebhook: ${errMsg(err)}\n`,
49
- );
50
- }
29
+ const handleCall = makeHandleCall(clientFor);
51
30
 
52
- for (;;) {
53
- try {
54
- const updates = await tg<Update[]>(
55
- id,
56
- 'getUpdates',
57
- {
58
- offset: acct.offset,
59
- timeout: 25,
60
- allowed_updates: [
61
- 'message',
62
- 'message_reaction',
63
- 'message_reaction_count',
64
- ],
65
- },
66
- 60_000,
67
- );
68
- for (const u of updates) {
69
- acct.offset = u.update_id + 1;
70
- handleUpdate(id, u);
71
- }
72
- } catch (err) {
73
- process.stderr.write(
74
- `telegram[${id}] poll error: ${errMsg(err)}\n`,
75
- );
76
- await new Promise((r) => setTimeout(r, 2_000));
77
- }
78
- }
79
- }
31
+ readCalls('telegram', handleCall);
80
32
 
81
- const cfgs = loadAccounts();
82
- for (const cfg of cfgs) {
83
- accounts.set(cfg.id, {
84
- cfg,
85
- api: `https://api.telegram.org/bot${cfg.token}`,
86
- fileApi: `https://api.telegram.org/file/bot${cfg.token}`,
87
- offset: 0,
88
- });
89
- }
90
- if (accounts.size === 0) {
91
- process.stderr.write('telegram: no accounts booted, exiting\n');
92
- process.exit(2);
33
+ function boot(): void {
34
+ for (const cfg of loadAccounts()) accounts.set(cfg.id, cfg);
35
+ for (const id of accounts.keys())
36
+ startInbound(clientFor(id)).catch((err: unknown) => {
37
+ process.stderr.write(`telegram[${id}] inbound failed: ${errMsg(err)}\n`);
38
+ });
39
+ process.stderr.write(
40
+ `telegram train ready (inbound+outbound) — ${accounts.size} account(s): ${[...accounts.keys()].join(', ')}\n`,
41
+ );
93
42
  }
94
- process.stderr.write(
95
- `telegram train ready (multi) — ${accounts.size} account(s): ${[...accounts.keys()].join(', ')}\n`,
96
- );
97
43
 
98
- for (const acct of accounts.values())
99
- runAccount(acct).catch((err: unknown) => {
100
- process.stderr.write(
101
- `telegram[${acct.cfg.id}] account loop failed: ${errMsg(err)}\n`,
102
- );
103
- });
44
+ boot();
45
+
46
+ export { clients, accounts, clientFor, accountFor, lineOf, targetOf };
@@ -1,19 +1,19 @@
1
1
  import { TelegramClient } from '@mtcute/bun';
2
2
 
3
- export class TelegramUserLoginError extends Error {}
3
+ export class TelegramLoginError extends Error {}
4
4
 
5
- export interface TelegramUserCredentials {
5
+ export interface TelegramCredentials {
6
6
  apiId: number;
7
7
  apiHash: string;
8
8
  phone: string;
9
9
  }
10
10
 
11
- export interface TelegramUserSignedIn {
11
+ export interface TelegramSignedIn {
12
12
  config: { session: string; apiId: number; apiHash: string };
13
13
  identity: { userId: string; displayName: string };
14
14
  }
15
15
 
16
- export type TelegramUserStep = 'code' | 'password';
16
+ export type TelegramStep = 'code' | 'password';
17
17
 
18
18
  const PHONE_RE = /^[0-9]{6,15}$/;
19
19
  const API_HASH_RE = /^[a-f0-9]{32}$/i;
@@ -48,21 +48,21 @@ export function validateCredentials(raw: {
48
48
  apiId: unknown;
49
49
  apiHash: unknown;
50
50
  phone: unknown;
51
- }): TelegramUserCredentials {
51
+ }): TelegramCredentials {
52
52
  const apiId = Number(raw.apiId);
53
53
  if (!Number.isInteger(apiId) || apiId <= 0)
54
- throw new TelegramUserLoginError(
54
+ throw new TelegramLoginError(
55
55
  'api id must be the integer from my.telegram.org',
56
56
  );
57
57
  const apiHash = typeof raw.apiHash === 'string' ? raw.apiHash.trim() : '';
58
58
  if (!API_HASH_RE.test(apiHash))
59
- throw new TelegramUserLoginError(
59
+ throw new TelegramLoginError(
60
60
  'api hash must be the 32-character hash from my.telegram.org',
61
61
  );
62
62
  const phone =
63
63
  typeof raw.phone === 'string' ? raw.phone.replace(/[^0-9]/g, '') : '';
64
64
  if (!PHONE_RE.test(phone))
65
- throw new TelegramUserLoginError(
65
+ throw new TelegramLoginError(
66
66
  'phone must be an international number, digits only',
67
67
  );
68
68
  return { apiId, apiHash, phone };
@@ -84,12 +84,12 @@ interface UserLike {
84
84
  displayName: string;
85
85
  }
86
86
 
87
- export class TelegramUserLogin {
87
+ export class TelegramLogin {
88
88
  private tg: TelegramClient;
89
89
  private phoneCodeHash = '';
90
90
  private closed = false;
91
91
 
92
- constructor(private creds: TelegramUserCredentials) {
92
+ constructor(private creds: TelegramCredentials) {
93
93
  this.tg = new TelegramClient({
94
94
  apiId: creds.apiId,
95
95
  apiHash: creds.apiHash,
@@ -97,25 +97,25 @@ export class TelegramUserLogin {
97
97
  });
98
98
  }
99
99
 
100
- async requestCode(): Promise<{ step: TelegramUserStep; length: number }> {
100
+ async requestCode(): Promise<{ step: TelegramStep; length: number }> {
101
101
  await this.tg.connect();
102
102
  const sent = await this.tg
103
103
  .sendCode({ phone: this.creds.phone })
104
104
  .catch((err: unknown) => {
105
- throw new TelegramUserLoginError(readableAuthError(err));
105
+ throw new TelegramLoginError(readableAuthError(err));
106
106
  });
107
107
  if (!isSentCode(sent))
108
- throw new TelegramUserLoginError(
108
+ throw new TelegramLoginError(
109
109
  'Telegram signed this number in without a code, which Metro cannot use',
110
110
  );
111
111
  this.phoneCodeHash = sent.phoneCodeHash;
112
112
  return { step: 'code', length: sent.length };
113
113
  }
114
114
 
115
- async submitCode(raw: string): Promise<TelegramUserSignedIn | 'password'> {
115
+ async submitCode(raw: string): Promise<TelegramSignedIn | 'password'> {
116
116
  const phoneCode = raw.replace(/[^0-9]/g, '');
117
117
  if (phoneCode === '')
118
- throw new TelegramUserLoginError('enter the login code Telegram sent you');
118
+ throw new TelegramLoginError('enter the login code Telegram sent you');
119
119
  try {
120
120
  const user = await this.tg.signIn({
121
121
  phone: this.creds.phone,
@@ -125,24 +125,24 @@ export class TelegramUserLogin {
125
125
  return await this.exportFor(user);
126
126
  } catch (err) {
127
127
  if (needsPassword(err)) return 'password';
128
- throw new TelegramUserLoginError(readableAuthError(err));
128
+ throw new TelegramLoginError(readableAuthError(err));
129
129
  }
130
130
  }
131
131
 
132
- async submitPassword(password: string): Promise<TelegramUserSignedIn> {
132
+ async submitPassword(password: string): Promise<TelegramSignedIn> {
133
133
  if (password === '')
134
- throw new TelegramUserLoginError(
134
+ throw new TelegramLoginError(
135
135
  'enter your Telegram two-step verification password',
136
136
  );
137
137
  try {
138
138
  const user = await this.tg.checkPassword(password);
139
139
  return await this.exportFor(user);
140
140
  } catch (err) {
141
- throw new TelegramUserLoginError(readableAuthError(err));
141
+ throw new TelegramLoginError(readableAuthError(err));
142
142
  }
143
143
  }
144
144
 
145
- private async exportFor(user: UserLike): Promise<TelegramUserSignedIn> {
145
+ private async exportFor(user: UserLike): Promise<TelegramSignedIn> {
146
146
  const session = await this.tg.exportSession();
147
147
  await this.close();
148
148
  return {
@@ -1,166 +1,80 @@
1
- import { tg, tgForm, targetOf } from './accounts.js';
2
- import { emit, mintId, respond, SELF_URI } from './wire.js';
3
- import { appendFile } from '@metro-labs/mcp/stations/attachments';
1
+ import type { InputMediaLike, Message } from '@mtcute/bun';
2
+ import { InputMedia } from '@mtcute/bun';
3
+ import { isImageMime, isImageExt } from '@metro-labs/mcp/stations/attachments';
4
+ import type { UserClient } from './client.js';
4
5
 
5
- export function emitOutbound(
6
- accountId: string,
7
- line: string,
8
- messageId: string,
9
- text: string,
10
- replyTo?: string,
11
- ): void {
12
- emit({
13
- kind: 'outbound',
14
- id: mintId(),
15
- ts: new Date().toISOString(),
16
- station: 'telegram',
17
- line,
18
- from: SELF_URI,
19
- to: line,
20
- message_id: messageId,
21
- text,
22
- reply_to: replyTo,
23
- ...(replyTo ? { event: { type: 'reply', replyTo } } : {}),
24
- account: accountId,
25
- payload: { account: accountId },
26
- });
6
+ export interface CanonicalAttachment {
7
+ kind?: string;
8
+ path?: string;
9
+ url?: string;
10
+ mime?: string;
11
+ name?: string;
27
12
  }
28
13
 
29
- export function finishSend(
30
- id: string,
31
- accountId: string,
32
- line: string,
33
- messageId: string,
34
- label: string,
35
- replyTo?: string,
36
- extra?: Record<string, unknown>,
37
- ): void {
38
- emitOutbound(accountId, line, messageId, label, replyTo);
39
- respond(id, {
40
- result: { messageId, account: accountId, ...extra },
41
- });
14
+ const srcOf = (att: CanonicalAttachment): string => att.path ?? att.url ?? '';
15
+
16
+ function isImage(att: CanonicalAttachment): boolean {
17
+ if (att.kind === 'image') return true;
18
+ if (att.mime !== undefined) return isImageMime(att.mime);
19
+ const src = srcOf(att);
20
+ return src !== '' && isImageExt(src);
42
21
  }
43
22
 
44
- interface MediaArgs {
45
- line: string;
46
- path: string;
47
- caption?: string;
48
- replyTo?: string;
49
- parseMode?: string;
50
- account?: string;
51
- name?: string;
23
+ function fileRef(att: CanonicalAttachment): string {
24
+ const src = srcOf(att);
25
+ if (src === '' || /^https?:\/\//i.test(src)) return src;
26
+ return src.startsWith('file:') ? src : `file:${src}`;
52
27
  }
53
28
 
54
- export async function sendMedia(
55
- method: string,
56
- fieldName: string,
57
- args: Record<string, unknown>,
58
- ): Promise<{ accountId: string; message_id: number }> {
59
- const {
60
- line,
61
- path,
62
- caption,
63
- replyTo,
64
- parseMode,
65
- account,
66
- name: fileName,
67
- } = args as unknown as MediaArgs;
68
- const { accountId, chatId, topicId } = targetOf(line, account);
69
- const form = new FormData();
70
- form.append('chat_id', String(chatId));
71
- if (topicId !== undefined) form.append('message_thread_id', String(topicId));
72
- if (caption) form.append('caption', caption);
73
- if (parseMode) form.append('parse_mode', parseMode);
74
- if (replyTo)
75
- form.append(
76
- 'reply_parameters',
77
- JSON.stringify({ message_id: Number(replyTo) }),
78
- );
79
- const name = fileName ?? path.split('/').pop() ?? fieldName;
80
- await appendFile(form, fieldName, path, name);
81
- const r = await tgForm<{ message_id: number }>(accountId, method, form);
82
- return { accountId, message_id: r.message_id };
29
+ export interface OutgoingMedia {
30
+ media: InputMediaLike;
31
+ kind: string;
83
32
  }
84
33
 
85
- export async function media(
86
- id: string,
87
- method: string,
88
- field: string,
89
- label: string,
90
- args: Record<string, unknown>,
91
- ): Promise<void> {
92
- const { accountId, message_id } = await sendMedia(method, field, args);
93
- const line = (args as { line: string }).line;
94
- finishSend(
95
- id,
96
- accountId,
97
- line,
98
- String(message_id),
99
- label,
100
- args.replyTo as string | undefined,
101
- );
34
+ export function buildInputMedia(
35
+ att: CanonicalAttachment,
36
+ caption: string | undefined,
37
+ ): OutgoingMedia {
38
+ const file = fileRef(att);
39
+ const params = {
40
+ ...(caption ? { caption } : {}),
41
+ ...(att.name ? { fileName: att.name } : {}),
42
+ };
43
+ return isImage(att)
44
+ ? { media: InputMedia.photo(file, params), kind: 'image' }
45
+ : { media: InputMedia.document(file, params), kind: 'file' };
102
46
  }
103
47
 
104
- export const MEDIA_METHOD_FIELD: Record<
105
- string,
106
- { method: string; field: string }
107
- > = {
108
- image: { method: 'sendPhoto', field: 'photo' },
109
- voice: { method: 'sendVoice', field: 'voice' },
110
- audio: { method: 'sendAudio', field: 'audio' },
111
- video: { method: 'sendVideo', field: 'video' },
112
- document: { method: 'sendDocument', field: 'document' },
113
- };
48
+ interface SendMediaTarget {
49
+ client: UserClient;
50
+ chatId: number;
51
+ replyTo?: number;
52
+ }
114
53
 
115
- export async function sendDice(
116
- id: string,
117
- args: Record<string, unknown>,
118
- ): Promise<void> {
119
- const {
120
- line,
121
- emoji = '\U0001F3B2',
122
- account,
123
- } = args as { line: string; emoji?: string; account?: string };
124
- const { accountId, chatId, topicId } = targetOf(line, account);
125
- const body: Record<string, unknown> = { chat_id: chatId, emoji };
126
- if (topicId !== undefined) body.message_thread_id = topicId;
127
- const r = await tg<{ message_id: number; dice?: { value: number } }>(
128
- accountId,
129
- 'sendDice',
130
- body,
131
- );
132
- finishSend(
133
- id,
134
- accountId,
135
- line,
136
- String(r.message_id),
137
- `[dice ${emoji} = ${r.dice?.value ?? '?'}]`,
138
- undefined,
139
- { value: r.dice?.value },
140
- );
54
+ export interface SentMedia {
55
+ message: Message;
56
+ delivered: string[];
141
57
  }
142
58
 
143
- export async function sendLocation(
144
- id: string,
145
- args: Record<string, unknown>,
146
- ): Promise<void> {
147
- const { line, latitude, longitude, account } = args as {
148
- line: string;
149
- latitude: number;
150
- longitude: number;
151
- account?: string;
152
- };
153
- const { accountId, chatId } = targetOf(line, account);
154
- const r = await tg<{ message_id: number }>(accountId, 'sendLocation', {
155
- chat_id: chatId,
156
- latitude,
157
- longitude,
158
- });
159
- finishSend(
160
- id,
161
- accountId,
162
- line,
163
- String(r.message_id),
164
- `[location: ${latitude}, ${longitude}]`,
165
- );
59
+ export async function sendAttachments(
60
+ target: SendMediaTarget,
61
+ attachments: CanonicalAttachment[],
62
+ text: string,
63
+ ): Promise<SentMedia> {
64
+ const { client, chatId, replyTo } = target;
65
+ const peer = await client.tg.resolvePeer(chatId);
66
+ let last: Message | undefined;
67
+ const delivered: string[] = [];
68
+ for (let i = 0; i < attachments.length; i++) {
69
+ const att = attachments[i];
70
+ if (!att || srcOf(att) === '') continue;
71
+ const caption = i === 0 ? text : undefined;
72
+ const out = buildInputMedia(att, caption);
73
+ last = await client.tg.sendMedia(peer, out.media, {
74
+ ...(replyTo !== undefined ? { replyTo } : {}),
75
+ });
76
+ delivered.push(out.kind);
77
+ }
78
+ if (!last) throw new Error('no attachments were sent');
79
+ return { message: last, delivered };
166
80
  }
@@ -1,47 +1,75 @@
1
1
  import type { MemberList, MetroMember } from '@metro-labs/mcp/stations/types';
2
+ import type { UserClient } from './client.js';
2
3
 
3
- export interface TgUser {
4
- id: number;
5
- username?: string;
6
- first_name?: string;
7
- last_name?: string;
8
- is_bot?: boolean;
9
- }
10
-
11
- export interface TgChatMember {
12
- user: TgUser;
4
+ export interface RawUserMember {
5
+ user: {
6
+ id: number;
7
+ username?: string | null;
8
+ displayName?: string;
9
+ isBot?: boolean;
10
+ };
13
11
  status: string;
12
+ title?: string | null;
14
13
  }
15
14
 
16
- export function mapTgMember(m: TgChatMember): MetroMember {
15
+ const PAGE = 200;
16
+ const HARD_CAP = 2000;
17
+
18
+ export function mapUserMember(m: RawUserMember): MetroMember {
17
19
  const u = m.user;
18
20
  const member: MetroMember = { id: String(u.id) };
19
21
  if (u.username) member.name = u.username;
20
- const display = [u.first_name, u.last_name].filter(Boolean).join(' ');
21
- if (display) member.display_name = display;
22
- member.is_admin = m.status === 'creator' || m.status === 'administrator';
23
- member.is_bot = Boolean(u.is_bot);
22
+ if (u.displayName) member.display_name = u.displayName;
23
+ if (m.title) member.roles = [m.title];
24
+ member.is_admin = m.status === 'creator' || m.status === 'admin';
25
+ member.is_bot = Boolean(u.isBot);
24
26
  return member;
25
27
  }
26
28
 
27
- const BOT_LIMIT_REASON =
28
- 'Telegram Bot API cannot enumerate full membership; returning administrators only.';
29
+ export function restrictedMemberList(reason: string): MemberList {
30
+ return { members: [], capability: { supported: false, complete: false, reason } };
31
+ }
29
32
 
30
- export function adminMemberList(
31
- admins: TgChatMember[],
32
- total: number | null,
33
- ): MemberList {
34
- return {
35
- members: admins.map(mapTgMember),
36
- capability: {
37
- supported: true,
38
- complete: false,
39
- reason: BOT_LIMIT_REASON,
40
- ...(total != null ? { total } : {}),
41
- },
42
- };
33
+ const RESTRICTED_RE =
34
+ /ADMIN|PRIVATE|CHAT_ADMIN_REQUIRED|not (a )?participant|permission|not enough rights/i;
35
+
36
+ export function isRestricted(msg: string): boolean {
37
+ return RESTRICTED_RE.test(msg);
43
38
  }
44
39
 
45
- export function inaccessibleMemberList(reason: string): MemberList {
46
- return { members: [], capability: { supported: false, complete: false, reason } };
40
+ type Chunk = RawUserMember[] & { total?: number };
41
+
42
+ export async function fetchMembers(
43
+ client: UserClient,
44
+ chatId: number,
45
+ limit?: number,
46
+ ): Promise<MemberList> {
47
+ const target = Math.min(Math.max(1, limit ?? PAGE), HARD_CAP);
48
+ const collected: RawUserMember[] = [];
49
+ let total = 0;
50
+ for (let offset = 0; collected.length < target; ) {
51
+ const want = Math.min(PAGE, target - collected.length);
52
+ const getChatMembers = (
53
+ client.tg as unknown as {
54
+ getChatMembers: (
55
+ id: number,
56
+ p: { offset: number; limit: number },
57
+ ) => Promise<Chunk>;
58
+ }
59
+ ).getChatMembers;
60
+ const chunk = await getChatMembers.call(client.tg, chatId, {
61
+ offset,
62
+ limit: want,
63
+ });
64
+ total = chunk.total ?? collected.length + chunk.length;
65
+ if (!chunk.length) break;
66
+ collected.push(...chunk);
67
+ offset += chunk.length;
68
+ if (collected.length >= total || chunk.length < want) break;
69
+ }
70
+ const members = collected.map(mapUserMember);
71
+ return {
72
+ members,
73
+ capability: { supported: true, complete: members.length >= total, total },
74
+ };
47
75
  }
@@ -160,7 +160,7 @@ export function subscribeReactions(client: UserClient): void {
160
160
  handleUpdate(client, update);
161
161
  } catch (e) {
162
162
  process.stderr.write(
163
- `telegram-user[${accountId}] reaction normalize failed: ${errMsg(e)}\n`,
163
+ `telegram[${accountId}] reaction normalize failed: ${errMsg(e)}\n`,
164
164
  );
165
165
  }
166
166
  });
@@ -1,4 +1,4 @@
1
- import type { Station, Verb } from '@metro-labs/mcp/stations/types';
1
+ import type { GroupOp, Station, Verb } from '@metro-labs/mcp/stations/types';
2
2
 
3
3
  export const telegramStation: Station = {
4
4
  name: 'telegram',
@@ -11,6 +11,13 @@ export const telegramStation: Station = {
11
11
  'unreact',
12
12
  'edit',
13
13
  'delete',
14
+ 'read',
15
+ ]),
16
+ groupOps: new Set<GroupOp>([
17
+ 'create_group',
18
+ 'add_members',
19
+ 'remove_members',
20
+ 'invite_link',
14
21
  ]),
15
22
  attachmentMode: 'canonical',
16
23
  tools: [],