@stage-labs/metro 0.1.0-beta.324 → 0.1.0-beta.326
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/agent.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/lock.js +15 -3
- package/dist/root-release-io.js +1 -2
- package/dist/root-release.js +2 -3
- package/dist/route.js +1 -1
- package/dist/service.js +4 -4
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
- package/runtime/node_modules/@metro-labs/core/src/lines.ts +2 -8
- package/runtime/node_modules/@metro-labs/core/src/protocol.ts +0 -6
- package/runtime/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
- package/runtime/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
- package/runtime/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
- package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
- package/runtime/node_modules/@metro-labs/core/src/train-error.ts +4 -4
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +2 -6
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +6 -6
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +0 -24
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -4
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +4 -9
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +1 -6
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/channels/pending.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-grant.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/files/upload-api.ts +3 -7
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-api.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/accounts.ts +10 -19
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/call-tools.ts +1 -5
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/event-store.ts +3 -4
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/group-tools.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/index.ts +2 -4
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/server-approvals.ts +2 -5
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/session.ts +1 -5
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +2 -4
- package/runtime/node_modules/@metro-labs/daemon/src/mcp-server/announced.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/tailscale-guard.ts +26 -0
- package/runtime/node_modules/@metro-labs/daemon/src/monitor/api.ts +1 -3
- package/runtime/node_modules/@metro-labs/daemon/src/routes/gmail-attach.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/routes/http.ts +6 -21
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -5
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-resolve.ts +6 -6
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/stations/materialize.ts +12 -61
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +13 -25
- package/runtime/node_modules/@metro-labs/daemon/src/stations/threema-callbacks.ts +5 -5
- package/runtime/node_modules/@metro-labs/daemon/src/stations/webhook-endpoints.ts +2 -7
- package/runtime/node_modules/@metro-labs/daemon/src/voice/call.ts +2 -6
- package/runtime/node_modules/@metro-labs/daemon/src/voice/calls.ts +12 -12
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +5 -23
- package/runtime/node_modules/@metro-labs/discord-bot/src/attachments.ts +1 -7
- package/runtime/node_modules/@metro-labs/discord-bot/src/channels.ts +2 -2
- package/runtime/node_modules/@metro-labs/discord-bot/src/group.ts +10 -35
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +5 -11
- package/runtime/node_modules/@metro-labs/discord-bot/src/members.ts +6 -34
- package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +11 -14
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +1 -3
- package/runtime/node_modules/@metro-labs/discord-bot/src/verify.ts +2 -3
- package/runtime/node_modules/@metro-labs/gmail/src/accounts.ts +3 -3
- package/runtime/node_modules/@metro-labs/gmail/src/auth.ts +8 -8
- package/runtime/node_modules/@metro-labs/gmail/src/managed.ts +3 -2
- package/runtime/node_modules/@metro-labs/http/src/agent-signature.ts +1 -1
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +3 -4
- package/runtime/node_modules/@metro-labs/http/src/connector-discovery.ts +0 -113
- package/runtime/node_modules/@metro-labs/http/src/connector-input.ts +1 -1
- package/runtime/node_modules/@metro-labs/http/src/connector-mcp.ts +12 -13
- package/runtime/node_modules/@metro-labs/http/src/connector-oauth.ts +1 -1
- package/runtime/node_modules/@metro-labs/http/src/connector-policy.ts +1 -6
- package/runtime/node_modules/@metro-labs/http/src/connector-public-url.ts +4 -2
- package/runtime/node_modules/@metro-labs/http/src/connector-tools.ts +1 -11
- package/runtime/node_modules/@metro-labs/http/src/connector-url.ts +0 -6
- package/runtime/node_modules/@metro-labs/http/src/mcp-server-protocol.ts +1 -1
- package/runtime/node_modules/@metro-labs/http/src/mcp-server-tools.ts +0 -2
- package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/http/src/return-to.ts +2 -2
- package/runtime/node_modules/@metro-labs/http/src/workos-token.ts +6 -7
- package/runtime/node_modules/@metro-labs/inference/package.json +1 -0
- package/runtime/node_modules/@metro-labs/inference/src/attempts.ts +3 -3
- package/runtime/node_modules/@metro-labs/inference/src/bedrock.ts +1 -1
- package/runtime/node_modules/@metro-labs/inference/src/codex-translate.ts +2 -2
- package/runtime/node_modules/@metro-labs/inference/src/delivered.ts +1 -1
- package/runtime/node_modules/@metro-labs/inference/src/effort.ts +0 -6
- package/runtime/node_modules/@metro-labs/inference/src/fallback.ts +7 -5
- package/runtime/node_modules/@metro-labs/inference/src/forward.ts +5 -5
- package/runtime/node_modules/@metro-labs/inference/src/measure.ts +2 -3
- package/runtime/node_modules/@metro-labs/inference/src/named.ts +3 -3
- package/runtime/node_modules/@metro-labs/inference/src/probes.ts +98 -0
- package/runtime/node_modules/@metro-labs/inference/src/usage.ts +5 -5
- package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +6 -8
- package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +2 -1
- package/runtime/node_modules/@metro-labs/outlook/src/inbound.ts +3 -5
- package/runtime/node_modules/@metro-labs/outlook/src/me.ts +6 -6
- package/runtime/node_modules/@metro-labs/outlook/src/outbound.ts +10 -13
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +7 -26
- package/runtime/node_modules/@metro-labs/telegram/src/attachments.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram/src/login.ts +3 -6
- package/runtime/node_modules/@metro-labs/telegram/src/profile.ts +3 -2
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +6 -20
- package/runtime/node_modules/@metro-labs/telegram-bot/src/attachments.ts +1 -7
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +0 -2
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +1 -16
- package/runtime/node_modules/@metro-labs/telegram-bot/src/members.ts +1 -8
- package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +2 -1
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +1 -8
- package/runtime/node_modules/@metro-labs/threema/src/chats.ts +4 -3
- package/runtime/node_modules/@metro-labs/threema/src/format.ts +15 -17
- package/runtime/node_modules/@metro-labs/threema/src/groups.ts +2 -4
- package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +2 -1
- package/runtime/node_modules/@metro-labs/threema/src/messages.ts +2 -1
- package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +7 -7
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +1 -3
- package/runtime/node_modules/@metro-labs/whatsapp/src/attachments.ts +0 -2
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +1 -1
- package/runtime/node_modules/@metro-labs/whatsapp/src/login.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +7 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +4 -10
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +4 -5
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +14 -26
- package/runtime/node_modules/@metro-labs/xmtp/src/attachments.ts +1 -7
- package/runtime/node_modules/@metro-labs/xmtp/src/codecs.ts +17 -58
- package/runtime/node_modules/@metro-labs/xmtp/src/conv-helpers.ts +1 -6
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +10 -31
- package/runtime/node_modules/@metro-labs/xmtp/src/frames.ts +12 -14
- package/runtime/node_modules/@metro-labs/xmtp/src/group.ts +2 -3
- package/runtime/node_modules/@metro-labs/xmtp/src/identity.ts +3 -9
- package/runtime/node_modules/@metro-labs/xmtp/src/labels.ts +8 -9
- package/runtime/node_modules/@metro-labs/xmtp/src/names.ts +4 -2
- package/runtime/node_modules/@metro-labs/xmtp/src/network.ts +2 -3
- package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +2 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/read-attachments.ts +2 -2
- package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +2 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +23 -46
- package/runtime/node_modules/@metro-labs/xmtp/src/wallet.ts +5 -4
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +12 -15
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-store.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/automation-types.ts +2 -2
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/lines.ts +2 -8
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/protocol.ts +0 -6
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/skill-source.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/account-status.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/attachments.ts +5 -9
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/channel-directory.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail-text.ts +4 -4
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/mail.ts +3 -2
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/oauth.ts +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/profile.ts +2 -2
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/types.ts +2 -2
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/train-error.ts +4 -4
- package/runtime/sdk-runner/runner.json +1 -1
- package/runtime/sdk-runner/src/approvals.ts +2 -2
- package/runtime/sdk-runner/src/automation-command.ts +2 -2
- package/runtime/sdk-runner/src/calls.ts +0 -6
- package/runtime/sdk-runner/src/errors.ts +1 -1
- package/runtime/sdk-runner/src/link.ts +1 -1
- package/runtime/sdk-runner/src/runner.ts +2 -2
- package/runtime/sdk-runner/src/session-watch.ts +5 -34
- package/runtime/sdk-runner/src/task-state.ts +1 -1
- package/runtime/sdk-runner/src/tool-proxy.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/member-args.ts +0 -40
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, OutgoingHttpHeader, OutgoingHttpHeaders, ServerResponse } from 'node:http';
|
|
2
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
2
3
|
import { StreamErrors, type FailureSite } from './failures.js';
|
|
3
4
|
import { UsageScanner } from './usage.js';
|
|
4
5
|
|
|
@@ -108,9 +109,8 @@ export function parseJson(raw: Buffer): Record<string, unknown> {
|
|
|
108
109
|
} catch {
|
|
109
110
|
throw new GatewayError(400, 'invalid_request_error', 'body must be JSON');
|
|
110
111
|
}
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
return parsed as Record<string, unknown>;
|
|
112
|
+
if (!isRecord(parsed)) throw new GatewayError(400, 'invalid_request_error', 'body must be a JSON object');
|
|
113
|
+
return parsed;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
export function forwardedHeaders(req: IncomingMessage): Record<string, string> {
|
|
@@ -153,7 +153,7 @@ export function addBeta(headers: Record<string, string>, beta: string): Record<s
|
|
|
153
153
|
return { ...headers, 'anthropic-beta': [...listed, beta].join(',') };
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
interface PipeOptions {
|
|
157
157
|
keepalive?: boolean;
|
|
158
158
|
ownCredential?: boolean;
|
|
159
159
|
scanner?: UsageScanner;
|
|
@@ -215,7 +215,7 @@ export async function pipeResponse(upstream: Response, res: ServerResponse, watc
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
interface Relayed {
|
|
219
219
|
push: (chunk: Uint8Array, emit: (frames: string) => void) => void;
|
|
220
220
|
close: () => string;
|
|
221
221
|
idle: (message: string) => string;
|
|
@@ -2,7 +2,7 @@ import type { ServerResponse } from 'node:http';
|
|
|
2
2
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
3
|
import { Attempt, inAttempt, tracked, type AttemptCall, type OnCall } from './attempts.js';
|
|
4
4
|
import type { RequestKind } from './effort.js';
|
|
5
|
-
import { GatewayError
|
|
5
|
+
import { GatewayError } from './forward.js';
|
|
6
6
|
import type { Route } from './model-config.js';
|
|
7
7
|
|
|
8
8
|
const MESSAGES = '/v1/messages';
|
|
@@ -15,8 +15,7 @@ export interface Asked {
|
|
|
15
15
|
|
|
16
16
|
function statusOf(failure: unknown, res: ServerResponse): number {
|
|
17
17
|
if (failure === null) return res.statusCode;
|
|
18
|
-
|
|
19
|
-
return failure instanceof SubscriptionRefused ? 401 : 502;
|
|
18
|
+
return failure instanceof GatewayError ? failure.status : 502;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
function report(onCall: OnCall, call: AttemptCall): void {
|
|
@@ -10,8 +10,8 @@ export interface Chosen {
|
|
|
10
10
|
|
|
11
11
|
const NO_ROUTE = "metro gateway: no model is set up for this agent yet: choose one on the organization's Gateway page";
|
|
12
12
|
|
|
13
|
-
const noRoute = (
|
|
14
|
-
throw new GatewayError(503, 'api_error',
|
|
13
|
+
const noRoute = (): never => {
|
|
14
|
+
throw new GatewayError(503, 'api_error', NO_ROUTE);
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
const MAIN = 'metro';
|
|
@@ -32,5 +32,5 @@ export function namedChoice(requested: string, setup: ModelSetup): Chosen {
|
|
|
32
32
|
if (requested !== MAIN && requested !== WORKERS) throw new GatewayError(400, 'invalid_request_error', NAMES);
|
|
33
33
|
const helpers = requested === WORKERS ? helperSetup(setup) : null;
|
|
34
34
|
const chain = helpers ?? setup;
|
|
35
|
-
return { lane: helpers === null ? 'main' : 'helpers', setup: chain, primary: primaryRoute(chain) ?? noRoute(
|
|
35
|
+
return { lane: helpers === null ? 'main' : 'helpers', setup: chain, primary: primaryRoute(chain) ?? noRoute() };
|
|
36
36
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
2
|
+
import { userAgent } from './codex.js';
|
|
3
|
+
import { OAUTH_BETA } from './forward.js';
|
|
4
|
+
import type { Connection } from './model-config.js';
|
|
5
|
+
import { openrouterKey } from './openrouter.js';
|
|
6
|
+
import { anthropicUsage, clamp, creditsLeft, mergeUsage, openrouterUsage, PER_MINUTE, whenFrom, windowLabel, type Reported, type UsageWindow } from './usage.js';
|
|
7
|
+
|
|
8
|
+
export const PROBE_EVERY_MS = 5 * 60_000;
|
|
9
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
10
|
+
const SECONDS_PER_MINUTE = 60;
|
|
11
|
+
const CHATGPT_BACKEND = 'https://chatgpt.com/backend-api';
|
|
12
|
+
const ANTHROPIC_API = 'https://api.anthropic.com';
|
|
13
|
+
const COUNT_MODEL = 'claude-haiku-4-5';
|
|
14
|
+
|
|
15
|
+
export interface ProbeDeps {
|
|
16
|
+
fetchImpl?: typeof fetch;
|
|
17
|
+
openrouterBase?: string;
|
|
18
|
+
chatgptBase?: string;
|
|
19
|
+
anthropicBase?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const numberOf = (value: unknown): number | null => {
|
|
23
|
+
const parsed = typeof value === 'number' ? value : typeof value === 'string' && value.trim() !== '' ? Number(value) : Number.NaN;
|
|
24
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const textOf = (value: unknown): string | null => (typeof value === 'string' || typeof value === 'number' ? String(value) : null);
|
|
28
|
+
|
|
29
|
+
function chatgptWindow(raw: unknown, now: Date): UsageWindow | null {
|
|
30
|
+
if (!isRecord(raw)) return null;
|
|
31
|
+
const percent = numberOf(raw.used_percent);
|
|
32
|
+
if (percent === null) return null;
|
|
33
|
+
const seconds = numberOf(raw.limit_window_seconds);
|
|
34
|
+
const minutes = seconds === null || seconds <= 0 ? null : Math.ceil(seconds / SECONDS_PER_MINUTE);
|
|
35
|
+
const resetAt = whenFrom(textOf(raw.reset_at), now) ?? whenFrom(textOf(raw.reset_after_seconds), now, true);
|
|
36
|
+
return { label: windowLabel(minutes), used: clamp(percent / 100), resetAt, detail: null };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function chatgptWall(limit: Record<string, unknown>, reached: unknown): string | null {
|
|
40
|
+
if (limit.limit_reached !== true && limit.allowed !== false) return null;
|
|
41
|
+
const kind = isRecord(reached) && typeof reached.type === 'string' ? reached.type : '';
|
|
42
|
+
return kind === '' ? 'limit reached' : kind.replaceAll('_', ' ');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function chatgptUsage(body: unknown, now = new Date()): Reported | null {
|
|
46
|
+
if (!isRecord(body)) return null;
|
|
47
|
+
const limit = isRecord(body.rate_limit) ? body.rate_limit : {};
|
|
48
|
+
const credits = isRecord(body.credits) ? body.credits : {};
|
|
49
|
+
const windows = [chatgptWindow(limit.primary_window, now), chatgptWindow(limit.secondary_window, now), creditsLeft(credits.has_credits === true, textOf(credits.balance))].filter(
|
|
50
|
+
(w): w is UsageWindow => w !== null,
|
|
51
|
+
);
|
|
52
|
+
return windows.length === 0 ? null : { windows, note: chatgptWall(limit, body.rate_limit_reached_type), at: now.toISOString() };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function answer(res: Response, provider: string): Promise<Response> {
|
|
56
|
+
if (res.ok) return res;
|
|
57
|
+
await res.body?.cancel();
|
|
58
|
+
throw new Error(`${provider} would not report its usage (${String(res.status)})`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function chatgptProbe(conn: Connection, deps: ProbeDeps, now: Date): Promise<Reported | null> {
|
|
62
|
+
const access = conn.codex;
|
|
63
|
+
if (access?.method !== 'code') return null;
|
|
64
|
+
const headers = { accept: 'application/json', authorization: `Bearer ${access.accessToken}`, 'chatgpt-account-id': access.accountId ?? '', originator: 'codex_cli_rs', 'user-agent': userAgent() };
|
|
65
|
+
const res = await (deps.fetchImpl ?? fetch)(`${deps.chatgptBase ?? CHATGPT_BACKEND}/wham/usage`, { headers, redirect: 'manual', signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
66
|
+
return chatgptUsage(await (await answer(res, 'ChatGPT')).json(), now);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function claudeProbe(conn: Connection, deps: ProbeDeps, now: Date): Promise<Reported | null> {
|
|
70
|
+
if (conn.claudeToken === '') return null;
|
|
71
|
+
const res = await (deps.fetchImpl ?? fetch)(`${deps.anthropicBase ?? ANTHROPIC_API}/v1/messages/count_tokens`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { authorization: `Bearer ${conn.claudeToken}`, 'anthropic-version': '2023-06-01', 'anthropic-beta': OAUTH_BETA, 'content-type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({ model: COUNT_MODEL, messages: [{ role: 'user', content: 'usage' }] }),
|
|
75
|
+
redirect: 'manual',
|
|
76
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
77
|
+
});
|
|
78
|
+
await (await answer(res, 'Anthropic')).body?.cancel();
|
|
79
|
+
const usage = anthropicUsage(res.headers, now);
|
|
80
|
+
return usage === null || usage.windows.every((w) => w.label === PER_MINUTE) ? null : usage;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function reported(conn: Connection, deps: ProbeDeps, now: Date): Promise<Reported | null> {
|
|
84
|
+
if (conn.provider === 'openrouter') return conn.apiKey === '' ? null : openrouterUsage(await openrouterKey(conn.apiKey, deps.openrouterBase, deps.fetchImpl), now);
|
|
85
|
+
if (conn.provider === 'codex') return chatgptProbe(conn, deps, now);
|
|
86
|
+
if (conn.provider === 'anthropic') return claudeProbe(conn, deps, now);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const probes = (conn: Connection): boolean =>
|
|
91
|
+
(conn.provider === 'openrouter' && conn.apiKey !== '') || (conn.provider === 'codex' && conn.codex?.method === 'code') || (conn.provider === 'anthropic' && conn.claudeToken !== '');
|
|
92
|
+
|
|
93
|
+
export async function probeUsage(conn: Connection, deps: ProbeDeps = {}, now = new Date()): Promise<boolean> {
|
|
94
|
+
const usage = await reported(conn, deps, now);
|
|
95
|
+
if (usage === null) return false;
|
|
96
|
+
mergeUsage(conn.id, usage);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
@@ -35,7 +35,7 @@ export const noteUsage = (key: Key, usage: Reported): void => {
|
|
|
35
35
|
stamps.set(key, new Map(usage.windows.map((w) => [w.label, Date.parse(usage.at)])));
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
function mergeUsage(key: Key, usage: Reported): void {
|
|
38
|
+
export function mergeUsage(key: Key, usage: Reported): void {
|
|
39
39
|
const seen = latest.get(key);
|
|
40
40
|
const marks = stamps.get(key);
|
|
41
41
|
if (seen === undefined || marks === undefined) {
|
|
@@ -76,7 +76,7 @@ export function mayProbe(key: Key, now: number, every: number): boolean {
|
|
|
76
76
|
return true;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
interface Counted {
|
|
80
80
|
input: number;
|
|
81
81
|
output: number;
|
|
82
82
|
cached: number;
|
|
@@ -158,7 +158,7 @@ export class UsageScanner {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
const clamp = (value: number): number => Math.min(1, Math.max(0, value));
|
|
161
|
+
export const clamp = (value: number): number => Math.min(1, Math.max(0, value));
|
|
162
162
|
|
|
163
163
|
function fraction(raw: string | null): number | null {
|
|
164
164
|
if (raw === null) return null;
|
|
@@ -168,7 +168,7 @@ function fraction(raw: string | null): number | null {
|
|
|
168
168
|
|
|
169
169
|
const MS_THRESHOLD = 1e12;
|
|
170
170
|
|
|
171
|
-
function whenFrom(raw: string | null, now: Date, relative = false): string | null {
|
|
171
|
+
export function whenFrom(raw: string | null, now: Date, relative = false): string | null {
|
|
172
172
|
if (raw === null || raw.trim() === '') return null;
|
|
173
173
|
const numeric = Number(raw);
|
|
174
174
|
if (Number.isFinite(numeric)) {
|
|
@@ -247,7 +247,7 @@ function codexWindow(headers: Headers, kind: 'primary' | 'secondary', now: Date)
|
|
|
247
247
|
return { label: windowLabel(Number.isFinite(minutes) ? minutes : null), used, resetAt, detail: null };
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
function creditsLeft(hasCredits: boolean, raw: string | null): UsageWindow | null {
|
|
250
|
+
export function creditsLeft(hasCredits: boolean, raw: string | null): UsageWindow | null {
|
|
251
251
|
if (!hasCredits) return null;
|
|
252
252
|
const balance = Number(raw ?? 'none');
|
|
253
253
|
if (!Number.isFinite(balance)) return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errMsg } from '@metro-labs/core/log';
|
|
1
2
|
import { makeAccountStore, resolveAccountId, type Die } from '@metro-labs/core/stations/account-store';
|
|
2
3
|
import { threadOfLine } from '@metro-labs/core/stations/mail';
|
|
3
4
|
import { TokenKeeper, type FetchLike } from '@metro-labs/core/stations/oauth';
|
|
@@ -27,7 +28,7 @@ export const { loadAccounts } = makeAccountStore<AccountConfig>({
|
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
const signedOut = (err: unknown): never => {
|
|
30
|
-
throw new TrainError('outlook_signed_out',
|
|
31
|
+
throw new TrainError('outlook_signed_out', errMsg(err), { retryable: false });
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
export class Account {
|
|
@@ -65,14 +66,11 @@ export class Account {
|
|
|
65
66
|
|
|
66
67
|
export const accounts = new Map<string, Account>();
|
|
67
68
|
|
|
68
|
-
function accountFor(id: string): Account {
|
|
69
|
-
const acct = accounts.get(id);
|
|
70
|
-
if (!acct) throw new Error(`unknown account '${id}' (have: ${[...accounts.keys()].join(', ')})`);
|
|
71
|
-
return acct;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
69
|
export function accountOf(args: { account?: unknown; line?: unknown }): Account {
|
|
75
70
|
const line = typeof args.line === 'string' && args.line !== '' ? args.line : undefined;
|
|
76
71
|
const account = typeof args.account === 'string' && args.account !== '' ? args.account : undefined;
|
|
77
|
-
|
|
72
|
+
const id = resolveAccountId(accounts, { account, line }, (l) => threadOfLine('outlook', l)?.accountId);
|
|
73
|
+
const acct = accounts.get(id);
|
|
74
|
+
if (!acct) throw new Error(`unknown account '${id}'`);
|
|
75
|
+
return acct;
|
|
78
76
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
1
2
|
import { postForm, seconds, SignInError, tokensOf, type FetchLike, type OAuthBody, type Tokens } from '@metro-labs/core/stations/oauth';
|
|
2
3
|
import { clientId, loginBase, NOT_SET_UP, SCOPES } from './config.js';
|
|
3
4
|
|
|
@@ -64,7 +65,7 @@ function decodeClaims(jwt: string): Body | null {
|
|
|
64
65
|
if (part === undefined) return null;
|
|
65
66
|
try {
|
|
66
67
|
const raw: unknown = JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
|
|
67
|
-
return
|
|
68
|
+
return isRecord(raw) ? raw : null;
|
|
68
69
|
} catch {
|
|
69
70
|
return null;
|
|
70
71
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { log } from '@metro-labs/core/log';
|
|
1
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
2
2
|
import { mailEnvelope, noteSeen, pollForever } from '@metro-labs/core/stations/mail';
|
|
3
3
|
import type { Screened } from '@metro-labs/core/stations/mail-trust';
|
|
4
4
|
import { emitInbound, reportAttachment } from '@metro-labs/core/stations/train-events';
|
|
@@ -26,11 +26,9 @@ function fresh(acct: Account, m: GraphMessage, since: string): boolean {
|
|
|
26
26
|
return Date.parse(m.receivedDateTime ?? '') >= Date.parse(since);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const reasonOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
30
|
-
|
|
31
29
|
async function screened(acct: Account, m: GraphMessage): Promise<Screened> {
|
|
32
30
|
return screen(acct, m).catch((err: unknown) => {
|
|
33
|
-
process.stderr.write(`outlook[${acct.id}] could not read the headers of a message, so it counts as unverified: ${
|
|
31
|
+
process.stderr.write(`outlook[${acct.id}] could not read the headers of a message, so it counts as unverified: ${errMsg(err)}\n`);
|
|
34
32
|
return { verified: false };
|
|
35
33
|
});
|
|
36
34
|
}
|
|
@@ -44,7 +42,7 @@ async function deliver(acct: Account, m: GraphMessage): Promise<boolean> {
|
|
|
44
42
|
const files: MailFile[] =
|
|
45
43
|
m.hasAttachments === true
|
|
46
44
|
? await listFiles(acct, m.id).catch((err: unknown) => {
|
|
47
|
-
process.stderr.write(`outlook[${acct.id}] could not list the files of a message: ${
|
|
45
|
+
process.stderr.write(`outlook[${acct.id}] could not list the files of a message: ${errMsg(err)}\n`);
|
|
48
46
|
return [];
|
|
49
47
|
})
|
|
50
48
|
: [];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { errMsg } from '@metro-labs/core/log';
|
|
2
|
+
import { str } from '@metro-labs/core/stations/mail';
|
|
1
3
|
import { SignInError, type FetchLike } from '@metro-labs/core/stations/oauth';
|
|
2
4
|
import { graphBase } from './config.js';
|
|
3
5
|
|
|
@@ -6,8 +8,6 @@ export interface Mailbox {
|
|
|
6
8
|
name: string;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
const text = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
|
|
10
|
-
|
|
11
11
|
const wrongMailbox = (actual: string, wanted: string): string =>
|
|
12
12
|
`You signed in as ${actual}, not ${wanted}. Nothing was connected. Start again and pick ${wanted} on Microsoft's page, or use "Use another account".`;
|
|
13
13
|
|
|
@@ -18,14 +18,14 @@ export async function verifyMailbox(accessToken: string, fetchImpl: FetchLike, w
|
|
|
18
18
|
headers: { authorization: `Bearer ${accessToken}`, accept: 'application/json' },
|
|
19
19
|
});
|
|
20
20
|
} catch (err) {
|
|
21
|
-
throw new SignInError(`Metro could not reach Microsoft Graph: ${
|
|
21
|
+
throw new SignInError(`Metro could not reach Microsoft Graph: ${errMsg(err)}`);
|
|
22
22
|
}
|
|
23
23
|
if (!res.ok) throw new SignInError(`Microsoft Graph refused the new sign-in (${String(res.status)}), so nothing was connected.`);
|
|
24
24
|
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
25
|
-
const mail =
|
|
26
|
-
const principal =
|
|
25
|
+
const mail = str(body.mail).toLowerCase();
|
|
26
|
+
const principal = str(body.userPrincipalName).toLowerCase();
|
|
27
27
|
const email = mail || principal;
|
|
28
28
|
if (email === '') throw new SignInError('Microsoft Graph did not say which mailbox this is, so nothing was connected.');
|
|
29
29
|
if (wanted !== null && wanted !== mail && wanted !== principal) throw new SignInError(wrongMailbox(email, wanted));
|
|
30
|
-
return { email, name:
|
|
30
|
+
return { email, name: str(body.displayName) };
|
|
31
31
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { checkAddress, filesOf, isAddress, mailLine, str, subjectOf, threadOfLine, type OutgoingFile } from '@metro-labs/core/stations/mail';
|
|
2
2
|
import { escapeHtml } from '@metro-labs/core/stations/mail-text';
|
|
3
|
+
import { errMsg } from '@metro-labs/core/log';
|
|
3
4
|
import { respond } from '@metro-labs/core/stations/station-runtime';
|
|
4
5
|
import { TrainError } from '@metro-labs/core/train-error';
|
|
5
6
|
import { accountOf, type Account } from './accounts.js';
|
|
@@ -22,14 +23,11 @@ export async function latestInConversation(acct: Account, conversationId: string
|
|
|
22
23
|
|
|
23
24
|
async function discard(acct: Account, draftId: string): Promise<void> {
|
|
24
25
|
await graph(acct, messagePath(draftId), { method: 'DELETE' }).catch((err: unknown) => {
|
|
25
|
-
process.stderr.write(`outlook[${acct.id}] could not remove an unsent draft: ${
|
|
26
|
+
process.stderr.write(`outlook[${acct.id}] could not remove an unsent draft: ${errMsg(err)}\n`);
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
async function
|
|
30
|
-
const draft = await graphJson<{ id?: string }>(acct, `${messagePath(target)}/${verb === 'reply' ? 'createReply' : 'createReplyAll'}`, jsonInit('POST', { comment }));
|
|
31
|
-
const draftId = draft.id;
|
|
32
|
-
if (draftId === undefined) throw new TrainError('outlook_graph_error', 'Outlook did not open a reply draft', { retryable: true });
|
|
30
|
+
async function sendDraft(acct: Account, draftId: string, files: OutgoingFile[]): Promise<string[]> {
|
|
33
31
|
const labels: string[] = [];
|
|
34
32
|
try {
|
|
35
33
|
for (const file of files) labels.push(await attachFile(acct, draftId, file));
|
|
@@ -41,6 +39,12 @@ async function withFiles(acct: Account, target: string, verb: 'reply' | 'replyAl
|
|
|
41
39
|
return labels;
|
|
42
40
|
}
|
|
43
41
|
|
|
42
|
+
async function withFiles(acct: Account, target: string, verb: 'reply' | 'replyAll', comment: string, files: OutgoingFile[]): Promise<string[]> {
|
|
43
|
+
const draft = await graphJson<{ id?: string }>(acct, `${messagePath(target)}/${verb === 'reply' ? 'createReply' : 'createReplyAll'}`, jsonInit('POST', { comment }));
|
|
44
|
+
if (draft.id === undefined) throw new TrainError('outlook_graph_error', 'Outlook did not open a reply draft', { retryable: true });
|
|
45
|
+
return sendDraft(acct, draft.id, files);
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
async function compose(id: string, acct: Account, to: string, args: Args, text: string, files: OutgoingFile[]): Promise<void> {
|
|
45
49
|
checkAddress('outlook', to);
|
|
46
50
|
const draft = await graphJson<{ id?: string; conversationId?: string }>(acct, '/me/messages', jsonInit('POST', {
|
|
@@ -49,14 +53,7 @@ async function compose(id: string, acct: Account, to: string, args: Args, text:
|
|
|
49
53
|
toRecipients: [{ emailAddress: { address: to } }],
|
|
50
54
|
}));
|
|
51
55
|
if (draft.id === undefined) throw new TrainError('outlook_graph_error', 'Outlook did not open a draft', { retryable: true });
|
|
52
|
-
const labels
|
|
53
|
-
try {
|
|
54
|
-
for (const file of files) labels.push(await attachFile(acct, draft.id, file));
|
|
55
|
-
await graph(acct, `${messagePath(draft.id)}/send`, { method: 'POST' });
|
|
56
|
-
} catch (err) {
|
|
57
|
-
await discard(acct, draft.id);
|
|
58
|
-
throw err;
|
|
59
|
-
}
|
|
56
|
+
const labels = await sendDraft(acct, draft.id, files);
|
|
60
57
|
const line = draft.conversationId === undefined ? undefined : mailLine('outlook', acct.id, draft.conversationId);
|
|
61
58
|
respond(id, { result: { account: acct.id, messageId: draft.id, ...(line === undefined ? {} : { line }), ...(labels.length > 0 ? { attachments: labels } : {}) } });
|
|
62
59
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TrainError } from '@metro-labs/core/train-error';
|
|
2
|
+
import type { GroupResult } from '@metro-labs/core/stations/types';
|
|
2
3
|
import { errMsg } from '@metro-labs/core/log';
|
|
3
4
|
import {
|
|
4
5
|
makeStation,
|
|
@@ -204,33 +205,13 @@ function makeGroupCreate(clientFor: ClientFor): StationHandler {
|
|
|
204
205
|
};
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
|
|
208
|
-
return async (id, args) => {
|
|
209
|
-
const { client, chatId } = resolve(args, clientFor);
|
|
210
|
-
const line = lineOfArgs(args);
|
|
211
|
-
respond(id, {
|
|
212
|
-
result: await guard(() => groupAddMembers(client, chatId, line, args)),
|
|
213
|
-
});
|
|
214
|
-
};
|
|
215
|
-
}
|
|
208
|
+
type GroupOp = (client: UserClient, chatId: number, line: string, args: Args) => Promise<GroupResult>;
|
|
216
209
|
|
|
217
|
-
function
|
|
210
|
+
function makeGroupOp(clientFor: ClientFor, op: GroupOp): StationHandler {
|
|
218
211
|
return async (id, args) => {
|
|
219
212
|
const { client, chatId } = resolve(args, clientFor);
|
|
220
213
|
const line = lineOfArgs(args);
|
|
221
|
-
respond(id, {
|
|
222
|
-
result: await guard(() => groupRemoveMembers(client, chatId, line, args)),
|
|
223
|
-
});
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function makeGroupInvite(clientFor: ClientFor): StationHandler {
|
|
228
|
-
return async (id, args) => {
|
|
229
|
-
const { client, chatId } = resolve(args, clientFor);
|
|
230
|
-
const line = lineOfArgs(args);
|
|
231
|
-
respond(id, {
|
|
232
|
-
result: await guard(() => groupInviteLink(client, chatId, line)),
|
|
233
|
-
});
|
|
214
|
+
respond(id, { result: await guard(() => op(client, chatId, line, args)) });
|
|
234
215
|
};
|
|
235
216
|
}
|
|
236
217
|
|
|
@@ -264,9 +245,9 @@ export function makeHandleCall(
|
|
|
264
245
|
listChannels: makeListChannels(clientFor),
|
|
265
246
|
listMembers: makeListMembers(clientFor),
|
|
266
247
|
groupCreate: makeGroupCreate(clientFor),
|
|
267
|
-
groupAddMembers:
|
|
268
|
-
groupRemoveMembers:
|
|
269
|
-
groupInviteLink:
|
|
248
|
+
groupAddMembers: makeGroupOp(clientFor, groupAddMembers),
|
|
249
|
+
groupRemoveMembers: makeGroupOp(clientFor, groupRemoveMembers),
|
|
250
|
+
groupInviteLink: makeGroupOp(clientFor, groupInviteLink),
|
|
270
251
|
set_profile: makeSetProfile(clientFor),
|
|
271
252
|
profile: makeReadProfile(clientFor),
|
|
272
253
|
},
|
|
@@ -19,7 +19,7 @@ interface MediaMeta {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function fallbackMime(media: DownloadableMedia): string | undefined {
|
|
22
|
-
const type = (media
|
|
22
|
+
const type = mediaType(media);
|
|
23
23
|
if (type === 'photo') return 'image/jpeg';
|
|
24
24
|
if (type === 'sticker') return 'image/webp';
|
|
25
25
|
return undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TelegramClient } from '@mtcute/bun';
|
|
2
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
2
3
|
|
|
3
4
|
export class TelegramLoginError extends Error {}
|
|
4
5
|
|
|
@@ -19,9 +20,7 @@ const PHONE_RE = /^[0-9]{6,15}$/;
|
|
|
19
20
|
const API_HASH_RE = /^[a-f0-9]{32}$/i;
|
|
20
21
|
|
|
21
22
|
export function rpcErrorText(err: unknown): string {
|
|
22
|
-
|
|
23
|
-
const text = (err as { text?: unknown }).text;
|
|
24
|
-
return typeof text === 'string' ? text : '';
|
|
23
|
+
return isRecord(err) && typeof err.text === 'string' ? err.text : '';
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
export function needsPassword(err: unknown): boolean {
|
|
@@ -75,9 +74,7 @@ interface SentCodeLike {
|
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
const isSentCode = (v: unknown): v is SentCodeLike =>
|
|
78
|
-
typeof v === '
|
|
79
|
-
v !== null &&
|
|
80
|
-
typeof (v as { phoneCodeHash?: unknown }).phoneCodeHash === 'string';
|
|
77
|
+
isRecord(v) && typeof v.phoneCodeHash === 'string';
|
|
81
78
|
|
|
82
79
|
interface UserLike {
|
|
83
80
|
id: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TelegramClient } from '@mtcute/bun';
|
|
2
|
+
import { errMsg } from '@metro-labs/core/log';
|
|
2
3
|
import {
|
|
3
4
|
assertImage,
|
|
4
5
|
fieldsOf,
|
|
@@ -54,7 +55,7 @@ function sendersOf(client: UserClient): ProfileCache<SenderProfile> {
|
|
|
54
55
|
...(nonEmpty(full.bio) === undefined ? {} : { about: nonEmpty(full.bio) }),
|
|
55
56
|
};
|
|
56
57
|
},
|
|
57
|
-
{ onError: (user, err) => process.stderr.write(`telegram[${client.account.id}] could not read the profile of ${user}: ${
|
|
58
|
+
{ onError: (user, err) => process.stderr.write(`telegram[${client.account.id}] could not read the profile of ${user}: ${errMsg(err)}\n`) },
|
|
58
59
|
);
|
|
59
60
|
senders.set(client.account.id, made);
|
|
60
61
|
return made;
|
|
@@ -63,7 +64,7 @@ function sendersOf(client: UserClient): ProfileCache<SenderProfile> {
|
|
|
63
64
|
export function makeReadProfile(clientFor: (accountId: string) => UserClient): StationHandler {
|
|
64
65
|
return async (id, args) => {
|
|
65
66
|
const accountId = accountFor({ account: str(args.account) });
|
|
66
|
-
const user = str(args.user)
|
|
67
|
+
const user = str(args.user);
|
|
67
68
|
if (!/^\d+$/.test(user)) throw new TrainError('telegram_user_required', 'profile needs the numeric Telegram id of the person', { retryable: false });
|
|
68
69
|
respond(id, { result: (await sendersOf(clientFor(accountId)).get(user)) ?? { id: user } });
|
|
69
70
|
};
|
|
@@ -6,19 +6,12 @@ import {
|
|
|
6
6
|
inaccessibleMemberList,
|
|
7
7
|
type TgChatMember,
|
|
8
8
|
} from './members.js';
|
|
9
|
-
import { makeStation, respond, type
|
|
9
|
+
import { makeStation, respond, type StationHandler } from '@metro-labs/core/stations/station-runtime';
|
|
10
10
|
import { mediaKindOf } from './attachments.js';
|
|
11
|
-
import {
|
|
12
|
-
emitOutbound,
|
|
13
|
-
finishSend,
|
|
14
|
-
MEDIA_METHOD_FIELD,
|
|
15
|
-
sendMedia,
|
|
16
|
-
} from './media-actions.js';
|
|
11
|
+
import { emitOutbound, MEDIA_METHOD_FIELD, sendMedia } from './media-actions.js';
|
|
17
12
|
import { readProfile, setProfile } from './profile.js';
|
|
18
13
|
import { listChannels } from './channels.js';
|
|
19
14
|
|
|
20
|
-
export type { CallMsg };
|
|
21
|
-
|
|
22
15
|
const meCache = new Map<string, { id: number; username: string | null }>();
|
|
23
16
|
async function getMe(
|
|
24
17
|
accountId: string,
|
|
@@ -70,7 +63,6 @@ interface SendArgs {
|
|
|
70
63
|
}[];
|
|
71
64
|
}
|
|
72
65
|
|
|
73
|
-
|
|
74
66
|
type WireAttachment = NonNullable<SendArgs['attachments']>[number];
|
|
75
67
|
|
|
76
68
|
const attachmentKind = (att: WireAttachment): string =>
|
|
@@ -139,7 +131,8 @@ async function send(id: string, args: Record<string, unknown>): Promise<void> {
|
|
|
139
131
|
if (topicId !== undefined) body.message_thread_id = topicId;
|
|
140
132
|
if (replyTo) body.reply_parameters = { message_id: Number(replyTo) };
|
|
141
133
|
const sent = await tg<{ message_id: number }>(accountId, 'sendMessage', body);
|
|
142
|
-
|
|
134
|
+
emitOutbound(accountId, line, String(sent.message_id), text, replyTo);
|
|
135
|
+
respond(id, { result: { messageId: String(sent.message_id), account: accountId } });
|
|
143
136
|
}
|
|
144
137
|
|
|
145
138
|
async function react(id: string, args: Record<string, unknown>): Promise<void> {
|
|
@@ -212,19 +205,12 @@ async function listMembers(
|
|
|
212
205
|
respond(id, { result: inaccessibleMemberList(errMsg(e)) });
|
|
213
206
|
return;
|
|
214
207
|
}
|
|
215
|
-
|
|
216
|
-
try {
|
|
217
|
-
total = await tg<number>(accountId, 'getChatMemberCount', {
|
|
218
|
-
chat_id: chatId,
|
|
219
|
-
});
|
|
220
|
-
} catch {
|
|
221
|
-
total = null;
|
|
222
|
-
}
|
|
208
|
+
const total = await tg<number>(accountId, 'getChatMemberCount', { chat_id: chatId }).catch(() => null);
|
|
223
209
|
respond(id, { result: adminMemberList(admins, total) });
|
|
224
210
|
}
|
|
225
211
|
|
|
226
212
|
const HANDLERS: Record<string, StationHandler> = {
|
|
227
|
-
accounts:
|
|
213
|
+
accounts: listAccounts,
|
|
228
214
|
send,
|
|
229
215
|
react,
|
|
230
216
|
typing,
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
saveBufferToCache,
|
|
3
|
-
assertContentLength,
|
|
4
|
-
} from '@metro-labs/core/stations/attachments';
|
|
5
|
-
import type { SavedAttachment } from '@metro-labs/core/stations/attachments';
|
|
1
|
+
import { assertContentLength, saveBufferToCache, type SavedAttachment } from '@metro-labs/core/stations/attachments';
|
|
6
2
|
import { tg, accounts } from './accounts.js';
|
|
7
3
|
import type { TgMsg } from './types.js';
|
|
8
4
|
|
|
9
|
-
export type { SavedAttachment };
|
|
10
|
-
|
|
11
5
|
function isOggRef(mime: string | undefined, ref: string | undefined): boolean {
|
|
12
6
|
const ext = ref?.split('?')[0]?.split('.').pop()?.toLowerCase();
|
|
13
7
|
return mime === 'audio/ogg' || ext === 'ogg' || ext === 'oga';
|
|
@@ -4,8 +4,6 @@ import { mintId } from '@metro-labs/core/stations/station-runtime';
|
|
|
4
4
|
import { mediaRefOf, saveTelegramMedia } from './attachments.js';
|
|
5
5
|
import type { TgMsg, TgReaction, TgReactionCount, TgUser } from './types.js';
|
|
6
6
|
|
|
7
|
-
export type { TgMsg, TgReaction, TgReactionCount };
|
|
8
|
-
|
|
9
7
|
function lineForMsg(
|
|
10
8
|
accountId: string,
|
|
11
9
|
m: TgMsg,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tg, targetOf } from './accounts.js';
|
|
2
2
|
import { selfUri } from '@metro-labs/core/stations/train-events';
|
|
3
|
-
import { emit, mintId
|
|
3
|
+
import { emit, mintId } from '@metro-labs/core/stations/station-runtime';
|
|
4
4
|
import { appendFile } from '@metro-labs/core/stations/attachments';
|
|
5
5
|
|
|
6
6
|
export function emitOutbound(
|
|
@@ -25,21 +25,6 @@ export function emitOutbound(
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function finishSend(
|
|
29
|
-
id: string,
|
|
30
|
-
accountId: string,
|
|
31
|
-
line: string,
|
|
32
|
-
messageId: string,
|
|
33
|
-
label: string,
|
|
34
|
-
replyTo?: string,
|
|
35
|
-
extra?: Record<string, unknown>,
|
|
36
|
-
): void {
|
|
37
|
-
emitOutbound(accountId, line, messageId, label, replyTo);
|
|
38
|
-
respond(id, {
|
|
39
|
-
result: { messageId, account: accountId, ...extra },
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
28
|
interface MediaArgs {
|
|
44
29
|
line: string;
|
|
45
30
|
path: string;
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import type { MemberList, MetroMember } from '@metro-labs/core/stations/types';
|
|
2
|
-
|
|
3
|
-
export interface TgUser {
|
|
4
|
-
id: number;
|
|
5
|
-
username?: string;
|
|
6
|
-
first_name?: string;
|
|
7
|
-
last_name?: string;
|
|
8
|
-
is_bot?: boolean;
|
|
9
|
-
}
|
|
2
|
+
import type { TgUser } from './types.js';
|
|
10
3
|
|
|
11
4
|
export interface TgChatMember {
|
|
12
5
|
user: TgUser;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errMsg } from '@metro-labs/core/log';
|
|
1
2
|
import { appendFile } from '@metro-labs/core/stations/attachments';
|
|
2
3
|
import {
|
|
3
4
|
assertImage,
|
|
@@ -74,7 +75,7 @@ const senders = makeProfileCache<SenderProfile>(
|
|
|
74
75
|
...(avatar === undefined ? {} : { avatar }),
|
|
75
76
|
};
|
|
76
77
|
},
|
|
77
|
-
{ onError: (key, err) => process.stderr.write(`telegram-bot: could not read the profile of ${key}: ${
|
|
78
|
+
{ onError: (key, err) => process.stderr.write(`telegram-bot: could not read the profile of ${key}: ${errMsg(err)}\n`) },
|
|
78
79
|
);
|
|
79
80
|
|
|
80
81
|
export async function readProfile(id: string, args: Record<string, unknown>): Promise<void> {
|