@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.
- package/dist/cli.js +18 -12
- package/dist/control.js +68 -12
- package/dist/prompt.js +51 -0
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/package.json +1 -1
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/accounts.ts +10 -10
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/actions.ts +1 -1
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/attachments.ts +1 -1
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/format.ts +7 -7
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group-actions.ts +1 -1
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/index.ts +10 -10
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/station.ts +2 -2
- package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-transcribe.ts +6 -6
- package/runtime/node_modules/@metro-labs/mcp/package.json +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +14 -8
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +2 -4
- package/runtime/node_modules/@metro-labs/mcp/src/db/materialize.ts +49 -27
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +6 -6
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach-interactive.ts +6 -6
- package/runtime/node_modules/@metro-labs/mcp/src/stations/attach.ts +6 -6
- package/runtime/node_modules/@metro-labs/mcp/src/stations/lines.ts +2 -2
- package/runtime/node_modules/@metro-labs/mcp/src/stations/registry.ts +4 -4
- package/runtime/node_modules/@metro-labs/telegram/package.json +5 -2
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +47 -86
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +210 -251
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +66 -107
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/client.ts +4 -4
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +110 -141
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/group.ts +1 -1
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/history.ts +1 -1
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/inbound.ts +6 -6
- package/runtime/node_modules/@metro-labs/telegram/src/index.ts +35 -92
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/login.ts +20 -20
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +65 -151
- package/runtime/node_modules/@metro-labs/telegram/src/members.ts +60 -32
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/reactions.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +8 -1
- package/runtime/node_modules/@metro-labs/telegram/src/types.ts +6 -47
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +1 -3
- package/runtime/node_modules/@metro-labs/{telegram-user → telegram-bot}/package.json +3 -6
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +120 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +295 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +128 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +103 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +166 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +47 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +17 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +48 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +3 -0
- package/runtime/runtime.json +1 -1
- package/runtime/node_modules/@metro-labs/telegram-user/src/accounts.ts +0 -81
- package/runtime/node_modules/@metro-labs/telegram-user/src/actions.ts +0 -254
- package/runtime/node_modules/@metro-labs/telegram-user/src/attachments.ts +0 -87
- package/runtime/node_modules/@metro-labs/telegram-user/src/format.ts +0 -141
- package/runtime/node_modules/@metro-labs/telegram-user/src/index.ts +0 -46
- package/runtime/node_modules/@metro-labs/telegram-user/src/media-actions.ts +0 -80
- package/runtime/node_modules/@metro-labs/telegram-user/src/members.ts +0 -75
- package/runtime/node_modules/@metro-labs/telegram-user/src/station.ts +0 -24
- package/runtime/node_modules/@metro-labs/telegram-user/src/types.ts +0 -7
- package/runtime/node_modules/@metro-labs/telegram-user/src/wire.ts +0 -1
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/api-base.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/group.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/members.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/send-files.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/verify.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice-speak.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/voice.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{discord → discord-bot}/src/wire.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{telegram-user → telegram}/src/normalize.ts +0 -0
- /package/runtime/node_modules/@metro-labs/{telegram → telegram-bot}/src/verify.ts +0 -0
|
@@ -52,20 +52,20 @@ const STATION_TARGETS: Record<StationName, StationTarget> = {
|
|
|
52
52
|
fileEnv: 'XMTP_ACCOUNTS_FILE',
|
|
53
53
|
trainImport: '@metro-labs/xmtp/train',
|
|
54
54
|
},
|
|
55
|
-
telegram: {
|
|
55
|
+
'telegram-bot': {
|
|
56
|
+
file: 'telegram-bot-accounts.json',
|
|
57
|
+
fileEnv: 'TELEGRAM_BOT_ACCOUNTS_FILE',
|
|
58
|
+
trainImport: '@metro-labs/telegram-bot/train',
|
|
59
|
+
},
|
|
60
|
+
'telegram': {
|
|
56
61
|
file: 'telegram-accounts.json',
|
|
57
62
|
fileEnv: 'TELEGRAM_ACCOUNTS_FILE',
|
|
58
63
|
trainImport: '@metro-labs/telegram/train',
|
|
59
64
|
},
|
|
60
|
-
'
|
|
61
|
-
file: '
|
|
62
|
-
fileEnv: '
|
|
63
|
-
trainImport: '@metro-labs/
|
|
64
|
-
},
|
|
65
|
-
discord: {
|
|
66
|
-
file: 'discord-accounts.json',
|
|
67
|
-
fileEnv: 'DISCORD_ACCOUNTS_FILE',
|
|
68
|
-
trainImport: '@metro-labs/discord/train',
|
|
65
|
+
'discord-bot': {
|
|
66
|
+
file: 'discord-bot-accounts.json',
|
|
67
|
+
fileEnv: 'DISCORD_BOT_ACCOUNTS_FILE',
|
|
68
|
+
trainImport: '@metro-labs/discord-bot/train',
|
|
69
69
|
},
|
|
70
70
|
whatsapp: {
|
|
71
71
|
file: 'whatsapp-accounts.json',
|
|
@@ -236,7 +236,12 @@ export const isLocalRuntime = (): boolean =>
|
|
|
236
236
|
export const stationRunsHere = (station: StationName): boolean =>
|
|
237
237
|
isLocalRuntime() || !MOVABLE_STATIONS.has(station);
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
interface WrittenStations {
|
|
240
|
+
active: Map<StationName, number>;
|
|
241
|
+
changed: StationName[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function writeStations(list: LoadedAgent[]): WrittenStations {
|
|
240
245
|
mkdirSync(METRO_DIR, { recursive: true });
|
|
241
246
|
mkdirSync(TRAINS_DIR, { recursive: true });
|
|
242
247
|
|
|
@@ -247,6 +252,13 @@ function writeStations(list: LoadedAgent[]): Map<StationName, number> {
|
|
|
247
252
|
for (const agent of list) {
|
|
248
253
|
names[agent.id] = agent.name;
|
|
249
254
|
for (const a of agent.accounts) {
|
|
255
|
+
if (!(a.station in STATION_TARGETS)) {
|
|
256
|
+
log.error(
|
|
257
|
+
{ station: a.station, id: a.id },
|
|
258
|
+
'unknown station — this metro is older or newer than the row; skipping it',
|
|
259
|
+
);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
250
262
|
map[`${a.station}/${a.id}`] = agent.id;
|
|
251
263
|
if (a.allowlist) allow[`${a.station}/${a.id}`] = a.allowlist;
|
|
252
264
|
if (stationRunsHere(a.station)) {
|
|
@@ -261,29 +273,36 @@ function writeStations(list: LoadedAgent[]): Map<StationName, number> {
|
|
|
261
273
|
carryTokenStores(list.flatMap((agent) => agent.accounts));
|
|
262
274
|
|
|
263
275
|
const active = new Map<StationName, number>();
|
|
276
|
+
const changed: StationName[] = [];
|
|
264
277
|
for (const [station, accts] of byStation) {
|
|
265
278
|
const records = accts.map((a) => ({ id: a.id, ...trainConfig(a.config) }));
|
|
266
|
-
|
|
279
|
+
const path = accountFilePath(station);
|
|
280
|
+
const content = JSON.stringify(records, null, 2);
|
|
281
|
+
if (currentText(path) !== content) {
|
|
282
|
+
writeSecure(path, content);
|
|
283
|
+
changed.push(station);
|
|
284
|
+
}
|
|
267
285
|
const { trainImport } = STATION_TARGETS[station];
|
|
268
286
|
if (trainImport !== null)
|
|
269
287
|
writeIfChanged(trainStubPath(station), `import '${trainImport}';\n`);
|
|
270
288
|
active.set(station, accts.length);
|
|
271
289
|
}
|
|
272
|
-
return active;
|
|
290
|
+
return { active, changed };
|
|
273
291
|
}
|
|
274
292
|
|
|
293
|
+
const BLANKED = '[]\n';
|
|
294
|
+
|
|
275
295
|
function pruneStations(present: Map<StationName, number>): StationName[] {
|
|
276
296
|
const removed: StationName[] = [];
|
|
277
297
|
for (const station of Object.keys(STATION_TARGETS) as StationName[]) {
|
|
278
298
|
if (present.has(station)) continue;
|
|
279
299
|
const hasTrain = STATION_TARGETS[station].trainImport !== null;
|
|
280
300
|
const stub = trainStubPath(station);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
writeSecure(accountFilePath(station), '[]\n');
|
|
301
|
+
const stubGone = !hasTrain || !existsSync(stub);
|
|
302
|
+
const file = currentText(accountFilePath(station));
|
|
303
|
+
const fileInert = file === null || file === BLANKED;
|
|
304
|
+
if (stubGone && fileInert) continue;
|
|
305
|
+
if (!fileInert) writeSecure(accountFilePath(station), BLANKED);
|
|
287
306
|
if (hasTrain) rmSync(stub, { force: true });
|
|
288
307
|
removed.push(station);
|
|
289
308
|
}
|
|
@@ -310,14 +329,15 @@ export function localAgentKey(): string | null {
|
|
|
310
329
|
|
|
311
330
|
async function loadAndWrite(
|
|
312
331
|
source: StationSource,
|
|
313
|
-
): Promise<
|
|
332
|
+
): Promise<WrittenStations & { agents: number }> {
|
|
314
333
|
const list = await source();
|
|
315
334
|
applyKeyMap(list);
|
|
316
|
-
return {
|
|
335
|
+
return { ...writeStations(list), agents: list.length };
|
|
317
336
|
}
|
|
318
337
|
|
|
319
338
|
export async function materializeFrom(source: StationSource): Promise<void> {
|
|
320
339
|
const { active, agents: found } = await loadAndWrite(source);
|
|
340
|
+
|
|
321
341
|
if (found === 0) throw new Error('no agents found — nothing to materialize');
|
|
322
342
|
log.info(
|
|
323
343
|
{ stations: stationLabels(active), agents: found },
|
|
@@ -338,18 +358,20 @@ export async function materializeFromDb(): Promise<void> {
|
|
|
338
358
|
export interface ReloadedStations {
|
|
339
359
|
active: StationName[];
|
|
340
360
|
removed: StationName[];
|
|
361
|
+
changed: StationName[];
|
|
341
362
|
}
|
|
342
363
|
|
|
343
364
|
export async function reloadFrom(
|
|
344
365
|
source: StationSource,
|
|
345
366
|
): Promise<ReloadedStations> {
|
|
346
|
-
const { active } = await loadAndWrite(source);
|
|
367
|
+
const { active, changed } = await loadAndWrite(source);
|
|
347
368
|
const removed = pruneStations(active);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
369
|
+
if (removed.length > 0 || changed.length > 0)
|
|
370
|
+
log.info(
|
|
371
|
+
{ stations: stationLabels(active), removed, changed },
|
|
372
|
+
'reloaded station accounts',
|
|
373
|
+
);
|
|
374
|
+
return { active: [...active.keys()], removed, changed };
|
|
353
375
|
}
|
|
354
376
|
|
|
355
377
|
export async function reloadAccountsFromDb(): Promise<ReloadedStations> {
|
|
@@ -235,7 +235,7 @@ export const COMMON_TOOLS = [
|
|
|
235
235
|
'limit?. The station is derived from the line. Returns {line, station, memberCount, ' +
|
|
236
236
|
'members:[{id, name?, display_name?, address?, roles?, is_admin?, is_bot?}], capability:' +
|
|
237
237
|
'{supported, complete, reason?, total?}}. Each station fills the fields it has (xmtp: id=' +
|
|
238
|
-
'inboxId + address; discord: id + username/nick + roles + is_bot; telegram/telegram
|
|
238
|
+
'inboxId + address; discord-bot: id + username/nick + roles + is_bot; telegram-bot/telegram: ' +
|
|
239
239
|
'id + username/first_name). Never throws on "not supported": stations that cannot ' +
|
|
240
240
|
'enumerate (e.g. the Telegram Bot API) return an empty or partial list with a reason in ' +
|
|
241
241
|
'`capability`. Check `capability.complete` before assuming the roster is exhaustive.',
|
|
@@ -255,9 +255,9 @@ export const COMMON_TOOLS = [
|
|
|
255
255
|
name: 'create_group',
|
|
256
256
|
description:
|
|
257
257
|
'Create a new group/channel on a station and add members, in a station-agnostic shape. ' +
|
|
258
|
-
'Args: station (required, xmtp|telegram
|
|
259
|
-
'(string[]; xmtp: 0x address or inboxId, telegram
|
|
260
|
-
'user id), account? (which station account), parent? (discord only: the metro:// line of ' +
|
|
258
|
+
'Args: station (required, xmtp|telegram|discord-bot|telegram-bot), name (required), members? ' +
|
|
259
|
+
'(string[]; xmtp: 0x address or inboxId, telegram: @username or user id, discord-bot: ' +
|
|
260
|
+
'user id), account? (which station account), parent? (discord-bot only: the metro:// line of ' +
|
|
261
261
|
'the channel to open the thread under). Returns {op, line (the NEW group line), station, ' +
|
|
262
262
|
'supported, reason?, id?, name?, members:[{id, status: added|invited|removed|failed, ' +
|
|
263
263
|
'reason?}], inviteLink?}. Never throws on "not supported": stations that lack the op ' +
|
|
@@ -269,7 +269,7 @@ export const COMMON_TOOLS = [
|
|
|
269
269
|
properties: {
|
|
270
270
|
station: {
|
|
271
271
|
type: 'string',
|
|
272
|
-
description: 'The station to create the group on (xmtp|telegram
|
|
272
|
+
description: 'The station to create the group on (xmtp|telegram|discord-bot).',
|
|
273
273
|
},
|
|
274
274
|
name: { type: 'string', description: 'The group/channel name.' },
|
|
275
275
|
members: {
|
|
@@ -357,7 +357,7 @@ export const COMMON_TOOLS = [
|
|
|
357
357
|
description:
|
|
358
358
|
'Get a join/invite link for a Metro group, for platforms where direct-add is not always ' +
|
|
359
359
|
'permitted. Args: line (required). The station is derived from the line. Returns {op, ' +
|
|
360
|
-
'line, station, supported, reason?, inviteLink?}. Only telegram
|
|
360
|
+
'line, station, supported, reason?, inviteLink?}. Only telegram supports this today; ' +
|
|
361
361
|
'other stations return {supported:false, reason}. Never throws on "not supported".',
|
|
362
362
|
inputSchema: {
|
|
363
363
|
type: 'object',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StationAttachError } from './attach.js';
|
|
2
2
|
|
|
3
|
-
export const INTERACTIVE_STATIONS = ['telegram
|
|
3
|
+
export const INTERACTIVE_STATIONS = ['telegram', 'whatsapp'] as const;
|
|
4
4
|
|
|
5
5
|
export type InteractiveStation = (typeof INTERACTIVE_STATIONS)[number];
|
|
6
6
|
|
|
@@ -61,12 +61,12 @@ async function startTelegramUser(
|
|
|
61
61
|
input: Record<string, unknown>,
|
|
62
62
|
hooks: DriverHooks,
|
|
63
63
|
): Promise<StartedAttach> {
|
|
64
|
-
const {
|
|
65
|
-
await import('@metro-labs/telegram
|
|
66
|
-
const isOwn = (e: unknown): boolean => e instanceof
|
|
64
|
+
const { TelegramLogin, TelegramLoginError, validateCredentials } =
|
|
65
|
+
await import('@metro-labs/telegram/login');
|
|
66
|
+
const isOwn = (e: unknown): boolean => e instanceof TelegramLoginError;
|
|
67
67
|
let login;
|
|
68
68
|
try {
|
|
69
|
-
login = new
|
|
69
|
+
login = new TelegramLogin(
|
|
70
70
|
validateCredentials({
|
|
71
71
|
apiId: input.apiId,
|
|
72
72
|
apiHash: input.apiHash,
|
|
@@ -155,7 +155,7 @@ export async function startInteractiveAttach(
|
|
|
155
155
|
input: Record<string, unknown>,
|
|
156
156
|
hooks: DriverHooks,
|
|
157
157
|
): Promise<StartedAttach> {
|
|
158
|
-
return station === 'telegram
|
|
158
|
+
return station === 'telegram'
|
|
159
159
|
? startTelegramUser(input, hooks)
|
|
160
160
|
: startWhatsapp(input, hooks);
|
|
161
161
|
}
|
|
@@ -2,11 +2,11 @@ import { randomBytes } from 'node:crypto';
|
|
|
2
2
|
import {
|
|
3
3
|
DiscordTokenError,
|
|
4
4
|
verifyDiscordBotToken,
|
|
5
|
-
} from '@metro-labs/discord/verify';
|
|
5
|
+
} from '@metro-labs/discord-bot/verify';
|
|
6
6
|
import {
|
|
7
7
|
TelegramTokenError,
|
|
8
8
|
verifyTelegramBotToken,
|
|
9
|
-
} from '@metro-labs/telegram/verify';
|
|
9
|
+
} from '@metro-labs/telegram-bot/verify';
|
|
10
10
|
import { ApiError } from '../daemon/api-error.js';
|
|
11
11
|
import { publicBaseOrDefault } from '../daemon/attach-serve.js';
|
|
12
12
|
import {
|
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
} from './attach-xmtp.js';
|
|
19
19
|
|
|
20
20
|
export const ATTACHABLE_STATIONS = [
|
|
21
|
-
'discord',
|
|
22
|
-
'telegram',
|
|
21
|
+
'discord-bot',
|
|
22
|
+
'telegram-bot',
|
|
23
23
|
'xmtp',
|
|
24
24
|
'webhook',
|
|
25
25
|
] as const;
|
|
@@ -175,8 +175,8 @@ export async function prepareAccount(
|
|
|
175
175
|
input: AttachInput,
|
|
176
176
|
verify: VerifyXmtpKey = verifyXmtpKeyOutOfProcess,
|
|
177
177
|
): Promise<PreparedAccount> {
|
|
178
|
-
if (input.station === 'discord') return prepareDiscord(input.token);
|
|
179
|
-
if (input.station === 'telegram') return prepareTelegram(input.token);
|
|
178
|
+
if (input.station === 'discord-bot') return prepareDiscord(input.token);
|
|
179
|
+
if (input.station === 'telegram-bot') return prepareTelegram(input.token);
|
|
180
180
|
if (input.station === 'webhook') return prepareWebhook();
|
|
181
181
|
return prepareXmtp(verify);
|
|
182
182
|
}
|
|
@@ -38,7 +38,7 @@ function parseTelegramLine(
|
|
|
38
38
|
line: Line | string,
|
|
39
39
|
): { accountId: string; chatId: number; topicId?: number } | null {
|
|
40
40
|
const p = Line.parse(line);
|
|
41
|
-
if (p?.station !== 'telegram') return null;
|
|
41
|
+
if (p?.station !== 'telegram-bot') return null;
|
|
42
42
|
const { accountId, rest } = splitTelegramAccount(p.path);
|
|
43
43
|
const [chatId, topicId] = rest;
|
|
44
44
|
if (rest.length < 1 || rest.length > 2 || chatId === undefined || !isSignedInt(chatId))
|
|
@@ -76,7 +76,7 @@ export const Line = {
|
|
|
76
76
|
parseXmtp: (line: Line | string) => parseAccountScoped(line, 'xmtp'),
|
|
77
77
|
|
|
78
78
|
parseDiscord: (line: Line | string) =>
|
|
79
|
-
parseAccountScoped(line, 'discord', isSnowflake),
|
|
79
|
+
parseAccountScoped(line, 'discord-bot', isSnowflake),
|
|
80
80
|
|
|
81
81
|
parseTelegram: (line: Line | string) => parseTelegramLine(line),
|
|
82
82
|
};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { Station } from './types.js';
|
|
2
2
|
import { Line } from './lines.js';
|
|
3
3
|
import { xmtpStation } from '@metro-labs/xmtp';
|
|
4
|
+
import { telegramBotStation } from '@metro-labs/telegram-bot';
|
|
4
5
|
import { telegramStation } from '@metro-labs/telegram';
|
|
5
|
-
import {
|
|
6
|
-
import { discordStation } from '@metro-labs/discord';
|
|
6
|
+
import { discordBotStation } from '@metro-labs/discord-bot';
|
|
7
7
|
import { whatsappStation } from '@metro-labs/whatsapp';
|
|
8
8
|
import { webhookStation } from '@metro-labs/webhook';
|
|
9
9
|
|
|
10
10
|
export const STATIONS: readonly Station[] = [
|
|
11
11
|
xmtpStation,
|
|
12
|
+
telegramBotStation,
|
|
12
13
|
telegramStation,
|
|
13
|
-
|
|
14
|
-
discordStation,
|
|
14
|
+
discordBotStation,
|
|
15
15
|
whatsappStation,
|
|
16
16
|
webhookStation,
|
|
17
17
|
];
|
|
@@ -6,20 +6,23 @@
|
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/station.ts",
|
|
8
8
|
"./train": "./src/index.ts",
|
|
9
|
-
"./
|
|
9
|
+
"./login": "./src/login.ts"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"test": "tsc --noEmit && bun test test/"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@metro-labs/mcp": "workspace:*"
|
|
16
|
+
"@metro-labs/mcp": "workspace:*",
|
|
17
|
+
"@mtcute/bun": "0.30.1"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@stage-labs/config": "0.1.0-beta.2",
|
|
21
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
20
22
|
"eslint": "^10.3.0",
|
|
21
23
|
"knip": "^6.15.0",
|
|
22
24
|
"madge": "^8.0.0",
|
|
25
|
+
"qrcode-terminal": "^0.12.0",
|
|
23
26
|
"typescript": "^5"
|
|
24
27
|
}
|
|
25
28
|
}
|
|
@@ -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 {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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<
|
|
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
|
|
25
|
-
const
|
|
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
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
}
|