@stage-labs/metro 0.1.0-beta.68 → 0.1.0-beta.69
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/package.json +1 -1
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +10 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/addressed.ts +25 -0
- package/runtime/node_modules/@metro-labs/mcp/src/channels/inbound.ts +10 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/events.ts +3 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/http.ts +12 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/protocol.ts +2 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/call-tools.ts +16 -3
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/session.ts +4 -0
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-dispatch.ts +13 -10
- package/runtime/node_modules/@metro-labs/mcp/src/mcp/tool-schemas.ts +4 -1
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +15 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +29 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +6 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +10 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +11 -2
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +6 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/parse.ts +44 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit-payloads.ts +2 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +14 -0
- package/runtime/runtime.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.69",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -65,6 +65,15 @@ function tagFor(att: {
|
|
|
65
65
|
return `[${kind}: ${att.name ?? kind}]`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function addressing(accountId: string, m: Message): { mentions_self: boolean; reply_to_self: boolean } {
|
|
69
|
+
const me = accounts.get(accountId)?.client.user ?? null;
|
|
70
|
+
if (me === null) return { mentions_self: false, reply_to_self: false };
|
|
71
|
+
return {
|
|
72
|
+
mentions_self: m.mentions.has(me, { ignoreRepliedUser: true, ignoreEveryone: true }),
|
|
73
|
+
reply_to_self: m.mentions.repliedUser?.id === me.id,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
export function messageEnvelope(
|
|
69
78
|
accountId: string,
|
|
70
79
|
m: Message,
|
|
@@ -112,6 +121,7 @@ export function messageEnvelope(
|
|
|
112
121
|
text,
|
|
113
122
|
is_private: m.guildId == null,
|
|
114
123
|
reply_to: m.reference?.messageId ?? undefined,
|
|
124
|
+
...addressing(accountId, m),
|
|
115
125
|
...(m.reference?.messageId
|
|
116
126
|
? { event: { type: 'reply', replyTo: m.reference.messageId } }
|
|
117
127
|
: {}),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { str } from '../mcp/str.js';
|
|
2
|
+
|
|
3
|
+
export type Addressed = 'direct' | 'mention' | 'reply';
|
|
4
|
+
|
|
5
|
+
interface AddressFacts {
|
|
6
|
+
isPrivate?: unknown;
|
|
7
|
+
mentionsSelf?: unknown;
|
|
8
|
+
replyToSelf?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function addressedBy(facts: AddressFacts, replyTo: string, sent: ReadonlySet<string>): Addressed | undefined {
|
|
12
|
+
if (facts.isPrivate === true) return 'direct';
|
|
13
|
+
if (facts.mentionsSelf === true) return 'mention';
|
|
14
|
+
if (facts.replyToSelf === true || (replyTo !== '' && sent.has(replyTo))) return 'reply';
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function replyMeta(ev: Record<string, unknown>, sent: ReadonlySet<string>): Record<string, string> {
|
|
19
|
+
const replyTo = str(ev.replyTo);
|
|
20
|
+
const addressed = addressedBy(ev, replyTo, sent);
|
|
21
|
+
return {
|
|
22
|
+
...(replyTo ? { reply_to: replyTo } : {}),
|
|
23
|
+
...(addressed === undefined ? {} : { addressed }),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type SavedMedia,
|
|
9
9
|
} from './media-note.js';
|
|
10
10
|
import { buildWebhookNote } from './webhook-note.js';
|
|
11
|
+
import { replyMeta } from './addressed.js';
|
|
11
12
|
import {
|
|
12
13
|
capSet,
|
|
13
14
|
displayNameMeta,
|
|
@@ -31,6 +32,7 @@ const DEDUPE_TTL_MS = 30_000;
|
|
|
31
32
|
const DEDUPE_MAX = 2_000;
|
|
32
33
|
const ALLOWED_LINES_MAX = 2_000;
|
|
33
34
|
const PENDING_PERMISSIONS_MAX = 500;
|
|
35
|
+
const SENT_IDS_MAX = 2_000;
|
|
34
36
|
|
|
35
37
|
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
|
|
36
38
|
|
|
@@ -43,6 +45,7 @@ export class InboundRelay {
|
|
|
43
45
|
private readonly seenEvents = new Map<string, number>();
|
|
44
46
|
private readonly allowedLines = new Set<string>();
|
|
45
47
|
private readonly pendingPermissions = new Set<string>();
|
|
48
|
+
private readonly sentIds = new Set<string>();
|
|
46
49
|
private lastLine: string | undefined;
|
|
47
50
|
|
|
48
51
|
constructor(deps: InboundDeps) {
|
|
@@ -58,6 +61,12 @@ export class InboundRelay {
|
|
|
58
61
|
capSet(this.pendingPermissions, PENDING_PERMISSIONS_MAX);
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
noteSent(messageId: string): void {
|
|
65
|
+
if (!messageId) return;
|
|
66
|
+
this.sentIds.add(messageId);
|
|
67
|
+
capSet(this.sentIds, SENT_IDS_MAX);
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
private notify(method: string, params: Record<string, unknown>): Promise<void> {
|
|
62
71
|
return this.deps.mcp.notification({ method, params });
|
|
63
72
|
}
|
|
@@ -318,6 +327,7 @@ export class InboundRelay {
|
|
|
318
327
|
line_name: str(ev.lineName),
|
|
319
328
|
from_name: str(ev.fromName),
|
|
320
329
|
...displayNameMeta(ev.fromDisplayName),
|
|
330
|
+
...replyMeta(ev, this.sentIds),
|
|
321
331
|
},
|
|
322
332
|
});
|
|
323
333
|
}
|
|
@@ -128,6 +128,17 @@ function eventText(env: TrainEvent): string | undefined {
|
|
|
128
128
|
return env.emoji ? `[react ${env.emoji}]` : undefined;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
function addressingFacts(
|
|
132
|
+
env: TrainEvent,
|
|
133
|
+
isPrivate: boolean,
|
|
134
|
+
): Pick<MetroEvent, 'isPrivate' | 'mentionsSelf' | 'replyToSelf'> {
|
|
135
|
+
return {
|
|
136
|
+
...(isPrivate ? { isPrivate: true } : {}),
|
|
137
|
+
...(env.mentions_self === true ? { mentionsSelf: true } : {}),
|
|
138
|
+
...(env.reply_to_self === true ? { replyToSelf: true } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
131
142
|
export function trainEventToMetroEvent(
|
|
132
143
|
env: TrainEvent,
|
|
133
144
|
trainName: string,
|
|
@@ -155,6 +166,7 @@ export function trainEventToMetroEvent(
|
|
|
155
166
|
text,
|
|
156
167
|
messageId: env.message_id,
|
|
157
168
|
replyTo: env.reply_to,
|
|
169
|
+
...addressingFacts(env, isPrivate),
|
|
158
170
|
payload: env.payload,
|
|
159
171
|
};
|
|
160
172
|
}
|
|
@@ -16,6 +16,7 @@ interface MessageArgs {
|
|
|
16
16
|
line: string;
|
|
17
17
|
a: Record<string, unknown>;
|
|
18
18
|
ctx: ReturnType<typeof makeCtx>;
|
|
19
|
+
onSent?: (messageId: string) => void;
|
|
19
20
|
station: Station;
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -105,6 +106,10 @@ const unsupported = (station: Station, atts: CanonicalAttachment[]): string =>
|
|
|
105
106
|
.map((a) => kindOf(a.mime ?? '', a.path ?? a.url ?? a.name ?? ''))
|
|
106
107
|
.join(', ')}); send a link in \`text\` instead`;
|
|
107
108
|
|
|
109
|
+
function noteSent(m: MessageArgs, messageId: string | undefined): void {
|
|
110
|
+
if (messageId !== undefined) m.onSent?.(messageId);
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
async function handleSend(m: MessageArgs): Promise<ToolResult> {
|
|
109
114
|
const text = m.a.text as string | undefined;
|
|
110
115
|
const replyTo = m.a.reply_to as string | undefined;
|
|
@@ -125,6 +130,7 @@ async function handleSend(m: MessageArgs): Promise<ToolResult> {
|
|
|
125
130
|
: await sendForwarded(m, text, replyTo, atts);
|
|
126
131
|
if (!sent.labels.length)
|
|
127
132
|
return errResult('send requires `text` or `attachments`');
|
|
133
|
+
noteSent(m, sent.messageId);
|
|
128
134
|
return ok(withId(`sent: ${sent.labels.join(', ')}`, sent.messageId));
|
|
129
135
|
} finally {
|
|
130
136
|
await cleanupAttachments(atts);
|
|
@@ -158,7 +164,7 @@ const MESSAGE_VERBS: Record<string, VerbSpec> = {
|
|
|
158
164
|
};
|
|
159
165
|
|
|
160
166
|
function makeVerbHandler(verb: string, spec: VerbSpec): MessageHandler {
|
|
161
|
-
return async ({ line, a, ctx }) => {
|
|
167
|
+
return async ({ line, a, ctx, onSent }) => {
|
|
162
168
|
const payload: Record<string, unknown> = { line };
|
|
163
169
|
for (const [snake, camel] of spec.args) {
|
|
164
170
|
const value = str(a[snake]);
|
|
@@ -169,7 +175,9 @@ function makeVerbHandler(verb: string, spec: VerbSpec): MessageHandler {
|
|
|
169
175
|
payload[camel] = value;
|
|
170
176
|
}
|
|
171
177
|
const response = await ctx.call(verb, payload);
|
|
172
|
-
|
|
178
|
+
const messageId = messageIdOf(response);
|
|
179
|
+
if (verb === 'reply' && messageId !== undefined) onSent?.(messageId);
|
|
180
|
+
return ok(withId(spec.success, messageId));
|
|
173
181
|
};
|
|
174
182
|
}
|
|
175
183
|
|
|
@@ -181,9 +189,14 @@ const MESSAGE_HANDLERS: Record<string, MessageHandler> = {
|
|
|
181
189
|
),
|
|
182
190
|
};
|
|
183
191
|
|
|
192
|
+
export interface ToolHooks {
|
|
193
|
+
onSent?: (messageId: string) => void;
|
|
194
|
+
}
|
|
195
|
+
|
|
184
196
|
export async function dispatchMessageTool(
|
|
185
197
|
name: string,
|
|
186
198
|
a: Record<string, unknown>,
|
|
199
|
+
hooks: ToolHooks = {},
|
|
187
200
|
): Promise<ToolResult> {
|
|
188
201
|
const line = str(a.line);
|
|
189
202
|
if (!line) return errResult(`${name} requires \`line\``);
|
|
@@ -196,7 +209,7 @@ export async function dispatchMessageTool(
|
|
|
196
209
|
const handler = MESSAGE_HANDLERS[name];
|
|
197
210
|
if (!handler) return errResult(`unknown tool: ${name}`);
|
|
198
211
|
try {
|
|
199
|
-
return await handler({ line, a, ctx: makeCtx(station.name), station });
|
|
212
|
+
return await handler({ line, a, ctx: makeCtx(station.name), station, onSent: hooks.onSent });
|
|
200
213
|
} catch (e) {
|
|
201
214
|
return toErr(name, e);
|
|
202
215
|
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import type { Station, StationTool, ToolResult } from '../stations/types.js';
|
|
13
13
|
import { COMMON_TOOLS, LIST_ACCOUNTS_TOOL } from './tool-schemas.js';
|
|
14
14
|
import { errResult, makeCtx, okJson, toErr } from './ctx.js';
|
|
15
|
-
import { dispatchMessageTool } from './call-tools.js';
|
|
15
|
+
import { dispatchMessageTool, type ToolHooks } from './call-tools.js';
|
|
16
16
|
import { dispatchListMembers } from './member-tools.js';
|
|
17
17
|
import {
|
|
18
18
|
dispatchAddMembers,
|
|
@@ -142,9 +142,10 @@ function logToolFailure(
|
|
|
142
142
|
);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
async function runTool(
|
|
146
|
-
params: { name: string; arguments?: Record<string, unknown> }
|
|
147
|
-
|
|
145
|
+
async function runTool(
|
|
146
|
+
req: { params: { name: string; arguments?: Record<string, unknown> } },
|
|
147
|
+
hooks: ToolHooks,
|
|
148
|
+
): Promise<ToolResult> {
|
|
148
149
|
const name = req.params.name;
|
|
149
150
|
const a = req.params.arguments ?? {};
|
|
150
151
|
|
|
@@ -166,13 +167,14 @@ async function runTool(req: {
|
|
|
166
167
|
|
|
167
168
|
if (name === 'list_accounts') return handleListAccounts(identity);
|
|
168
169
|
|
|
169
|
-
return dispatchMessageTool(name, a);
|
|
170
|
+
return dispatchMessageTool(name, a, hooks);
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
export async function callToolHandler(
|
|
173
|
-
params: { name: string; arguments?: Record<string, unknown> }
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
export async function callToolHandler(
|
|
174
|
+
req: { params: { name: string; arguments?: Record<string, unknown> } },
|
|
175
|
+
hooks: ToolHooks = {},
|
|
176
|
+
): Promise<ToolResult> {
|
|
177
|
+
const result = await runTool(req, hooks);
|
|
176
178
|
if (result.isError === true)
|
|
177
179
|
logToolFailure(req.params.name, req.params.arguments ?? {}, result);
|
|
178
180
|
return result;
|
|
@@ -192,6 +194,7 @@ interface NotifyingExtra {
|
|
|
192
194
|
export function registerToolHandlers(
|
|
193
195
|
server: Server,
|
|
194
196
|
notice?: SchemaNotice,
|
|
197
|
+
hooks: ToolHooks = {},
|
|
195
198
|
): void {
|
|
196
199
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
197
200
|
notice?.markCurrent();
|
|
@@ -206,7 +209,7 @@ export function registerToolHandlers(
|
|
|
206
209
|
notice?.deliver(() =>
|
|
207
210
|
extra.sendNotification({ method: TOOL_LIST_CHANGED }),
|
|
208
211
|
);
|
|
209
|
-
return callToolHandler(req);
|
|
212
|
+
return callToolHandler(req, hooks);
|
|
210
213
|
}) as Parameters<typeof server.setRequestHandler>[1],
|
|
211
214
|
);
|
|
212
215
|
}
|
|
@@ -394,4 +394,7 @@ export const MCP_INSTRUCTIONS =
|
|
|
394
394
|
'straight to metro and never through this conversation), then `send` with ' +
|
|
395
395
|
'attachments:[{upload:"<upload_id>"}]. Inline `data` is for tiny files only, `url` only for ' +
|
|
396
396
|
'an already-public file, and `path` is read on the DAEMON host, not yours. Tool-approval ' +
|
|
397
|
-
'prompts are relayed to the same chat - answer "yes <id>"/"no <id>".'
|
|
397
|
+
'prompts are relayed to the same chat - answer "yes <id>"/"no <id>". A message meant for you ' +
|
|
398
|
+
'carries addressed="direct" (a private chat), "mention" (you were named) or "reply" (it ' +
|
|
399
|
+
'answers one of your own messages); one without `addressed` merely happened in a room you ' +
|
|
400
|
+
'watch, so treat it as context and do not answer unless the person plainly wants you.';
|
|
@@ -45,6 +45,20 @@ export function isOwnEcho(m: Message): boolean {
|
|
|
45
45
|
return isUser(sender) && sender.isSelf;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function replyMeta(m: Message): Record<string, unknown> {
|
|
49
|
+
const replied = m.replyToMessage;
|
|
50
|
+
const target = replied?.id;
|
|
51
|
+
if (replied === null || typeof target !== 'number') return {};
|
|
52
|
+
if (m.isTopicMessage && target === replied.threadId) return {};
|
|
53
|
+
const id = String(target);
|
|
54
|
+
const sender = replied.sender;
|
|
55
|
+
return {
|
|
56
|
+
reply_to: id,
|
|
57
|
+
event: { type: 'reply', replyTo: id },
|
|
58
|
+
reply_to_self: sender?.type === 'user' && sender.isSelf,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
export function envelope(
|
|
49
63
|
accountId: string,
|
|
50
64
|
m: Message,
|
|
@@ -71,6 +85,7 @@ export function envelope(
|
|
|
71
85
|
text: projectText(m),
|
|
72
86
|
is_private: isPrivate,
|
|
73
87
|
has_media: media !== null,
|
|
88
|
+
...replyMeta(m),
|
|
74
89
|
payload: {
|
|
75
90
|
account: accountId,
|
|
76
91
|
message_id: String(m.id),
|
|
@@ -44,6 +44,34 @@ function projectText(m: TgMsg): string {
|
|
|
44
44
|
return [m.text ?? m.caption, ...tags].filter(Boolean).join(' ');
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
const botIdOf = (accountId: string): number | null => {
|
|
48
|
+
const token = accounts.get(accountId)?.cfg.token ?? '';
|
|
49
|
+
const id = Number(token.split(':')[0]);
|
|
50
|
+
return Number.isInteger(id) && id > 0 ? id : null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function mentionsBot(m: TgMsg, botId: number | null, username: string | undefined): boolean {
|
|
54
|
+
const text = m.text ?? m.caption ?? '';
|
|
55
|
+
const handle = username === undefined ? null : `@${username.toLowerCase()}`;
|
|
56
|
+
return [...(m.entities ?? []), ...(m.caption_entities ?? [])].some((e) => {
|
|
57
|
+
if (e.type === 'text_mention') return botId !== null && e.user?.id === botId;
|
|
58
|
+
if (e.type !== 'mention' || handle === null) return false;
|
|
59
|
+
return text.slice(e.offset, e.offset + e.length).toLowerCase() === handle;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function addressing(accountId: string, m: TgMsg): Record<string, unknown> {
|
|
64
|
+
const botId = botIdOf(accountId);
|
|
65
|
+
const topicRoot = m.is_topic_message === true && m.reply_to_message?.message_id === m.message_thread_id;
|
|
66
|
+
const replied = topicRoot ? undefined : m.reply_to_message;
|
|
67
|
+
const replyTo = replied === undefined ? undefined : String(replied.message_id);
|
|
68
|
+
return {
|
|
69
|
+
...(replyTo === undefined ? {} : { reply_to: replyTo, event: { type: 'reply', replyTo } }),
|
|
70
|
+
mentions_self: mentionsBot(m, botId, accounts.get(accountId)?.username),
|
|
71
|
+
reply_to_self: botId !== null && replied?.from?.id === botId,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
export function envelope(accountId: string, m: TgMsg): Record<string, unknown> {
|
|
48
76
|
const { line } = lineForMsg(accountId, m);
|
|
49
77
|
return {
|
|
@@ -60,6 +88,7 @@ export function envelope(accountId: string, m: TgMsg): Record<string, unknown> {
|
|
|
60
88
|
text: projectText(m),
|
|
61
89
|
payload: m,
|
|
62
90
|
is_private: m.chat.type === 'private',
|
|
91
|
+
...addressing(accountId, m),
|
|
63
92
|
};
|
|
64
93
|
}
|
|
65
94
|
|
|
@@ -48,6 +48,12 @@ async function runAccount(acct: Account): Promise<void> {
|
|
|
48
48
|
`telegram-bot[${id}] deleteWebhook: ${errMsg(err)}\n`,
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
|
+
try {
|
|
52
|
+
const me = await tg<{ username?: string }>(id, 'getMe', {});
|
|
53
|
+
if (typeof me.username === 'string') acct.username = me.username;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
process.stderr.write(`telegram-bot[${id}] getMe: ${errMsg(err)}\n`);
|
|
56
|
+
}
|
|
51
57
|
|
|
52
58
|
for (;;) {
|
|
53
59
|
try {
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
export interface TgEntity {
|
|
2
|
+
type: string;
|
|
3
|
+
offset: number;
|
|
4
|
+
length: number;
|
|
5
|
+
user?: { id: number };
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
export interface TgMsg {
|
|
9
|
+
entities?: TgEntity[];
|
|
10
|
+
caption_entities?: TgEntity[];
|
|
11
|
+
reply_to_message?: { message_id: number; from?: { id: number; username?: string; is_bot?: boolean } };
|
|
2
12
|
message_id: number;
|
|
3
13
|
date: number;
|
|
4
14
|
chat: { id: number; type: string; title?: string; first_name?: string };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import makeWASocket, {
|
|
2
|
+
jidNormalizedUser,
|
|
2
3
|
Browsers,
|
|
3
4
|
DisconnectReason,
|
|
4
5
|
fetchLatestWaWebVersion,
|
|
@@ -11,7 +12,7 @@ import { TrainError } from '@metro-labs/mcp/train-error';
|
|
|
11
12
|
import { errMsg } from '@metro-labs/mcp/log';
|
|
12
13
|
import type { WhatsAppAccount } from './types.js';
|
|
13
14
|
import type { InboundMessage, ReactionInput } from './format.js';
|
|
14
|
-
import { toInbound, toReaction, type ReactionEvent } from './parse.js';
|
|
15
|
+
import { toInbound, toReaction, type ReactionEvent, type SelfRef } from './parse.js';
|
|
15
16
|
import { baileysLogger } from './logger.js';
|
|
16
17
|
import { useAccountAuthState } from './auth-state.js';
|
|
17
18
|
import { knownKey, makeKeyCache, targetKey, type KeyCache } from './keys.js';
|
|
@@ -72,13 +73,21 @@ function resetGate(st: State): void {
|
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
function selfRef(st: State, sock: WASocket): SelfRef {
|
|
77
|
+
const jids = new Set<string>();
|
|
78
|
+
const me = sock.user;
|
|
79
|
+
if (me?.id) jids.add(jidNormalizedUser(me.id));
|
|
80
|
+
if (me?.lid) jids.add(jidNormalizedUser(me.lid));
|
|
81
|
+
return { jids, sentByMe: (jid, id) => st.keys.lookup(jid, id)?.fromMe === true };
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
function bindInbound(st: State, sock: WASocket): void {
|
|
76
85
|
sock.ev.on('messages.upsert', ({ messages, type }) => {
|
|
77
86
|
for (const m of messages) st.keys.remember(m.key);
|
|
78
87
|
if (type !== 'notify' || !st.handlers) return;
|
|
79
88
|
for (const m of messages) {
|
|
80
89
|
if (m.key.fromMe) continue;
|
|
81
|
-
const inbound = toInbound(st.account.id, m);
|
|
90
|
+
const inbound = toInbound(st.account.id, m, selfRef(st, sock));
|
|
82
91
|
if (inbound) st.handlers.onMessage(inbound, m);
|
|
83
92
|
}
|
|
84
93
|
});
|
|
@@ -13,6 +13,9 @@ export interface InboundMessage {
|
|
|
13
13
|
isPrivate: boolean;
|
|
14
14
|
pushName?: string;
|
|
15
15
|
media?: WAMediaRef;
|
|
16
|
+
replyTo?: string;
|
|
17
|
+
mentionsSelf?: boolean;
|
|
18
|
+
replyToSelf?: boolean;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export interface ReactionInput {
|
|
@@ -48,6 +51,9 @@ export function envelope(m: InboundMessage): Record<string, unknown> {
|
|
|
48
51
|
message_id: m.messageId,
|
|
49
52
|
text: m.text,
|
|
50
53
|
is_private: m.isPrivate,
|
|
54
|
+
...(m.replyTo === undefined ? {} : { reply_to: m.replyTo, event: { type: 'reply', replyTo: m.replyTo } }),
|
|
55
|
+
...(m.mentionsSelf === true ? { mentions_self: true } : {}),
|
|
56
|
+
...(m.replyToSelf === true ? { reply_to_self: true } : {}),
|
|
51
57
|
payload: {
|
|
52
58
|
account: m.accountId,
|
|
53
59
|
message_id: m.messageId,
|
|
@@ -80,9 +80,52 @@ function senderJidOf(key: WAMessageKey, chatJid: string): string {
|
|
|
80
80
|
return chatJid;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
export interface SelfRef {
|
|
84
|
+
jids: ReadonlySet<string>;
|
|
85
|
+
sentByMe: (chatJid: string, id: string) => boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const QUOTING_NODES = [
|
|
89
|
+
'extendedTextMessage',
|
|
90
|
+
'imageMessage',
|
|
91
|
+
'videoMessage',
|
|
92
|
+
'documentMessage',
|
|
93
|
+
'audioMessage',
|
|
94
|
+
'stickerMessage',
|
|
95
|
+
] as const;
|
|
96
|
+
|
|
97
|
+
function contextInfoOf(inner: proto.IMessage): proto.IContextInfo | undefined {
|
|
98
|
+
for (const node of QUOTING_NODES) {
|
|
99
|
+
const ctx = inner[node]?.contextInfo;
|
|
100
|
+
if (ctx) return ctx;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function quoteFacts(ctx: proto.IContextInfo, replyTo: string | undefined, chatJid: string, self: SelfRef): Addressing {
|
|
106
|
+
const quotedBy = typeof ctx.participant === 'string' ? ctx.participant : '';
|
|
107
|
+
const mentioned = (ctx.mentionedJid ?? []).some((jid) => self.jids.has(jid));
|
|
108
|
+
const replyToSelf = replyTo !== undefined && (self.jids.has(quotedBy) || self.sentByMe(chatJid, replyTo));
|
|
109
|
+
return {
|
|
110
|
+
...(mentioned ? { mentionsSelf: true } : {}),
|
|
111
|
+
...(replyToSelf ? { replyToSelf: true } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type Addressing = Pick<InboundMessage, 'replyTo' | 'mentionsSelf' | 'replyToSelf'>;
|
|
116
|
+
|
|
117
|
+
function addressing(inner: proto.IMessage, chatJid: string, self: SelfRef | undefined): Addressing {
|
|
118
|
+
const ctx = contextInfoOf(inner);
|
|
119
|
+
if (ctx === undefined) return {};
|
|
120
|
+
const replyTo = typeof ctx.stanzaId === 'string' && ctx.stanzaId !== '' ? ctx.stanzaId : undefined;
|
|
121
|
+
const base: Addressing = replyTo === undefined ? {} : { replyTo };
|
|
122
|
+
return self === undefined ? base : { ...base, ...quoteFacts(ctx, replyTo, chatJid, self) };
|
|
123
|
+
}
|
|
124
|
+
|
|
83
125
|
export function toInbound(
|
|
84
126
|
accountId: string,
|
|
85
127
|
m: WAMessage,
|
|
128
|
+
self?: SelfRef,
|
|
86
129
|
): InboundMessage | undefined {
|
|
87
130
|
const chatJid = m.key.remoteJid;
|
|
88
131
|
const messageId = m.key.id;
|
|
@@ -102,6 +145,7 @@ export function toInbound(
|
|
|
102
145
|
isPrivate: isPrivateJid(chatJid),
|
|
103
146
|
...(m.pushName ? { pushName: m.pushName } : {}),
|
|
104
147
|
...(media ? { media } : {}),
|
|
148
|
+
...addressing(inner, chatJid, self),
|
|
105
149
|
};
|
|
106
150
|
}
|
|
107
151
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type RemoteEntry,
|
|
9
9
|
} from './attachments.js';
|
|
10
10
|
import { emitInbound, emitAttachmentSaved } from './emit-core.js';
|
|
11
|
+
import { sentByMe } from './wire.js';
|
|
11
12
|
|
|
12
13
|
export interface EnvelopeCtx {
|
|
13
14
|
accountId: string;
|
|
@@ -150,6 +151,7 @@ function replyPayload(
|
|
|
150
151
|
? c.content
|
|
151
152
|
: `[reply with ${c.contentType?.typeId ?? 'unknown'}]`,
|
|
152
153
|
event: { type: 'reply', replyTo: c.reference },
|
|
154
|
+
...(sentByMe(c.reference) ? { reply_to_self: true } : {}),
|
|
153
155
|
payload: {
|
|
154
156
|
contentType: typeId,
|
|
155
157
|
replyTo: c.reference,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Conversation, DecodedMessage } from '@xmtp/node-sdk';
|
|
2
2
|
import { lineOf, parseLine } from './accounts.js';
|
|
3
|
-
import { emit, mintId, rememberUid, SELF_URI } from './wire.js';
|
|
3
|
+
import { emit, mintId, rememberSent, rememberUid, SELF_URI } from './wire.js';
|
|
4
4
|
import { fcmPushToAll } from './push.js';
|
|
5
5
|
import { emitInbound, emitAttachmentSaved } from './emit-core.js';
|
|
6
6
|
import { typedEnvelope, type EnvelopeCtx } from './emit-payloads.js';
|
|
@@ -23,6 +23,7 @@ export function envelope(
|
|
|
23
23
|
line,
|
|
24
24
|
from: `metro://xmtp/${accountId}/user/${msg.senderInboxId}`,
|
|
25
25
|
message_id: msg.id,
|
|
26
|
+
is_private: typeof (conv as unknown as { peerInboxId?: unknown }).peerInboxId === 'function',
|
|
26
27
|
};
|
|
27
28
|
rememberUid(base.id, msg.id);
|
|
28
29
|
if (typeof c === 'string')
|
|
@@ -53,6 +54,7 @@ export function emitOutbound(
|
|
|
53
54
|
): void {
|
|
54
55
|
const uid = mintId();
|
|
55
56
|
rememberUid(uid, messageId);
|
|
57
|
+
rememberSent(messageId);
|
|
56
58
|
emit({
|
|
57
59
|
kind: 'outbound',
|
|
58
60
|
id: uid,
|
|
@@ -37,3 +37,17 @@ export function resolveMsgId(rawId: string): string {
|
|
|
37
37
|
'(not seen by this train; pass the raw xmtp message_id)',
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
const SENT_MAX = 5000;
|
|
42
|
+
const sentIds = new Set<string>();
|
|
43
|
+
|
|
44
|
+
export function rememberSent(xmtpId: string | undefined): void {
|
|
45
|
+
if (!xmtpId) return;
|
|
46
|
+
sentIds.add(xmtpId);
|
|
47
|
+
if (sentIds.size > SENT_MAX) {
|
|
48
|
+
const oldest = sentIds.values().next().value;
|
|
49
|
+
if (oldest !== undefined) sentIds.delete(oldest);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const sentByMe = (xmtpId: string): boolean => sentIds.has(xmtpId);
|
package/runtime/runtime.json
CHANGED