@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
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { TrainError } from '@metro-labs/mcp/train-error';
|
|
2
|
-
import { errMsg } from '@metro-labs/mcp/log';
|
|
3
|
-
import {
|
|
4
|
-
makeStation,
|
|
5
|
-
respond,
|
|
6
|
-
type CallMsg,
|
|
7
|
-
type StationHandler,
|
|
8
|
-
} from '@metro-labs/mcp/stations/station-runtime';
|
|
9
|
-
import { accountFor, accounts, targetOf } from './accounts.js';
|
|
10
|
-
import type { UserClient } from './client.js';
|
|
11
|
-
import { normalizeTelegramUser } from './normalize.js';
|
|
12
|
-
import {
|
|
13
|
-
sendAttachments,
|
|
14
|
-
type CanonicalAttachment,
|
|
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
|
-
}
|
|
33
|
-
|
|
34
|
-
type ClientFor = (accountId: string) => UserClient;
|
|
35
|
-
|
|
36
|
-
function str(v: unknown): string | undefined {
|
|
37
|
-
return typeof v === 'string' ? v : undefined;
|
|
38
|
-
}
|
|
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
|
-
}
|
|
45
|
-
|
|
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
|
-
}
|
|
58
|
-
|
|
59
|
-
async function guard<T>(run: () => Promise<T>): Promise<T> {
|
|
60
|
-
try {
|
|
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_user_call', msg);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function attachmentsOf(args: Args): CanonicalAttachment[] {
|
|
71
|
-
const raw = args.attachments;
|
|
72
|
-
return Array.isArray(raw) ? (raw as CanonicalAttachment[]) : [];
|
|
73
|
-
}
|
|
74
|
-
|
|
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
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
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
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
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
|
-
}
|
|
132
|
-
|
|
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 }),
|
|
139
|
-
);
|
|
140
|
-
respond(id, { result: { ok: true, account: accountId } });
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
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
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
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
|
-
}
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
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
|
-
});
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
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
|
-
});
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
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
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
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)),
|
|
214
|
-
});
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
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
|
-
}
|
|
234
|
-
|
|
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
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type { Message } from '@mtcute/bun';
|
|
2
|
-
import { FileLocation } from '@mtcute/bun';
|
|
3
|
-
import {
|
|
4
|
-
saveBufferToCache,
|
|
5
|
-
type SavedAttachment,
|
|
6
|
-
} from '@metro-labs/mcp/stations/attachments';
|
|
7
|
-
import type { UserClient } from './client.js';
|
|
8
|
-
|
|
9
|
-
type Media = NonNullable<Message['media']>;
|
|
10
|
-
export type DownloadableMedia = Media & FileLocation;
|
|
11
|
-
|
|
12
|
-
export function isDownloadable(media: Media): media is DownloadableMedia {
|
|
13
|
-
return media instanceof FileLocation;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface MediaMeta {
|
|
17
|
-
mime?: string;
|
|
18
|
-
name?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function fallbackMime(media: DownloadableMedia): string | undefined {
|
|
22
|
-
const type = (media as { type?: unknown }).type;
|
|
23
|
-
if (type === 'photo') return 'image/jpeg';
|
|
24
|
-
if (type === 'sticker') return 'image/webp';
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function metaOf(media: DownloadableMedia): MediaMeta {
|
|
29
|
-
const withMime = media as { mimeType?: unknown };
|
|
30
|
-
const withName = media as { fileName?: unknown };
|
|
31
|
-
const mime =
|
|
32
|
-
(typeof withMime.mimeType === 'string' ? withMime.mimeType : undefined) ??
|
|
33
|
-
fallbackMime(media);
|
|
34
|
-
const name =
|
|
35
|
-
typeof withName.fileName === 'string' ? withName.fileName : undefined;
|
|
36
|
-
return {
|
|
37
|
-
...(mime ? { mime } : {}),
|
|
38
|
-
...(name ? { name } : {}),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface PendingDescriptor {
|
|
43
|
-
kind: string;
|
|
44
|
-
name?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const KIND_BY_TYPE: Record<string, string> = {
|
|
48
|
-
photo: 'image',
|
|
49
|
-
sticker: 'image',
|
|
50
|
-
voice: 'audio',
|
|
51
|
-
audio: 'audio',
|
|
52
|
-
video: 'video',
|
|
53
|
-
video_note: 'video',
|
|
54
|
-
animation: 'video',
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
function mediaType(media: DownloadableMedia): string {
|
|
58
|
-
const type = (media as { type?: unknown }).type;
|
|
59
|
-
return typeof type === 'string' ? type : '';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function kindOf(media: DownloadableMedia, mime: string | undefined): string {
|
|
63
|
-
const byType = KIND_BY_TYPE[mediaType(media)];
|
|
64
|
-
if (byType) return byType;
|
|
65
|
-
if (mime?.startsWith('image/')) return 'image';
|
|
66
|
-
if (mime?.startsWith('audio/')) return 'audio';
|
|
67
|
-
if (mime?.startsWith('video/')) return 'video';
|
|
68
|
-
return 'file';
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function pendingDescriptorOf(media: DownloadableMedia): PendingDescriptor {
|
|
72
|
-
const meta = metaOf(media);
|
|
73
|
-
return {
|
|
74
|
-
kind: kindOf(media, meta.mime),
|
|
75
|
-
...(meta.name ? { name: meta.name } : {}),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function downloadMedia(
|
|
80
|
-
client: UserClient,
|
|
81
|
-
media: DownloadableMedia,
|
|
82
|
-
messageId: string,
|
|
83
|
-
index = 0,
|
|
84
|
-
): Promise<SavedAttachment> {
|
|
85
|
-
const buf = await client.tg.downloadAsBuffer(media);
|
|
86
|
-
return saveBufferToCache(buf, messageId, index, metaOf(media));
|
|
87
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import type { Chat, Message, Peer, User } from '@mtcute/bun';
|
|
2
|
-
import type { SavedAttachment } from '@metro-labs/mcp/stations/attachments';
|
|
3
|
-
import { lineOf } from './accounts.js';
|
|
4
|
-
import { mintId } from './wire.js';
|
|
5
|
-
import { isDownloadable, pendingDescriptorOf } from './attachments.js';
|
|
6
|
-
|
|
7
|
-
const isUser = (peer: Peer): peer is User => peer.type === 'user';
|
|
8
|
-
const isChat = (peer: Peer): peer is Chat => peer.type === 'chat';
|
|
9
|
-
|
|
10
|
-
export function senderName(peer: Peer): string | undefined {
|
|
11
|
-
if (isUser(peer)) return peer.username ? `@${peer.username}` : peer.firstName;
|
|
12
|
-
return peer.title;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function displayName(peer: Peer): string | undefined {
|
|
16
|
-
if (isUser(peer)) return peer.firstName;
|
|
17
|
-
return peer.title;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function chatName(peer: Peer): string | undefined {
|
|
21
|
-
if (isChat(peer)) return peer.title;
|
|
22
|
-
if (isUser(peer)) return peer.username ? `@${peer.username}` : peer.firstName;
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function topicOf(m: Message): number | undefined {
|
|
27
|
-
if (!m.isTopicMessage) return undefined;
|
|
28
|
-
const threadId = m.replyToMessage?.threadId;
|
|
29
|
-
return threadId ?? undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function mediaTag(m: Message): string | undefined {
|
|
33
|
-
if (m.media === null) return undefined;
|
|
34
|
-
return `[${m.media.type}]`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function projectText(m: Message): string {
|
|
38
|
-
const tag = mediaTag(m);
|
|
39
|
-
return [m.text, tag].filter(Boolean).join(' ');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function isOwnEcho(m: Message): boolean {
|
|
43
|
-
if (m.isOutgoing) return true;
|
|
44
|
-
const { sender } = m;
|
|
45
|
-
return isUser(sender) && sender.isSelf;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function envelope(
|
|
49
|
-
accountId: string,
|
|
50
|
-
m: Message,
|
|
51
|
-
): Record<string, unknown> {
|
|
52
|
-
const topicId = topicOf(m);
|
|
53
|
-
const line = lineOf(accountId, m.chat.id, topicId);
|
|
54
|
-
const isPrivate = isUser(m.chat);
|
|
55
|
-
const { media } = m;
|
|
56
|
-
const attachments =
|
|
57
|
-
media !== null && isDownloadable(media)
|
|
58
|
-
? [pendingDescriptorOf(media)]
|
|
59
|
-
: undefined;
|
|
60
|
-
return {
|
|
61
|
-
kind: 'inbound',
|
|
62
|
-
id: mintId(),
|
|
63
|
-
ts: m.date.toISOString(),
|
|
64
|
-
station: 'telegram-user',
|
|
65
|
-
line,
|
|
66
|
-
line_name: chatName(m.chat),
|
|
67
|
-
from: `metro://telegram-user/${accountId}/user/${m.sender.id}`,
|
|
68
|
-
from_name: senderName(m.sender),
|
|
69
|
-
from_display_name: displayName(m.sender),
|
|
70
|
-
message_id: String(m.id),
|
|
71
|
-
text: projectText(m),
|
|
72
|
-
is_private: isPrivate,
|
|
73
|
-
has_media: media !== null,
|
|
74
|
-
payload: {
|
|
75
|
-
account: accountId,
|
|
76
|
-
message_id: String(m.id),
|
|
77
|
-
...(attachments ? { attachments } : {}),
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function attachmentSavedEnvelope(
|
|
83
|
-
accountId: string,
|
|
84
|
-
line: string,
|
|
85
|
-
sourceEnvId: string,
|
|
86
|
-
saved: SavedAttachment,
|
|
87
|
-
index = 0,
|
|
88
|
-
): Record<string, unknown> {
|
|
89
|
-
return {
|
|
90
|
-
kind: 'inbound',
|
|
91
|
-
id: mintId(),
|
|
92
|
-
ts: new Date().toISOString(),
|
|
93
|
-
station: 'telegram-user',
|
|
94
|
-
line,
|
|
95
|
-
from: `metro://telegram-user/${accountId}/self`,
|
|
96
|
-
text: `📎 saved: ${saved.path}`,
|
|
97
|
-
payload: {
|
|
98
|
-
account: accountId,
|
|
99
|
-
contentType: 'attachmentSaved',
|
|
100
|
-
attachmentFor: sourceEnvId,
|
|
101
|
-
index,
|
|
102
|
-
attachmentPath: saved.path,
|
|
103
|
-
localPath: saved.path,
|
|
104
|
-
mime: saved.mime,
|
|
105
|
-
name: saved.name,
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export interface ReactionInput {
|
|
111
|
-
accountId: string;
|
|
112
|
-
chatId: number;
|
|
113
|
-
messageId: number;
|
|
114
|
-
emoji: string;
|
|
115
|
-
senderId: number;
|
|
116
|
-
date: Date;
|
|
117
|
-
isPrivate: boolean;
|
|
118
|
-
senderName?: string;
|
|
119
|
-
removed?: boolean;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function reactionEnvelope(r: ReactionInput): Record<string, unknown> {
|
|
123
|
-
return {
|
|
124
|
-
kind: 'react',
|
|
125
|
-
id: mintId(),
|
|
126
|
-
ts: r.date.toISOString(),
|
|
127
|
-
station: 'telegram-user',
|
|
128
|
-
line: lineOf(r.accountId, r.chatId),
|
|
129
|
-
from: `metro://telegram-user/${r.accountId}/user/${r.senderId}`,
|
|
130
|
-
from_name: r.senderName,
|
|
131
|
-
message_id: String(r.messageId),
|
|
132
|
-
emoji: r.emoji,
|
|
133
|
-
event: { type: 'react', emoji: r.emoji, targetId: String(r.messageId) },
|
|
134
|
-
is_private: r.isPrivate,
|
|
135
|
-
payload: {
|
|
136
|
-
account: r.accountId,
|
|
137
|
-
message_id: String(r.messageId),
|
|
138
|
-
removed: r.removed === true,
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { TrainError } from '@metro-labs/mcp/train-error';
|
|
2
|
-
import { errMsg } from '@metro-labs/mcp/log';
|
|
3
|
-
import { readCalls } from '@metro-labs/mcp/trains/protocol';
|
|
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';
|
|
14
|
-
|
|
15
|
-
const clients = new Map<string, UserClient>();
|
|
16
|
-
|
|
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);
|
|
25
|
-
}
|
|
26
|
-
return client;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const handleCall = makeHandleCall(clientFor);
|
|
30
|
-
|
|
31
|
-
readCalls('telegram-user', handleCall);
|
|
32
|
-
|
|
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-user[${id}] inbound failed: ${errMsg(err)}\n`);
|
|
38
|
-
});
|
|
39
|
-
process.stderr.write(
|
|
40
|
-
`telegram-user train ready (inbound+outbound) — ${accounts.size} account(s): ${[...accounts.keys()].join(', ')}\n`,
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
boot();
|
|
45
|
-
|
|
46
|
-
export { clients, accounts, clientFor, accountFor, lineOf, targetOf };
|
|
@@ -1,80 +0,0 @@
|
|
|
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';
|
|
5
|
-
|
|
6
|
-
export interface CanonicalAttachment {
|
|
7
|
-
kind?: string;
|
|
8
|
-
path?: string;
|
|
9
|
-
url?: string;
|
|
10
|
-
mime?: string;
|
|
11
|
-
name?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
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);
|
|
21
|
-
}
|
|
22
|
-
|
|
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}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface OutgoingMedia {
|
|
30
|
-
media: InputMediaLike;
|
|
31
|
-
kind: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
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' };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface SendMediaTarget {
|
|
49
|
-
client: UserClient;
|
|
50
|
-
chatId: number;
|
|
51
|
-
replyTo?: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface SentMedia {
|
|
55
|
-
message: Message;
|
|
56
|
-
delivered: string[];
|
|
57
|
-
}
|
|
58
|
-
|
|
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 };
|
|
80
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import type { MemberList, MetroMember } from '@metro-labs/mcp/stations/types';
|
|
2
|
-
import type { UserClient } from './client.js';
|
|
3
|
-
|
|
4
|
-
export interface RawUserMember {
|
|
5
|
-
user: {
|
|
6
|
-
id: number;
|
|
7
|
-
username?: string | null;
|
|
8
|
-
displayName?: string;
|
|
9
|
-
isBot?: boolean;
|
|
10
|
-
};
|
|
11
|
-
status: string;
|
|
12
|
-
title?: string | null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const PAGE = 200;
|
|
16
|
-
const HARD_CAP = 2000;
|
|
17
|
-
|
|
18
|
-
export function mapUserMember(m: RawUserMember): MetroMember {
|
|
19
|
-
const u = m.user;
|
|
20
|
-
const member: MetroMember = { id: String(u.id) };
|
|
21
|
-
if (u.username) member.name = u.username;
|
|
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);
|
|
26
|
-
return member;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function restrictedMemberList(reason: string): MemberList {
|
|
30
|
-
return { members: [], capability: { supported: false, complete: false, reason } };
|
|
31
|
-
}
|
|
32
|
-
|
|
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);
|
|
38
|
-
}
|
|
39
|
-
|
|
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
|
-
};
|
|
75
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { GroupOp, Station, Verb } from '@metro-labs/mcp/stations/types';
|
|
2
|
-
|
|
3
|
-
export const telegramUserStation: Station = {
|
|
4
|
-
name: 'telegram-user',
|
|
5
|
-
hasAccounts: true,
|
|
6
|
-
hasTrain: true,
|
|
7
|
-
messageVerbs: new Set<Verb>([
|
|
8
|
-
'send',
|
|
9
|
-
'reply',
|
|
10
|
-
'react',
|
|
11
|
-
'unreact',
|
|
12
|
-
'edit',
|
|
13
|
-
'delete',
|
|
14
|
-
'read',
|
|
15
|
-
]),
|
|
16
|
-
groupOps: new Set<GroupOp>([
|
|
17
|
-
'create_group',
|
|
18
|
-
'add_members',
|
|
19
|
-
'remove_members',
|
|
20
|
-
'invite_link',
|
|
21
|
-
]),
|
|
22
|
-
attachmentMode: 'canonical',
|
|
23
|
-
tools: [],
|
|
24
|
-
};
|