@stage-labs/metro 0.1.0-beta.170 → 0.1.0-beta.172
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 +7 -10
- package/dist/route.js +16 -8
- package/dist/serve.js +1 -1
- package/dist/service.js +1 -1
- package/package.json +1 -1
- package/runtime/marketplace/.claude-plugin/marketplace.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +2 -3
- package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +14 -7
- package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +3 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +41 -3
- package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +11 -2
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +30 -6
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +12 -8
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +4 -4
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +18 -7
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +6 -8
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +5 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +18 -7
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +4 -6
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-relay.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-inline.ts +29 -1
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +4 -9
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +1 -144
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +14 -5
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +19 -0
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +2 -1
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +1 -45
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +13 -5
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +1 -73
- package/runtime/node_modules/@metro-labs/xmtp/package.json +0 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +2 -173
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +2 -41
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +1 -129
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-core.ts +13 -5
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +1 -10
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +3 -4
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +1 -19
- package/runtime/runtime.json +1 -1
- package/runtime/stations.json +0 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-push.ts +0 -87
- package/runtime/node_modules/@metro-labs/xmtp/src/push.ts +0 -349
|
@@ -5,10 +5,10 @@ import { isRecord } from '@metro-labs/core/is-record';
|
|
|
5
5
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
6
6
|
import { beginLogin, finishLogin, readCodexCliAuth, type CodexTokens } from './codex-auth.js';
|
|
7
7
|
import { beginDeviceLogin, pollDeviceLogin } from './codex-device.js';
|
|
8
|
-
import { codexModels, currentTokens,
|
|
8
|
+
import { codexModels, currentTokens, sharedCodexState } from './codex.js';
|
|
9
9
|
import { beginLogin as beginGeminiLogin, exchangeCode as exchangeGeminiCode, userEmail, type GeminiTokens } from './gemini-auth.js';
|
|
10
10
|
import { onboard, parseGeminiProject } from './gemini-setup.js';
|
|
11
|
-
import { currentGeminiTokens,
|
|
11
|
+
import { currentGeminiTokens, listGeminiModels, sharedGeminiState, type GeminiDeps } from './gemini.js';
|
|
12
12
|
import { openrouterCredits } from './openrouter.js';
|
|
13
13
|
import { lastServed } from './served.js';
|
|
14
14
|
import { geminiUsage, noteUsage, openrouterUsage, usageOf } from './usage.js';
|
|
@@ -23,8 +23,6 @@ import {
|
|
|
23
23
|
} from './model-config.js';
|
|
24
24
|
import { asApiError, askedConnection, BODY_MAX, connectionFor, CREDITS_TTL_MS, settingsBody, type ModelApiDeps, type Route, type Store } from './model-store.js';
|
|
25
25
|
|
|
26
|
-
const codexState = freshCodexState();
|
|
27
|
-
const geminiState = freshGeminiState();
|
|
28
26
|
|
|
29
27
|
function connectionToFill(store: Store, req: IncomingMessage, provider: Provider): { cfg: ModelConfig; id: string } {
|
|
30
28
|
const asked = askedConnection(req);
|
|
@@ -100,7 +98,7 @@ export const CODEX_ROUTES: Record<string, Route> = {
|
|
|
100
98
|
const conn = connectionFor(store.read(), req, 'codex');
|
|
101
99
|
if (conn.codex === null) throw new ApiError('this connection is not signed in yet', 400);
|
|
102
100
|
const codexDeps = codexDepsFor(deps, store, conn.id);
|
|
103
|
-
const auth = await currentTokens(conn, codexDeps,
|
|
101
|
+
const auth = await currentTokens(conn, codexDeps, sharedCodexState).catch(asApiError);
|
|
104
102
|
return { models: await codexModels(auth, codexDeps).catch(asApiError) };
|
|
105
103
|
},
|
|
106
104
|
},
|
|
@@ -117,7 +115,7 @@ export const geminiDeps = (deps: ModelApiDeps, store: Store): GeminiDeps => ({
|
|
|
117
115
|
|
|
118
116
|
async function geminiModelsOf(conn: Connection, deps: ModelApiDeps, store: Store): Promise<Awaited<ReturnType<typeof listGeminiModels>>> {
|
|
119
117
|
if (conn.gemini === null) throw new ApiError('this connection is not signed in yet', 400);
|
|
120
|
-
const tokens = await currentGeminiTokens(conn, geminiDeps(deps, store),
|
|
118
|
+
const tokens = await currentGeminiTokens(conn, geminiDeps(deps, store), sharedGeminiState);
|
|
121
119
|
return listGeminiModels(tokens, geminiDeps(deps, store));
|
|
122
120
|
}
|
|
123
121
|
|
|
@@ -55,7 +55,7 @@ export function registerPermissionRelay(deps: PermissionRelayDeps): void {
|
|
|
55
55
|
);
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
-
relay.registerPermission(params.request_id);
|
|
58
|
+
relay.registerPermission(params.request_id, line);
|
|
59
59
|
try {
|
|
60
60
|
await metroSend(line, promptBody(params));
|
|
61
61
|
} catch (e) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdtemp, open, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { assertAttachmentSize } from '@metro-labs/core/stations/attachments';
|
|
2
3
|
import { tmpdir } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
|
|
@@ -98,3 +99,30 @@ export async function writeInlineTemp(
|
|
|
98
99
|
|
|
99
100
|
export const removeInlineTemp = (dir: string): Promise<void> =>
|
|
100
101
|
rm(dir, { recursive: true, force: true });
|
|
102
|
+
|
|
103
|
+
export async function streamToTemp(
|
|
104
|
+
body: ReadableStream<Uint8Array>,
|
|
105
|
+
name: string | undefined,
|
|
106
|
+
): Promise<InlineTemp & { bytes: number }> {
|
|
107
|
+
const dir = await mkdtemp(join(tmpdir(), INLINE_TEMP_PREFIX));
|
|
108
|
+
const path = join(dir, safeFileName(name));
|
|
109
|
+
const handle = await open(path, 'wx', 0o600);
|
|
110
|
+
const reader = body.getReader();
|
|
111
|
+
let bytes = 0;
|
|
112
|
+
try {
|
|
113
|
+
for (;;) {
|
|
114
|
+
const { done, value } = await reader.read();
|
|
115
|
+
if (done) break;
|
|
116
|
+
bytes += value.length;
|
|
117
|
+
assertAttachmentSize(bytes);
|
|
118
|
+
await handle.write(value);
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
await reader.cancel().catch(() => undefined);
|
|
122
|
+
await handle.close();
|
|
123
|
+
await removeInlineTemp(dir);
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
await handle.close();
|
|
127
|
+
return { dir, path, bytes };
|
|
128
|
+
}
|
|
@@ -2,13 +2,13 @@ import {
|
|
|
2
2
|
assertAttachmentSize,
|
|
3
3
|
assertContentLength,
|
|
4
4
|
guessMime,
|
|
5
|
-
saveBufferToCache,
|
|
6
5
|
} from '@metro-labs/core/stations/attachments';
|
|
7
6
|
import {
|
|
8
7
|
assertInlineTotal,
|
|
9
8
|
decodeInline,
|
|
10
9
|
removeInlineTemp,
|
|
11
10
|
splitInlineData,
|
|
11
|
+
streamToTemp,
|
|
12
12
|
writeInlineTemp,
|
|
13
13
|
} from './attach-inline.js';
|
|
14
14
|
import { realpathSync } from 'node:fs';
|
|
@@ -39,8 +39,6 @@ const basenameOf = (src: string): string =>
|
|
|
39
39
|
|
|
40
40
|
const isHttpUrl = (src: string): boolean => /^https?:\/\//i.test(src);
|
|
41
41
|
|
|
42
|
-
const mintId = (): string => Math.random().toString(36).slice(2, 12);
|
|
43
|
-
|
|
44
42
|
const HIDDEN_SEGMENT = /(^|\/)\.[^/]/;
|
|
45
43
|
|
|
46
44
|
function assertReadablePath(path: string): void {
|
|
@@ -93,15 +91,12 @@ async function fromUrl(
|
|
|
93
91
|
if (!res.ok)
|
|
94
92
|
throw new Error(`attachment fetch failed ${res.status} for '${url}'`);
|
|
95
93
|
assertContentLength(res.headers.get('content-length'));
|
|
96
|
-
const data = new Uint8Array(await res.arrayBuffer());
|
|
97
94
|
const mime =
|
|
98
95
|
a.mime ?? res.headers.get('content-type')?.split(';')[0] ?? guessMime(url);
|
|
99
96
|
const name = a.name ?? basenameOf(url) ?? 'attachment';
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
104
|
-
return { path: saved.path, mime, name, bytes: data.length };
|
|
97
|
+
if (res.body === null) throw new Error(`attachment fetch returned no body for '${url}'`);
|
|
98
|
+
const { dir, path, bytes } = await streamToTemp(res.body, name);
|
|
99
|
+
return { path, mime, name, bytes, temp: dir };
|
|
105
100
|
}
|
|
106
101
|
|
|
107
102
|
async function fromData(
|
|
@@ -1,20 +1,13 @@
|
|
|
1
|
+
import type { Message } from 'discord.js';
|
|
1
2
|
import {
|
|
2
|
-
ActivityType,
|
|
3
|
-
type Message,
|
|
4
|
-
type PresenceStatusData,
|
|
5
|
-
} from 'discord.js';
|
|
6
|
-
import {
|
|
7
|
-
accountFor,
|
|
8
3
|
accounts,
|
|
9
4
|
encodeEmoji,
|
|
10
|
-
lineOf,
|
|
11
5
|
rest,
|
|
12
6
|
routeOf,
|
|
13
7
|
} from './accounts.js';
|
|
14
8
|
import { emitOutbound, emitOutboundEdit, emitOutboundReact } from './format.js';
|
|
15
9
|
import { respond } from './wire.js';
|
|
16
10
|
import { normalizeDiscord } from '@metro-labs/core/stations/messaging-normalize';
|
|
17
|
-
import { assertContentLength } from '@metro-labs/core/stations/attachments';
|
|
18
11
|
import {
|
|
19
12
|
appendFiles,
|
|
20
13
|
outgoingFiles,
|
|
@@ -111,38 +104,6 @@ async function send(id: string, args: Record<string, unknown>): Promise<void> {
|
|
|
111
104
|
});
|
|
112
105
|
}
|
|
113
106
|
|
|
114
|
-
function presence(id: string, args: Record<string, unknown>): void {
|
|
115
|
-
const {
|
|
116
|
-
text,
|
|
117
|
-
status = 'online',
|
|
118
|
-
account,
|
|
119
|
-
} = args as {
|
|
120
|
-
text?: string;
|
|
121
|
-
status?: PresenceStatusData;
|
|
122
|
-
account?: string;
|
|
123
|
-
};
|
|
124
|
-
const accountId = accountFor({ account });
|
|
125
|
-
const acct = accounts.get(accountId);
|
|
126
|
-
if (!acct) {
|
|
127
|
-
respond(id, { error: `unknown account '${accountId}'` });
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const client = acct.client;
|
|
131
|
-
if (!client.user) {
|
|
132
|
-
respond(id, { error: `gateway not ready for account '${accountId}'` });
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
client.user.setPresence({
|
|
136
|
-
status,
|
|
137
|
-
activities: text
|
|
138
|
-
? [{ name: 'Custom Status', type: ActivityType.Custom, state: text }]
|
|
139
|
-
: [],
|
|
140
|
-
});
|
|
141
|
-
respond(id, {
|
|
142
|
-
result: { ok: true, text: text ?? null, status, account: accountId },
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
107
|
const profileUrl = (userId?: string): string | null =>
|
|
147
108
|
userId === undefined ? null : `https://discord.com/users/${userId}`;
|
|
148
109
|
|
|
@@ -245,104 +206,6 @@ async function fetchMessages(
|
|
|
245
206
|
respond(id, { result: { messages: msgs, account: accountId } });
|
|
246
207
|
}
|
|
247
208
|
|
|
248
|
-
async function download(
|
|
249
|
-
id: string,
|
|
250
|
-
args: Record<string, unknown>,
|
|
251
|
-
): Promise<void> {
|
|
252
|
-
const {
|
|
253
|
-
line,
|
|
254
|
-
messageId,
|
|
255
|
-
outDir = '/tmp',
|
|
256
|
-
account,
|
|
257
|
-
} = args as {
|
|
258
|
-
line: string;
|
|
259
|
-
messageId: string;
|
|
260
|
-
outDir?: string;
|
|
261
|
-
account?: string;
|
|
262
|
-
};
|
|
263
|
-
const { accountId, channelId } = routeOf(line, account);
|
|
264
|
-
const msg = await rest<{
|
|
265
|
-
attachments: { url: string; content_type?: string; filename: string }[];
|
|
266
|
-
}>(accountId, 'GET', `/channels/${channelId}/messages/${messageId}`);
|
|
267
|
-
const files: { path: string; mediaType: string }[] = [];
|
|
268
|
-
for (const att of msg.attachments) {
|
|
269
|
-
const res = await fetch(att.url, { signal: AbortSignal.timeout(60_000) });
|
|
270
|
-
if (!res.ok)
|
|
271
|
-
throw new Error(`discord-bot download ${res.status} for ${att.url}`);
|
|
272
|
-
assertContentLength(res.headers.get('content-length'));
|
|
273
|
-
const buf = await res.arrayBuffer();
|
|
274
|
-
const path = `${outDir}/${messageId}-${att.filename}`;
|
|
275
|
-
await Bun.write(path, buf);
|
|
276
|
-
files.push({
|
|
277
|
-
path,
|
|
278
|
-
mediaType: att.content_type ?? 'application/octet-stream',
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
respond(id, { result: { files, account: accountId } });
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async function threadCreate(
|
|
285
|
-
id: string,
|
|
286
|
-
args: Record<string, unknown>,
|
|
287
|
-
): Promise<void> {
|
|
288
|
-
const {
|
|
289
|
-
line,
|
|
290
|
-
messageId,
|
|
291
|
-
name,
|
|
292
|
-
autoArchiveDuration = 1440,
|
|
293
|
-
account,
|
|
294
|
-
} = args as {
|
|
295
|
-
line: string;
|
|
296
|
-
messageId?: string;
|
|
297
|
-
name: string;
|
|
298
|
-
autoArchiveDuration?: number;
|
|
299
|
-
account?: string;
|
|
300
|
-
};
|
|
301
|
-
const { accountId, channelId } = routeOf(line, account);
|
|
302
|
-
const path = messageId
|
|
303
|
-
? `/channels/${channelId}/messages/${messageId}/threads`
|
|
304
|
-
: `/channels/${channelId}/threads`;
|
|
305
|
-
const res = await rest<{ id: string }>(accountId, 'POST', path, {
|
|
306
|
-
name,
|
|
307
|
-
auto_archive_duration: autoArchiveDuration,
|
|
308
|
-
});
|
|
309
|
-
respond(id, {
|
|
310
|
-
result: {
|
|
311
|
-
threadId: res.id,
|
|
312
|
-
line: lineOf(accountId, res.id),
|
|
313
|
-
account: accountId,
|
|
314
|
-
},
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async function pin(id: string, args: Record<string, unknown>): Promise<void> {
|
|
319
|
-
const { line, messageId, account } = args as {
|
|
320
|
-
line: string;
|
|
321
|
-
messageId: string;
|
|
322
|
-
account?: string;
|
|
323
|
-
};
|
|
324
|
-
const { accountId, channelId } = routeOf(line, account);
|
|
325
|
-
await rest(accountId, 'PUT', `/channels/${channelId}/pins/${messageId}`);
|
|
326
|
-
respond(id, { result: { ok: true, account: accountId } });
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async function typing(id: string, args: Record<string, unknown>): Promise<void> {
|
|
330
|
-
const { line, account } = args as { line: string; account?: string };
|
|
331
|
-
const { accountId, channelId } = routeOf(line, account);
|
|
332
|
-
await rest(accountId, 'POST', `/channels/${channelId}/typing`);
|
|
333
|
-
respond(id, { result: { ok: true, account: accountId } });
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async function channel(
|
|
337
|
-
id: string,
|
|
338
|
-
args: Record<string, unknown>,
|
|
339
|
-
): Promise<void> {
|
|
340
|
-
const { line, account } = args as { line: string; account?: string };
|
|
341
|
-
const { accountId, channelId } = routeOf(line, account);
|
|
342
|
-
const res = await rest(accountId, 'GET', `/channels/${channelId}`);
|
|
343
|
-
respond(id, { result: res });
|
|
344
|
-
}
|
|
345
|
-
|
|
346
209
|
async function listMembers(
|
|
347
210
|
id: string,
|
|
348
211
|
args: Record<string, unknown>,
|
|
@@ -369,13 +232,7 @@ const HANDLERS: Record<string, StationHandler> = {
|
|
|
369
232
|
edit,
|
|
370
233
|
delete: remove,
|
|
371
234
|
fetch: fetchMessages,
|
|
372
|
-
download,
|
|
373
|
-
thread_create: threadCreate,
|
|
374
|
-
pin,
|
|
375
|
-
typing,
|
|
376
|
-
channel,
|
|
377
235
|
listMembers,
|
|
378
|
-
set_presence: presence,
|
|
379
236
|
set_profile: setProfile,
|
|
380
237
|
profile: readProfile,
|
|
381
238
|
joinVoice,
|
|
@@ -44,11 +44,20 @@ function emitAttachmentSaved(
|
|
|
44
44
|
},
|
|
45
45
|
});
|
|
46
46
|
})
|
|
47
|
-
.catch((err: unknown) =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
.catch((err: unknown) => {
|
|
48
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
49
|
+
process.stderr.write(`discord-bot attachment save failed: ${reason}\n`);
|
|
50
|
+
emitInbound(accountId, {
|
|
51
|
+
kind: 'inbound',
|
|
52
|
+
id: mintId(),
|
|
53
|
+
ts: new Date().toISOString(),
|
|
54
|
+
station: 'discord-bot',
|
|
55
|
+
line,
|
|
56
|
+
from: SELF_URI,
|
|
57
|
+
text: `📎 not fetched: ${reason}`,
|
|
58
|
+
payload: { contentType: 'attachmentFailed', attachmentFor: sourceMsgId, index, name: ref.name ?? undefined, mime: ref.contentType ?? undefined, reason },
|
|
59
|
+
});
|
|
60
|
+
});
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
const AV_TAG: Record<string, string> = { audio: 'audio', video: 'video' };
|
|
@@ -123,6 +123,25 @@ export function attachmentSavedEnvelope(
|
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
export function attachmentFailedEnvelope(
|
|
127
|
+
accountId: string,
|
|
128
|
+
line: string,
|
|
129
|
+
sourceEnvId: string,
|
|
130
|
+
reason: string,
|
|
131
|
+
index = 0,
|
|
132
|
+
): Record<string, unknown> {
|
|
133
|
+
return {
|
|
134
|
+
kind: 'inbound',
|
|
135
|
+
id: mintId(),
|
|
136
|
+
ts: new Date().toISOString(),
|
|
137
|
+
station: 'telegram',
|
|
138
|
+
line,
|
|
139
|
+
from: `metro://telegram/${accountId}/self`,
|
|
140
|
+
text: `📎 not fetched: ${reason}`,
|
|
141
|
+
payload: { account: accountId, contentType: 'attachmentFailed', attachmentFor: sourceEnvId, index, reason },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
126
145
|
export interface ReactionInput {
|
|
127
146
|
accountId: string;
|
|
128
147
|
chatId: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Message } from '@mtcute/bun';
|
|
2
2
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
3
|
import { emit } from './wire.js';
|
|
4
|
-
import { envelope, isOwnEcho, attachmentSavedEnvelope } from './format.js';
|
|
4
|
+
import { envelope, isOwnEcho, attachmentFailedEnvelope, attachmentSavedEnvelope } from './format.js';
|
|
5
5
|
import { downloadMedia, isDownloadable } from './attachments.js';
|
|
6
6
|
import { subscribeReactions } from './reactions.js';
|
|
7
7
|
import type { UserClient } from './client.js';
|
|
@@ -28,6 +28,7 @@ async function saveMediaAndEmit(
|
|
|
28
28
|
process.stderr.write(
|
|
29
29
|
`telegram[${accountId}] media save failed: ${errMsg(e)}\n`,
|
|
30
30
|
);
|
|
31
|
+
emit(attachmentFailedEnvelope(accountId, env.line as string, env.id as string, errMsg(e)));
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { accounts, tg, targetOf } from './accounts.js';
|
|
2
2
|
import { respond } from './wire.js';
|
|
3
3
|
import { errMsg } from '@metro-labs/core/log';
|
|
4
4
|
import { normalizeTelegram } from '@metro-labs/core/stations/messaging-normalize';
|
|
@@ -13,21 +13,14 @@ import {
|
|
|
13
13
|
type StationHandler,
|
|
14
14
|
} from '@metro-labs/core/stations/station-runtime';
|
|
15
15
|
import { mediaKindOf } from './attachments.js';
|
|
16
|
-
import { assertContentLength } from '@metro-labs/core/stations/attachments';
|
|
17
16
|
import {
|
|
18
17
|
emitOutbound,
|
|
19
18
|
finishSend,
|
|
20
|
-
media,
|
|
21
19
|
MEDIA_METHOD_FIELD,
|
|
22
|
-
sendDice,
|
|
23
|
-
sendLocation,
|
|
24
20
|
sendMedia,
|
|
25
21
|
} from './media-actions.js';
|
|
26
22
|
import { readProfile, setProfile } from './profile.js';
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
24
|
export type { CallMsg };
|
|
32
25
|
|
|
33
26
|
const meCache = new Map<string, { id: number; username: string | null }>();
|
|
@@ -211,34 +204,6 @@ async function remove(id: string, args: Record<string, unknown>): Promise<void>
|
|
|
211
204
|
respond(id, { result: { ok: true, account: accountId } });
|
|
212
205
|
}
|
|
213
206
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
async function download(id: string, args: Record<string, unknown>): Promise<void> {
|
|
217
|
-
const {
|
|
218
|
-
fileId,
|
|
219
|
-
outDir = '/tmp',
|
|
220
|
-
account,
|
|
221
|
-
} = args as { fileId: string; outDir?: string; account?: string };
|
|
222
|
-
const accountId = accountFor({ account });
|
|
223
|
-
const acct = accounts.get(accountId);
|
|
224
|
-
if (!acct) throw new Error(`unknown account '${accountId}'`);
|
|
225
|
-
const meta = await tg<{ file_path: string }>(accountId, 'getFile', {
|
|
226
|
-
file_id: fileId,
|
|
227
|
-
});
|
|
228
|
-
const res = await fetch(`${acct.fileApi}/${meta.file_path}`, {
|
|
229
|
-
signal: AbortSignal.timeout(60_000),
|
|
230
|
-
});
|
|
231
|
-
if (!res.ok) throw new Error(`telegram-bot download ${res.status}`);
|
|
232
|
-
assertContentLength(res.headers.get('content-length'));
|
|
233
|
-
const data = await res.arrayBuffer();
|
|
234
|
-
const filename = meta.file_path.split('/').pop() ?? `${fileId}.bin`;
|
|
235
|
-
const path = `${outDir}/${Date.now()}-${filename}`;
|
|
236
|
-
await Bun.write(path, data);
|
|
237
|
-
respond(id, {
|
|
238
|
-
result: { path, fileSize: data.byteLength, account: accountId },
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
207
|
async function listMembers(
|
|
243
208
|
id: string,
|
|
244
209
|
args: Record<string, unknown>,
|
|
@@ -274,15 +239,6 @@ const HANDLERS: Record<string, StationHandler> = {
|
|
|
274
239
|
listMembers,
|
|
275
240
|
set_profile: setProfile,
|
|
276
241
|
profile: readProfile,
|
|
277
|
-
send_photo: (id, args) =>
|
|
278
|
-
media(id, 'sendPhoto', 'photo', ((args.caption as string) ?? '') + ' [image]', args),
|
|
279
|
-
send_document: (id, args) =>
|
|
280
|
-
media(id, 'sendDocument', 'document', ((args.caption as string) ?? '') + ' [file]', args),
|
|
281
|
-
send_voice: (id, args) => media(id, 'sendVoice', 'voice', '[voice]', args),
|
|
282
|
-
send_sticker: (id, args) => media(id, 'sendSticker', 'sticker', '[sticker]', args),
|
|
283
|
-
send_dice: sendDice,
|
|
284
|
-
send_location: sendLocation,
|
|
285
|
-
download,
|
|
286
242
|
};
|
|
287
243
|
|
|
288
244
|
export const handleCall = makeStation({
|
|
@@ -198,9 +198,17 @@ export function saveMediaAndEmit(
|
|
|
198
198
|
},
|
|
199
199
|
});
|
|
200
200
|
})
|
|
201
|
-
.catch((err: unknown) =>
|
|
202
|
-
process.stderr.write(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
.catch((err: unknown) => {
|
|
202
|
+
process.stderr.write(`telegram-bot media save failed: ${errMsg(err)}\n`);
|
|
203
|
+
emitInbound(emit, accountId, {
|
|
204
|
+
kind: 'inbound',
|
|
205
|
+
id: mintId(),
|
|
206
|
+
ts: new Date().toISOString(),
|
|
207
|
+
station: 'telegram-bot',
|
|
208
|
+
line,
|
|
209
|
+
from: SELF_URI || `metro://telegram-bot/${accountId}/self`,
|
|
210
|
+
text: `📎 not fetched: ${errMsg(err)}`,
|
|
211
|
+
payload: { contentType: 'attachmentFailed', attachmentFor: sourceEnvId, index: 0, reason: errMsg(err) },
|
|
212
|
+
});
|
|
213
|
+
});
|
|
206
214
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tgForm, targetOf } from './accounts.js';
|
|
2
2
|
import { emit, mintId, respond, SELF_URI } from './wire.js';
|
|
3
3
|
import { appendFile } from '@metro-labs/core/stations/attachments';
|
|
4
4
|
|
|
@@ -82,25 +82,6 @@ export async function sendMedia(
|
|
|
82
82
|
return { accountId, message_id: r.message_id };
|
|
83
83
|
}
|
|
84
84
|
|
|
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
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
85
|
export const MEDIA_METHOD_FIELD: Record<
|
|
105
86
|
string,
|
|
106
87
|
{ method: string; field: string }
|
|
@@ -111,56 +92,3 @@ export const MEDIA_METHOD_FIELD: Record<
|
|
|
111
92
|
video: { method: 'sendVideo', field: 'video' },
|
|
112
93
|
document: { method: 'sendDocument', field: 'document' },
|
|
113
94
|
};
|
|
114
|
-
|
|
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
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
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
|
-
);
|
|
166
|
-
}
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
"@xmtp/content-type-reaction": "^2.0.2",
|
|
19
19
|
"@xmtp/content-type-remote-attachment": "^2.0.4",
|
|
20
20
|
"@xmtp/content-type-reply": "^2.0.3",
|
|
21
|
-
"@xmtp/content-type-wallet-send-calls": "^2.0.0",
|
|
22
21
|
"@xmtp/node-bindings": "1.9.1",
|
|
23
22
|
"@xmtp/node-sdk": "^6.0.0",
|
|
24
23
|
"@zerodev/ecdsa-validator": "5.4.9",
|