@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,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 {
|
|
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
|
-
|
|
15
|
+
const clients = new Map<string, UserClient>();
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
for (const cfg of
|
|
83
|
-
accounts.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
3
|
+
export class TelegramLoginError extends Error {}
|
|
4
4
|
|
|
5
|
-
export interface
|
|
5
|
+
export interface TelegramCredentials {
|
|
6
6
|
apiId: number;
|
|
7
7
|
apiHash: string;
|
|
8
8
|
phone: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export interface
|
|
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
|
|
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
|
-
}):
|
|
51
|
+
}): TelegramCredentials {
|
|
52
52
|
const apiId = Number(raw.apiId);
|
|
53
53
|
if (!Number.isInteger(apiId) || apiId <= 0)
|
|
54
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
87
|
+
export class TelegramLogin {
|
|
88
88
|
private tg: TelegramClient;
|
|
89
89
|
private phoneCodeHash = '';
|
|
90
90
|
private closed = false;
|
|
91
91
|
|
|
92
|
-
constructor(private creds:
|
|
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:
|
|
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
|
|
105
|
+
throw new TelegramLoginError(readableAuthError(err));
|
|
106
106
|
});
|
|
107
107
|
if (!isSentCode(sent))
|
|
108
|
-
throw new
|
|
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<
|
|
115
|
+
async submitCode(raw: string): Promise<TelegramSignedIn | 'password'> {
|
|
116
116
|
const phoneCode = raw.replace(/[^0-9]/g, '');
|
|
117
117
|
if (phoneCode === '')
|
|
118
|
-
throw new
|
|
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
|
|
128
|
+
throw new TelegramLoginError(readableAuthError(err));
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
async submitPassword(password: string): Promise<
|
|
132
|
+
async submitPassword(password: string): Promise<TelegramSignedIn> {
|
|
133
133
|
if (password === '')
|
|
134
|
-
throw new
|
|
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
|
|
141
|
+
throw new TelegramLoginError(readableAuthError(err));
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
private async exportFor(user: UserLike): Promise<
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
if (
|
|
22
|
-
member.is_admin = m.status === 'creator' || m.status === '
|
|
23
|
-
member.is_bot = Boolean(u.
|
|
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
|
-
|
|
28
|
-
|
|
29
|
+
export function restrictedMemberList(reason: string): MemberList {
|
|
30
|
+
return { members: [], capability: { supported: false, complete: false, reason } };
|
|
31
|
+
}
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
):
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
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: [],
|