@stage-labs/metro 0.1.0-beta.171 → 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/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +1 -144
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +1 -45
- 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.ts +1 -10
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +0 -3
- 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
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.172",
|
|
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": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.172",
|
|
4
4
|
"description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -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,
|
|
@@ -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({
|
|
@@ -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",
|
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
import { IdentifierKind } from '@xmtp/node-sdk';
|
|
2
|
-
import {
|
|
3
|
-
accounts,
|
|
4
|
-
accountForCall,
|
|
5
|
-
convOf,
|
|
6
|
-
lineOf,
|
|
7
|
-
parseLine,
|
|
8
|
-
type Account,
|
|
9
|
-
} from './accounts.js';
|
|
2
|
+
import { accountForCall, convOf, lineOf, parseLine } from './accounts.js';
|
|
10
3
|
import { respond } from './wire.js';
|
|
11
4
|
import { TrainError } from '@metro-labs/core/train-error';
|
|
12
|
-
import { pushHandlers } from './actions-push.js';
|
|
13
|
-
import { cleanLabels, labelsBlob } from './labels.js';
|
|
14
5
|
import {
|
|
15
|
-
applyMemberOp,
|
|
16
6
|
buildGroupInfo,
|
|
17
7
|
buildMemberList,
|
|
18
|
-
createGroupWithMembers,
|
|
19
8
|
ethIdentifiers,
|
|
20
|
-
parseMemberArgs,
|
|
21
|
-
resolveMembers,
|
|
22
9
|
warmGroupName,
|
|
23
10
|
} from './conv-helpers.js';
|
|
24
11
|
import { closeGroup } from './actions-close.js';
|
|
@@ -27,17 +14,10 @@ import {
|
|
|
27
14
|
groupCreate,
|
|
28
15
|
groupRemoveMembers,
|
|
29
16
|
} from './group.js';
|
|
30
|
-
import {
|
|
31
|
-
updateChannelMeta,
|
|
32
|
-
applyChannelMeta,
|
|
33
|
-
resolveLine,
|
|
34
|
-
setGithub,
|
|
35
|
-
setPreview,
|
|
36
|
-
} from './actions-meta.js';
|
|
17
|
+
import { updateChannelMeta } from './actions-meta.js';
|
|
37
18
|
|
|
38
19
|
type Args = Record<string, unknown>;
|
|
39
20
|
type Handler = (id: string, args: Args) => Promise<void>;
|
|
40
|
-
type Conv = Awaited<ReturnType<typeof convOf>>['conv'] & object;
|
|
41
21
|
|
|
42
22
|
async function newDm(id: string, args: Args): Promise<void> {
|
|
43
23
|
const { address } = args as { address: string };
|
|
@@ -79,72 +59,6 @@ async function newGroup(id: string, args: Args): Promise<void> {
|
|
|
79
59
|
});
|
|
80
60
|
}
|
|
81
61
|
|
|
82
|
-
async function createRequestGroup(id: string, args: Args): Promise<void> {
|
|
83
|
-
const { name, description, labels } = args as {
|
|
84
|
-
name: string;
|
|
85
|
-
description?: string;
|
|
86
|
-
labels?: string[];
|
|
87
|
-
};
|
|
88
|
-
const acct = accountForCall(args);
|
|
89
|
-
if (!name || typeof name !== 'string')
|
|
90
|
-
throw new TrainError('INVALID_ARGS', 'createRequestGroup requires a `name`');
|
|
91
|
-
const { addrs, inboxes } = resolveMembers(args);
|
|
92
|
-
if (addrs.length === 0 && inboxes.length === 0)
|
|
93
|
-
throw new TrainError(
|
|
94
|
-
'INVALID_ARGS',
|
|
95
|
-
'createRequestGroup requires addresses[] or inboxIds[]',
|
|
96
|
-
);
|
|
97
|
-
const opts: {
|
|
98
|
-
groupName: string;
|
|
99
|
-
groupDescription?: string;
|
|
100
|
-
appData?: string;
|
|
101
|
-
} = { groupName: name };
|
|
102
|
-
if (description) opts.groupDescription = description;
|
|
103
|
-
if (Array.isArray(labels) && labels.length)
|
|
104
|
-
opts.appData = labelsBlob(undefined, labels);
|
|
105
|
-
|
|
106
|
-
const group = await createGroupWithMembers(acct, addrs, inboxes, opts);
|
|
107
|
-
warmGroupName(group.id, name);
|
|
108
|
-
respond(id, {
|
|
109
|
-
result: {
|
|
110
|
-
line: lineOf(acct.cfg.id, group.id),
|
|
111
|
-
id: group.id,
|
|
112
|
-
account: acct.cfg.id,
|
|
113
|
-
name,
|
|
114
|
-
description: description ?? '',
|
|
115
|
-
labels: cleanLabels(labels ?? []),
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function setLabels(id: string, args: Args): Promise<void> {
|
|
121
|
-
const { labels, setName, setDescription, setGithub } = args as {
|
|
122
|
-
line?: string;
|
|
123
|
-
groupId?: string;
|
|
124
|
-
labels: string[];
|
|
125
|
-
setName?: string;
|
|
126
|
-
setDescription?: string;
|
|
127
|
-
setGithub?: string;
|
|
128
|
-
};
|
|
129
|
-
const resolvedLine = resolveLine(args, 'setLabels');
|
|
130
|
-
if (!Array.isArray(labels))
|
|
131
|
-
throw new TrainError('INVALID_ARGS', 'setLabels requires a `labels` array');
|
|
132
|
-
const appData: Record<string, unknown> = { labels };
|
|
133
|
-
if (typeof setGithub === 'string') appData.github = setGithub;
|
|
134
|
-
const result = await applyChannelMeta(
|
|
135
|
-
{ line: resolvedLine, name: setName, description: setDescription, appData },
|
|
136
|
-
'setLabels',
|
|
137
|
-
);
|
|
138
|
-
respond(id, {
|
|
139
|
-
result: {
|
|
140
|
-
line: result.line,
|
|
141
|
-
id: result.id,
|
|
142
|
-
account: result.account,
|
|
143
|
-
labels: result.labels,
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
62
|
async function query(id: string, args: Args): Promise<void> {
|
|
149
63
|
const { line, limit } = args as { line: string; limit?: number };
|
|
150
64
|
const { conv } = await convOf(line);
|
|
@@ -178,30 +92,6 @@ async function query(id: string, args: Args): Promise<void> {
|
|
|
178
92
|
respond(id, { result: { line, count: messages.length, messages } });
|
|
179
93
|
}
|
|
180
94
|
|
|
181
|
-
async function addMembers(id: string, args: Args): Promise<void> {
|
|
182
|
-
const { line, addrs, inboxes } = parseMemberArgs(args, 'addMembers');
|
|
183
|
-
const { acct, conv } = await convOf(line);
|
|
184
|
-
if (!conv)
|
|
185
|
-
throw new TrainError('NOT_FOUND', `conversation not found for ${line}`);
|
|
186
|
-
await applyMemberOp(conv, addrs, inboxes, 'add');
|
|
187
|
-
const info = await buildGroupInfo(line, acct, conv);
|
|
188
|
-
respond(id, {
|
|
189
|
-
result: { ...info, added: { addresses: addrs, inboxIds: inboxes } },
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function removeMembers(id: string, args: Args): Promise<void> {
|
|
194
|
-
const { line, addrs, inboxes } = parseMemberArgs(args, 'removeMembers');
|
|
195
|
-
const { acct, conv } = await convOf(line);
|
|
196
|
-
if (!conv)
|
|
197
|
-
throw new TrainError('NOT_FOUND', `conversation not found for ${line}`);
|
|
198
|
-
await applyMemberOp(conv, addrs, inboxes, 'remove');
|
|
199
|
-
const info = await buildGroupInfo(line, acct, conv);
|
|
200
|
-
respond(id, {
|
|
201
|
-
result: { ...info, removed: { addresses: addrs, inboxIds: inboxes } },
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
95
|
async function groupInfo(id: string, args: Args): Promise<void> {
|
|
206
96
|
const { line } = args as { line: string };
|
|
207
97
|
const { acct, conv } = await convOf(line);
|
|
@@ -218,74 +108,14 @@ async function listMembers(id: string, args: Args): Promise<void> {
|
|
|
218
108
|
respond(id, { result: await buildMemberList(acct, conv) });
|
|
219
109
|
}
|
|
220
110
|
|
|
221
|
-
async function summarizeConv(acct: Account, c: Conv): Promise<unknown> {
|
|
222
|
-
const recent = await c.messages({ limit: 1, direction: 1 }).catch(() => []);
|
|
223
|
-
const last = recent[0];
|
|
224
|
-
let preview = '';
|
|
225
|
-
if (last) {
|
|
226
|
-
const cc: unknown = last.content;
|
|
227
|
-
preview =
|
|
228
|
-
typeof cc === 'string'
|
|
229
|
-
? cc.slice(0, 80)
|
|
230
|
-
: `[${last.contentType?.typeId ?? 'unknown'}]`;
|
|
231
|
-
}
|
|
232
|
-
const isDm =
|
|
233
|
-
typeof (c as unknown as { peerInboxId?: unknown }).peerInboxId ===
|
|
234
|
-
'function';
|
|
235
|
-
const gn = (c as unknown as { name?: string | (() => Promise<string>) }).name;
|
|
236
|
-
const resolvedName =
|
|
237
|
-
typeof gn === 'function' ? await gn().catch(() => '') : (gn ?? '');
|
|
238
|
-
return {
|
|
239
|
-
line: lineOf(acct.cfg.id, c.id),
|
|
240
|
-
id: c.id,
|
|
241
|
-
account: acct.cfg.id,
|
|
242
|
-
version: isDm ? 'dm' : 'group',
|
|
243
|
-
name: resolvedName ?? '',
|
|
244
|
-
lastTs: last
|
|
245
|
-
? new Date(Number(last.sentAtNs / 1_000_000n)).toISOString()
|
|
246
|
-
: null,
|
|
247
|
-
lastPreview: preview,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async function listConvs(id: string, args: Args): Promise<void> {
|
|
252
|
-
const { limit, account } = args as { limit?: number; account?: string };
|
|
253
|
-
const lim = Math.min(Math.max(1, limit ?? 50), 200);
|
|
254
|
-
const targets = account
|
|
255
|
-
? [accounts.get(account)].filter((a): a is Account => a !== undefined)
|
|
256
|
-
: [...accounts.values()];
|
|
257
|
-
const summaries: unknown[] = [];
|
|
258
|
-
for (const acct of targets) {
|
|
259
|
-
await acct.client.conversations.syncAll();
|
|
260
|
-
const all = await acct.client.conversations.list();
|
|
261
|
-
for (const c of all.slice(0, lim))
|
|
262
|
-
summaries.push(await summarizeConv(acct, c));
|
|
263
|
-
}
|
|
264
|
-
summaries.sort((a, b) =>
|
|
265
|
-
((b as { lastTs?: string }).lastTs ?? '').localeCompare(
|
|
266
|
-
(a as { lastTs?: string }).lastTs ?? '',
|
|
267
|
-
),
|
|
268
|
-
);
|
|
269
|
-
respond(id, {
|
|
270
|
-
result: { count: summaries.length, conversations: summaries.slice(0, lim) },
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
111
|
export const convHandlers: Record<string, Handler> = {
|
|
275
112
|
newDm,
|
|
276
113
|
newGroup,
|
|
277
|
-
createRequestGroup,
|
|
278
|
-
addMembers,
|
|
279
|
-
removeMembers,
|
|
280
|
-
setLabels,
|
|
281
|
-
setGithub,
|
|
282
|
-
setPreview,
|
|
283
114
|
updateChannelMeta,
|
|
284
115
|
closeGroup,
|
|
285
116
|
query,
|
|
286
117
|
groupInfo,
|
|
287
118
|
listMembers,
|
|
288
|
-
listConvs,
|
|
289
119
|
groupCreate: async (id, args) => {
|
|
290
120
|
respond(id, { result: await groupCreate(args) });
|
|
291
121
|
},
|
|
@@ -295,5 +125,4 @@ export const convHandlers: Record<string, Handler> = {
|
|
|
295
125
|
groupRemoveMembers: async (id, args) => {
|
|
296
126
|
respond(id, { result: await groupRemoveMembers(args) });
|
|
297
127
|
},
|
|
298
|
-
...pushHandlers,
|
|
299
128
|
};
|
|
@@ -6,7 +6,7 @@ import { TrainError } from '@metro-labs/core/train-error';
|
|
|
6
6
|
|
|
7
7
|
type Args = Record<string, unknown>;
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
function resolveLine(args: Args, verb: string): string {
|
|
10
10
|
const line = (args as { line?: string }).line;
|
|
11
11
|
if (line) return line;
|
|
12
12
|
const groupId = (args as { groupId?: string }).groupId;
|
|
@@ -53,7 +53,7 @@ async function applyMergedAppData(
|
|
|
53
53
|
return readAppData(group.appData);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
async function applyChannelMeta(
|
|
57
57
|
args: {
|
|
58
58
|
line: string;
|
|
59
59
|
name?: string;
|
|
@@ -116,42 +116,3 @@ export async function updateChannelMeta(id: string, args: Args): Promise<void> {
|
|
|
116
116
|
);
|
|
117
117
|
respond(id, { result });
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
export async function setGithub(id: string, args: Args): Promise<void> {
|
|
121
|
-
const line = resolveLine(args, 'setGithub');
|
|
122
|
-
const { url } = args as { url: string };
|
|
123
|
-
if (typeof url !== 'string')
|
|
124
|
-
throw new Error('setGithub requires a `url` string');
|
|
125
|
-
const result = await applyChannelMeta(
|
|
126
|
-
{ line, appData: { github: url } },
|
|
127
|
-
'setGithub',
|
|
128
|
-
);
|
|
129
|
-
respond(id, {
|
|
130
|
-
result: {
|
|
131
|
-
line: result.line,
|
|
132
|
-
id: result.id,
|
|
133
|
-
account: result.account,
|
|
134
|
-
github: result.github,
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export async function setPreview(id: string, args: Args): Promise<void> {
|
|
140
|
-
const line = resolveLine(args, 'setPreview');
|
|
141
|
-
const a = args as { preview?: unknown; url?: unknown };
|
|
142
|
-
const value = typeof a.preview === 'string' ? a.preview : a.url;
|
|
143
|
-
if (typeof value !== 'string')
|
|
144
|
-
throw new Error('setPreview requires a `preview` string');
|
|
145
|
-
const result = await applyChannelMeta(
|
|
146
|
-
{ line, appData: { preview: value } },
|
|
147
|
-
'setPreview',
|
|
148
|
-
);
|
|
149
|
-
respond(id, {
|
|
150
|
-
result: {
|
|
151
|
-
line: result.line,
|
|
152
|
-
id: result.id,
|
|
153
|
-
account: result.account,
|
|
154
|
-
preview: result.preview,
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
}
|
|
@@ -4,20 +4,10 @@ import {
|
|
|
4
4
|
AttachmentCodec,
|
|
5
5
|
type Attachment,
|
|
6
6
|
} from '@xmtp/content-type-remote-attachment';
|
|
7
|
-
import {
|
|
8
|
-
WalletSendCallsCodec,
|
|
9
|
-
type WalletSendCallsParams,
|
|
10
|
-
} from '@xmtp/content-type-wallet-send-calls';
|
|
11
|
-
import { toHex } from 'viem';
|
|
12
7
|
import { convOf } from './accounts.js';
|
|
13
8
|
import { resolveMsgId, respond } from './wire.js';
|
|
14
9
|
import { emitOutbound } from './emit.js';
|
|
15
|
-
import {
|
|
16
|
-
PollCodec,
|
|
17
|
-
buildPollContent,
|
|
18
|
-
SignatureRequestCodec,
|
|
19
|
-
type SignatureRequestContent,
|
|
20
|
-
} from './codecs.js';
|
|
10
|
+
import { PollCodec, buildPollContent } from './codecs.js';
|
|
21
11
|
import { convHandlers } from './actions-conv.js';
|
|
22
12
|
import { normalizeXmtp } from '@metro-labs/core/stations/messaging-normalize';
|
|
23
13
|
import { TrainError } from '@metro-labs/core/train-error';
|
|
@@ -215,121 +205,6 @@ async function sendImage(id: string, args: Args): Promise<void> {
|
|
|
215
205
|
respond(id, { result: { messageId: sentId } });
|
|
216
206
|
}
|
|
217
207
|
|
|
218
|
-
interface TxRequestArgs {
|
|
219
|
-
to: string;
|
|
220
|
-
amountEth?: number;
|
|
221
|
-
data?: string;
|
|
222
|
-
note?: string;
|
|
223
|
-
chainId?: number;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function validateTxAmount(amountEth: number | undefined, hasData: boolean): void {
|
|
227
|
-
if (!hasData && (typeof amountEth !== 'number' || !(amountEth > 0))) {
|
|
228
|
-
throw badArgs(
|
|
229
|
-
'sendTxRequest requires a positive `amountEth` (or `data` for a contract call)',
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
if (amountEth != null && (typeof amountEth !== 'number' || amountEth < 0)) {
|
|
233
|
-
throw badArgs('sendTxRequest `amountEth` must be a non-negative number');
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function validateTxRequest(a: TxRequestArgs): boolean {
|
|
238
|
-
const { to, amountEth, data } = a;
|
|
239
|
-
if (!to || typeof to !== 'string' || !/^0x[0-9a-fA-F]{40}$/.test(to)) {
|
|
240
|
-
throw badArgs('sendTxRequest requires a valid 0x `to` address');
|
|
241
|
-
}
|
|
242
|
-
const hasData = data != null;
|
|
243
|
-
if (
|
|
244
|
-
hasData &&
|
|
245
|
-
(typeof data !== 'string' || !/^0x([0-9a-fA-F]{2})*$/.test(data))
|
|
246
|
-
) {
|
|
247
|
-
throw badArgs('sendTxRequest `data` must be 0x-prefixed hex calldata');
|
|
248
|
-
}
|
|
249
|
-
validateTxAmount(amountEth, hasData);
|
|
250
|
-
return hasData;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function buildTxContent(
|
|
254
|
-
a: TxRequestArgs,
|
|
255
|
-
from: string,
|
|
256
|
-
hasData: boolean,
|
|
257
|
-
): WalletSendCallsParams {
|
|
258
|
-
const { to, amountEth, data, note, chainId } = a;
|
|
259
|
-
const weiHex = amountEth
|
|
260
|
-
? '0x' + BigInt(Math.round(amountEth * 1e18)).toString(16)
|
|
261
|
-
: '0x0';
|
|
262
|
-
return {
|
|
263
|
-
version: '1.0',
|
|
264
|
-
chainId: toHex(chainId ?? 1),
|
|
265
|
-
from: from as `0x${string}`,
|
|
266
|
-
calls: [
|
|
267
|
-
{
|
|
268
|
-
to: to as `0x${string}`,
|
|
269
|
-
value: weiHex as `0x${string}`,
|
|
270
|
-
...(hasData ? { data: data as `0x${string}` } : {}),
|
|
271
|
-
metadata: {
|
|
272
|
-
description: note ?? (hasData ? 'Contract call' : 'Payment request'),
|
|
273
|
-
transactionType: 'transfer',
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
],
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function txRequestLabel(a: TxRequestArgs, hasData: boolean): string {
|
|
281
|
-
const { amountEth, note } = a;
|
|
282
|
-
return hasData
|
|
283
|
-
? `📝 ${note ?? 'Contract call'}${amountEth ? ` (${amountEth} ETH)` : ''}`
|
|
284
|
-
: `💸 ${note ?? 'Payment request'} (${amountEth} ETH)`;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function sendTxRequest(id: string, args: Args): Promise<void> {
|
|
288
|
-
const { line } = args as { line: string };
|
|
289
|
-
const txArgs = args as Args & TxRequestArgs;
|
|
290
|
-
const { acct, conv } = await convOf(line);
|
|
291
|
-
if (!conv) throw noConv(line);
|
|
292
|
-
const hasData = validateTxRequest(txArgs);
|
|
293
|
-
const content = buildTxContent(txArgs, acct.address, hasData);
|
|
294
|
-
const sentId = await conv.send(new WalletSendCallsCodec().encode(content));
|
|
295
|
-
emitOutbound(acct.cfg.id, line, sentId, txRequestLabel(txArgs, hasData));
|
|
296
|
-
respond(id, { result: { messageId: sentId } });
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async function sendSignatureRequest(id: string, args: Args): Promise<void> {
|
|
300
|
-
const { line, kind, eip712, message, description } = args as {
|
|
301
|
-
line: string;
|
|
302
|
-
kind?: 'eip712' | 'personal';
|
|
303
|
-
eip712?: unknown;
|
|
304
|
-
message?: string;
|
|
305
|
-
description?: string;
|
|
306
|
-
};
|
|
307
|
-
const { acct, conv } = await convOf(line);
|
|
308
|
-
if (!conv) throw noConv(line);
|
|
309
|
-
const k: 'eip712' | 'personal' = kind === 'eip712' ? 'eip712' : 'personal';
|
|
310
|
-
if (k === 'eip712' && !eip712)
|
|
311
|
-
throw badArgs(
|
|
312
|
-
'sendSignatureRequest eip712 requires an `eip712` typed-data object',
|
|
313
|
-
);
|
|
314
|
-
if (k === 'personal' && (!message || typeof message !== 'string')) {
|
|
315
|
-
throw badArgs('sendSignatureRequest personal requires a `message` string');
|
|
316
|
-
}
|
|
317
|
-
const content: SignatureRequestContent = {
|
|
318
|
-
id: 'sig_' + Date.now().toString(36),
|
|
319
|
-
kind: k,
|
|
320
|
-
...(k === 'eip712' ? { eip712 } : { message }),
|
|
321
|
-
description,
|
|
322
|
-
};
|
|
323
|
-
const sentId = await conv.send(new SignatureRequestCodec().encode(content));
|
|
324
|
-
emitOutbound(
|
|
325
|
-
acct.cfg.id,
|
|
326
|
-
line,
|
|
327
|
-
sentId,
|
|
328
|
-
`✍️ ${description ?? 'Signature request'}`,
|
|
329
|
-
);
|
|
330
|
-
respond(id, { result: { messageId: sentId } });
|
|
331
|
-
}
|
|
332
|
-
|
|
333
208
|
async function accountsAction(id: string): Promise<void> {
|
|
334
209
|
const { accounts } = await import('./accounts.js');
|
|
335
210
|
respond(id, {
|
|
@@ -362,13 +237,10 @@ const handlers: Record<string, (id: string, args: Args) => Promise<void>> = {
|
|
|
362
237
|
profile: profileAction,
|
|
363
238
|
send,
|
|
364
239
|
ask,
|
|
365
|
-
sendPoll: ask,
|
|
366
240
|
react,
|
|
367
241
|
reply,
|
|
368
242
|
sendAttachment,
|
|
369
243
|
sendImage,
|
|
370
|
-
sendTxRequest,
|
|
371
|
-
sendSignatureRequest,
|
|
372
244
|
edit: (id) => unsupportedVerb(id, 'edit'),
|
|
373
245
|
delete: (id) => unsupportedVerb(id, 'delete'),
|
|
374
246
|
...convHandlers,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Conversation, DecodedMessage } from '@xmtp/node-sdk';
|
|
2
|
-
import { lineOf
|
|
2
|
+
import { lineOf } from './accounts.js';
|
|
3
3
|
import { emit, mintId, rememberSent, rememberUid, SELF_URI } from './wire.js';
|
|
4
|
-
import { fcmPushToAll } from './push.js';
|
|
5
4
|
import { emitInbound, emitAttachmentSaved } from './emit-core.js';
|
|
6
5
|
import { typedEnvelope, type EnvelopeCtx } from './emit-payloads.js';
|
|
7
6
|
import type { StructuredEvent } from '@metro-labs/core/events';
|
|
@@ -69,12 +68,4 @@ export function emitOutbound(
|
|
|
69
68
|
...(event ? { event } : {}),
|
|
70
69
|
payload: { account: accountId },
|
|
71
70
|
});
|
|
72
|
-
void (async (): Promise<void> => {
|
|
73
|
-
const data: Record<string, string> = { line, messageId };
|
|
74
|
-
{
|
|
75
|
-
const p = parseLine(line);
|
|
76
|
-
if (p) data.convId = p.convId.toLowerCase();
|
|
77
|
-
}
|
|
78
|
-
await fcmPushToAll(accountId, data);
|
|
79
|
-
})().catch(() => undefined);
|
|
80
71
|
}
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
import { emitInbound, envelope } from './emit.js';
|
|
10
10
|
import { groupNameFor } from './conv-helpers.js';
|
|
11
11
|
import { senderFieldsNow } from './sender.js';
|
|
12
|
-
import { handleControlDm, pushInbound } from './push.js';
|
|
13
12
|
import { readCalls } from '@metro-labs/core/trains/protocol';
|
|
14
13
|
import { handleCall } from './actions.js';
|
|
15
14
|
|
|
@@ -60,7 +59,6 @@ async function handleStreamMessage(
|
|
|
60
59
|
const { id } = acct.cfg;
|
|
61
60
|
if (msg.senderInboxId === acct.client.inboxId) return;
|
|
62
61
|
if (SILENT_TYPES.has(msg.contentType?.typeId ?? '')) return;
|
|
63
|
-
if (handleControlDm(id, msg)) return;
|
|
64
62
|
const conv = await acct.client.conversations.getConversationById(
|
|
65
63
|
msg.conversationId,
|
|
66
64
|
);
|
|
@@ -75,7 +73,6 @@ async function handleStreamMessage(
|
|
|
75
73
|
env.payload = { ...p, lineName: name };
|
|
76
74
|
}
|
|
77
75
|
emitInbound(id, env);
|
|
78
|
-
pushInbound(id, env, msg, conv);
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
async function runAccount(acct: Account): Promise<void> {
|
|
@@ -11,7 +11,7 @@ export interface GroupLike {
|
|
|
11
11
|
const MAX_LABELS = 16;
|
|
12
12
|
const MAX_LABEL_LEN = 24;
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
function cleanLabels(raw: unknown): string[] {
|
|
15
15
|
if (!Array.isArray(raw)) return [];
|
|
16
16
|
const out: string[] = [];
|
|
17
17
|
const seen = new Set<string>();
|
|
@@ -41,24 +41,6 @@ function parseAppDataObject(
|
|
|
41
41
|
return {};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export function labelsBlob(
|
|
45
|
-
existingAppData: string | undefined,
|
|
46
|
-
labels: string[],
|
|
47
|
-
github?: string,
|
|
48
|
-
): string {
|
|
49
|
-
const existing = parseAppDataObject(existingAppData);
|
|
50
|
-
const blob: Record<string, unknown> = {
|
|
51
|
-
...existing,
|
|
52
|
-
v: 1,
|
|
53
|
-
labels: cleanLabels(labels),
|
|
54
|
-
};
|
|
55
|
-
if (typeof github === 'string') {
|
|
56
|
-
if (github.trim()) blob.github = github.trim();
|
|
57
|
-
else delete blob.github;
|
|
58
|
-
}
|
|
59
|
-
return JSON.stringify(blob);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
44
|
function trimmedString(v: unknown): string | undefined {
|
|
63
45
|
return typeof v === 'string' && v.trim() ? v.trim() : undefined;
|
|
64
46
|
}
|
package/runtime/runtime.json
CHANGED
package/runtime/stations.json
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"@xmtp/content-type-reaction": "^2.0.2",
|
|
13
13
|
"@xmtp/content-type-remote-attachment": "^2.0.4",
|
|
14
14
|
"@xmtp/content-type-reply": "^2.0.3",
|
|
15
|
-
"@xmtp/content-type-wallet-send-calls": "^2.0.0",
|
|
16
15
|
"@xmtp/node-bindings": "1.9.1",
|
|
17
16
|
"@xmtp/node-sdk": "^6.0.0",
|
|
18
17
|
"@zerodev/ecdsa-validator": "5.4.9",
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { accounts } from './accounts.js';
|
|
2
|
-
import { respond } from './wire.js';
|
|
3
|
-
import {
|
|
4
|
-
fcmPushToAll,
|
|
5
|
-
loadPushTokens,
|
|
6
|
-
removePushToken,
|
|
7
|
-
savePushTokens,
|
|
8
|
-
storePushToken,
|
|
9
|
-
} from './push.js';
|
|
10
|
-
|
|
11
|
-
type Args = Record<string, unknown>;
|
|
12
|
-
type Handler = (id: string, args: Args) => Promise<void>;
|
|
13
|
-
|
|
14
|
-
function registerPush(id: string, args: Args): Promise<void> {
|
|
15
|
-
const { token, account, platform, inboxId } = args as {
|
|
16
|
-
token?: string;
|
|
17
|
-
account?: string;
|
|
18
|
-
platform?: string;
|
|
19
|
-
inboxId?: string;
|
|
20
|
-
};
|
|
21
|
-
if (!token || typeof token !== 'string' || token.length < 20) {
|
|
22
|
-
throw new Error('register-push requires a non-empty FCM device token');
|
|
23
|
-
}
|
|
24
|
-
const total = storePushToken({ token, account, platform, inboxId });
|
|
25
|
-
respond(id, { result: { stored: true, total, account: account ?? null } });
|
|
26
|
-
return Promise.resolve();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function listPush(id: string): Promise<void> {
|
|
30
|
-
const tokens = loadPushTokens();
|
|
31
|
-
respond(id, {
|
|
32
|
-
result: {
|
|
33
|
-
count: tokens.length,
|
|
34
|
-
tokens: tokens.map((t) => ({
|
|
35
|
-
token: `${t.token.slice(0, 12)}…${t.token.slice(-6)}`,
|
|
36
|
-
registeredAt: t.registeredAt,
|
|
37
|
-
lastSeenAt: t.lastSeenAt ?? null,
|
|
38
|
-
account: t.account ?? null,
|
|
39
|
-
platform: t.platform ?? null,
|
|
40
|
-
inboxId: t.inboxId ?? null,
|
|
41
|
-
})),
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
return Promise.resolve();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function testPush(id: string, args: Args): Promise<void> {
|
|
48
|
-
const { account } = args as { account?: string };
|
|
49
|
-
const acctId =
|
|
50
|
-
account ??
|
|
51
|
-
(accounts.size === 1 ? [...accounts.keys()][0] : 'default') ??
|
|
52
|
-
'default';
|
|
53
|
-
await fcmPushToAll(acctId, { channelId: 'xmtp', source: 'test-push' });
|
|
54
|
-
const sent = loadPushTokens().filter(
|
|
55
|
-
(t) => !t.account || t.account === acctId,
|
|
56
|
-
).length;
|
|
57
|
-
respond(id, { result: { sent, account: acctId } });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function unregisterPush(id: string, args: Args): Promise<void> {
|
|
61
|
-
const { token } = args as { token: string };
|
|
62
|
-
savePushTokens(loadPushTokens().filter((t) => t.token !== token));
|
|
63
|
-
respond(id, { result: { removed: true } });
|
|
64
|
-
return Promise.resolve();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function disablePush(id: string, args: Args): Promise<void> {
|
|
68
|
-
const { token } = args as { token?: string };
|
|
69
|
-
if (!token || typeof token !== 'string')
|
|
70
|
-
throw new Error('disable-push requires a token');
|
|
71
|
-
const remaining = removePushToken(token);
|
|
72
|
-
respond(id, {
|
|
73
|
-
result: {
|
|
74
|
-
removed: remaining !== -1,
|
|
75
|
-
remaining: remaining === -1 ? null : remaining,
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
return Promise.resolve();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export const pushHandlers: Record<string, Handler> = {
|
|
82
|
-
'register-push': registerPush,
|
|
83
|
-
'list-push': (id) => listPush(id),
|
|
84
|
-
'test-push': testPush,
|
|
85
|
-
'unregister-push': unregisterPush,
|
|
86
|
-
'disable-push': disablePush,
|
|
87
|
-
};
|
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
import { errMsg } from '@metro-labs/core/log';
|
|
2
|
-
import { type DecodedMessage } from '@xmtp/node-sdk';
|
|
3
|
-
import { existsSync, readFileSync, mkdirSync } from 'node:fs';
|
|
4
|
-
import { dirname } from 'node:path';
|
|
5
|
-
import { readJson, writeJson } from '@metro-labs/core/secure-fs';
|
|
6
|
-
import { parseLine } from './accounts.js';
|
|
7
|
-
|
|
8
|
-
const FCM_SVC_PATH = `${process.env.HOME}/.config/metro/firebase-service-account.json`;
|
|
9
|
-
const FCM_TOKENS_PATH = `${process.env.HOME}/.cache/metro/xmtp-push-tokens.json`;
|
|
10
|
-
interface FcmServiceAccount {
|
|
11
|
-
client_email: string;
|
|
12
|
-
private_key: string;
|
|
13
|
-
project_id: string;
|
|
14
|
-
token_uri: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface StoredPushToken {
|
|
18
|
-
token: string;
|
|
19
|
-
registeredAt: string;
|
|
20
|
-
account?: string;
|
|
21
|
-
inboxId?: string;
|
|
22
|
-
inboxIds?: string[];
|
|
23
|
-
platform?: string;
|
|
24
|
-
lastSeenAt?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function loadFcmSvc(): FcmServiceAccount | null {
|
|
28
|
-
if (!existsSync(FCM_SVC_PATH)) return null;
|
|
29
|
-
try {
|
|
30
|
-
return JSON.parse(readFileSync(FCM_SVC_PATH, 'utf8')) as FcmServiceAccount;
|
|
31
|
-
} catch (err) {
|
|
32
|
-
process.stderr.write(
|
|
33
|
-
`fcm: bad service account: ${errMsg(err)}\n`,
|
|
34
|
-
);
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
export function loadPushTokens(): StoredPushToken[] {
|
|
39
|
-
return readJson<StoredPushToken[]>(FCM_TOKENS_PATH, []);
|
|
40
|
-
}
|
|
41
|
-
export function savePushTokens(tokens: StoredPushToken[]): void {
|
|
42
|
-
mkdirSync(dirname(FCM_TOKENS_PATH), { recursive: true });
|
|
43
|
-
writeJson(FCM_TOKENS_PATH, tokens);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function collectInboxIds(
|
|
47
|
-
existing: StoredPushToken | undefined,
|
|
48
|
-
entryInboxId: string | undefined,
|
|
49
|
-
): Set<string> {
|
|
50
|
-
const inboxIds = new Set<string>(existing?.inboxIds ?? []);
|
|
51
|
-
if (existing?.inboxId) inboxIds.add(existing.inboxId);
|
|
52
|
-
if (entryInboxId) inboxIds.add(entryInboxId);
|
|
53
|
-
return inboxIds;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function storePushToken(entry: {
|
|
57
|
-
token: string;
|
|
58
|
-
account?: string;
|
|
59
|
-
inboxId?: string;
|
|
60
|
-
platform?: string;
|
|
61
|
-
}): number {
|
|
62
|
-
const now = new Date().toISOString();
|
|
63
|
-
const all = loadPushTokens();
|
|
64
|
-
const existing = all.find((t) => t.token === entry.token);
|
|
65
|
-
const remaining = all.filter((t) => t.token !== entry.token);
|
|
66
|
-
const inboxIds = collectInboxIds(existing, entry.inboxId);
|
|
67
|
-
const row: StoredPushToken = {
|
|
68
|
-
token: entry.token,
|
|
69
|
-
registeredAt: existing?.registeredAt ?? now,
|
|
70
|
-
lastSeenAt: now,
|
|
71
|
-
};
|
|
72
|
-
if (entry.account) row.account = entry.account;
|
|
73
|
-
if (entry.inboxId) row.inboxId = entry.inboxId;
|
|
74
|
-
if (inboxIds.size) row.inboxIds = [...inboxIds];
|
|
75
|
-
if (entry.platform) row.platform = entry.platform;
|
|
76
|
-
remaining.push(row);
|
|
77
|
-
savePushTokens(remaining);
|
|
78
|
-
return remaining.length;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let cachedAccessToken: { token: string; expiresAt: number } | null = null;
|
|
82
|
-
async function fcmAccessToken(): Promise<string | null> {
|
|
83
|
-
const svc = loadFcmSvc();
|
|
84
|
-
if (!svc) return null;
|
|
85
|
-
if (cachedAccessToken && cachedAccessToken.expiresAt > Date.now() + 60_000)
|
|
86
|
-
return cachedAccessToken.token;
|
|
87
|
-
const { createSign } = await import('node:crypto');
|
|
88
|
-
const enc = (o: unknown): string =>
|
|
89
|
-
Buffer.from(JSON.stringify(o)).toString('base64url');
|
|
90
|
-
const now = Math.floor(Date.now() / 1000);
|
|
91
|
-
const header = { alg: 'RS256', typ: 'JWT' };
|
|
92
|
-
const payload = {
|
|
93
|
-
iss: svc.client_email,
|
|
94
|
-
scope: 'https://www.googleapis.com/auth/firebase.messaging',
|
|
95
|
-
aud: svc.token_uri,
|
|
96
|
-
iat: now,
|
|
97
|
-
exp: now + 3600,
|
|
98
|
-
};
|
|
99
|
-
const sigInput = `${enc(header)}.${enc(payload)}`;
|
|
100
|
-
const signer = createSign('RSA-SHA256');
|
|
101
|
-
signer.update(sigInput);
|
|
102
|
-
signer.end();
|
|
103
|
-
const sig = signer.sign(svc.private_key).toString('base64url');
|
|
104
|
-
const jwt = `${sigInput}.${sig}`;
|
|
105
|
-
const grant = encodeURIComponent(
|
|
106
|
-
'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
107
|
-
);
|
|
108
|
-
const res = await fetch(svc.token_uri, {
|
|
109
|
-
method: 'POST',
|
|
110
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
111
|
-
body: `grant_type=${grant}&assertion=${jwt}`,
|
|
112
|
-
signal: AbortSignal.timeout(15_000),
|
|
113
|
-
});
|
|
114
|
-
if (!res.ok) {
|
|
115
|
-
process.stderr.write(
|
|
116
|
-
`fcm token exchange ${res.status}: ${await res.text()}\n`,
|
|
117
|
-
);
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
const json = (await res.json()) as {
|
|
121
|
-
access_token: string;
|
|
122
|
-
expires_in: number;
|
|
123
|
-
};
|
|
124
|
-
cachedAccessToken = {
|
|
125
|
-
token: json.access_token,
|
|
126
|
-
expiresAt: Date.now() + json.expires_in * 1000,
|
|
127
|
-
};
|
|
128
|
-
return cachedAccessToken.token;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function fcmPushTo(
|
|
132
|
-
deviceToken: string,
|
|
133
|
-
data: Record<string, string> = {},
|
|
134
|
-
): Promise<void> {
|
|
135
|
-
const svc = loadFcmSvc();
|
|
136
|
-
if (!svc) return;
|
|
137
|
-
const at = await fcmAccessToken();
|
|
138
|
-
if (!at) return;
|
|
139
|
-
const url = `https://fcm.googleapis.com/v1/projects/${svc.project_id}/messages:send`;
|
|
140
|
-
const payloadData: Record<string, string> = { channelId: 'xmtp', ...data };
|
|
141
|
-
const res = await fetch(url, {
|
|
142
|
-
method: 'POST',
|
|
143
|
-
headers: {
|
|
144
|
-
'Content-Type': 'application/json',
|
|
145
|
-
Authorization: `Bearer ${at}`,
|
|
146
|
-
},
|
|
147
|
-
body: JSON.stringify({
|
|
148
|
-
message: {
|
|
149
|
-
token: deviceToken,
|
|
150
|
-
android: { priority: 'HIGH' },
|
|
151
|
-
data: payloadData,
|
|
152
|
-
},
|
|
153
|
-
}),
|
|
154
|
-
signal: AbortSignal.timeout(15_000),
|
|
155
|
-
});
|
|
156
|
-
if (!res.ok) {
|
|
157
|
-
const txt = await res.text().catch(() => '');
|
|
158
|
-
const dead =
|
|
159
|
-
res.status === 404 ||
|
|
160
|
-
txt.includes('UNREGISTERED') ||
|
|
161
|
-
txt.includes('NOT_FOUND') ||
|
|
162
|
-
txt.includes('registration-token-not-registered') ||
|
|
163
|
-
txt.includes('NotRegistered');
|
|
164
|
-
if (dead) {
|
|
165
|
-
removePushToken(deviceToken);
|
|
166
|
-
process.stderr.write(
|
|
167
|
-
`fcm: pruned stale token ${deviceToken.slice(0, 12)}…\n`,
|
|
168
|
-
);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
process.stderr.write(`fcm push ${res.status}: ${txt}\n`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function freshestPerAccount(tokens: StoredPushToken[]): StoredPushToken[] {
|
|
176
|
-
const freshness = (t: StoredPushToken): number =>
|
|
177
|
-
new Date(t.lastSeenAt ?? t.registeredAt ?? 0).getTime();
|
|
178
|
-
const byAccount = new Map<string, StoredPushToken>();
|
|
179
|
-
const unscoped: StoredPushToken[] = [];
|
|
180
|
-
for (const t of tokens) {
|
|
181
|
-
if (!t.account) {
|
|
182
|
-
unscoped.push(t);
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
const cur = byAccount.get(t.account);
|
|
186
|
-
if (!cur || freshness(t) >= freshness(cur)) byAccount.set(t.account, t);
|
|
187
|
-
}
|
|
188
|
-
return [...byAccount.values(), ...unscoped];
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export async function fcmPushToAll(
|
|
192
|
-
accountId: string,
|
|
193
|
-
data: Record<string, string> = {},
|
|
194
|
-
excludeInboxId?: string,
|
|
195
|
-
): Promise<void> {
|
|
196
|
-
const tokens = loadPushTokens().filter((t) => {
|
|
197
|
-
if (t.account && t.account !== accountId) return false;
|
|
198
|
-
if (
|
|
199
|
-
excludeInboxId &&
|
|
200
|
-
(t.inboxId === excludeInboxId || t.inboxIds?.includes(excludeInboxId))
|
|
201
|
-
)
|
|
202
|
-
return false;
|
|
203
|
-
return true;
|
|
204
|
-
});
|
|
205
|
-
const deduped = freshestPerAccount(tokens);
|
|
206
|
-
if (deduped.length === 0) return;
|
|
207
|
-
await Promise.all(
|
|
208
|
-
deduped.map((t) =>
|
|
209
|
-
fcmPushTo(t.token, { ...data, account: accountId }).catch(
|
|
210
|
-
() => undefined,
|
|
211
|
-
),
|
|
212
|
-
),
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const METRO_CTRL_PREFIX = 'METRO_CTRL:';
|
|
217
|
-
const CTRL_REGISTER_PUSH = 'register-push';
|
|
218
|
-
const CTRL_DISABLE_PUSH = 'disable-push';
|
|
219
|
-
|
|
220
|
-
export function removePushToken(token: string): number {
|
|
221
|
-
const all = loadPushTokens();
|
|
222
|
-
const remaining = all.filter((t) => t.token !== token);
|
|
223
|
-
if (remaining.length === all.length) return -1;
|
|
224
|
-
savePushTokens(remaining);
|
|
225
|
-
return remaining.length;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function isMetroControlBody(text: unknown): text is string {
|
|
229
|
-
return typeof text === 'string' && text.startsWith(METRO_CTRL_PREFIX);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
interface RegisterPushPayload {
|
|
233
|
-
v?: number;
|
|
234
|
-
token: string;
|
|
235
|
-
platform?: string;
|
|
236
|
-
address?: string;
|
|
237
|
-
inboxId?: string;
|
|
238
|
-
}
|
|
239
|
-
interface DisablePushPayload {
|
|
240
|
-
v?: number;
|
|
241
|
-
token: string;
|
|
242
|
-
address?: string;
|
|
243
|
-
inboxId?: string;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function isValidPushToken(token: unknown): token is string {
|
|
247
|
-
return typeof token === 'string' && token.length >= 20;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function handleRegisterPushCtrl(
|
|
251
|
-
accountId: string,
|
|
252
|
-
msg: DecodedMessage,
|
|
253
|
-
arg: string,
|
|
254
|
-
): void {
|
|
255
|
-
const obj = JSON.parse(arg) as Partial<RegisterPushPayload>;
|
|
256
|
-
if (!obj || !isValidPushToken(obj.token)) {
|
|
257
|
-
process.stderr.write(
|
|
258
|
-
`xmtp[${accountId}]: register-push (ctrl-dm) ignored — bad/short token\n`,
|
|
259
|
-
);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
const total = storePushToken({
|
|
263
|
-
token: obj.token,
|
|
264
|
-
account: accountId,
|
|
265
|
-
inboxId: msg.senderInboxId,
|
|
266
|
-
platform: typeof obj.platform === 'string' ? obj.platform : undefined,
|
|
267
|
-
});
|
|
268
|
-
process.stderr.write(
|
|
269
|
-
`xmtp[${accountId}]: register-push (ctrl-dm) stored token ${obj.token.slice(0, 12)}… ` +
|
|
270
|
-
`from inbox ${msg.senderInboxId.slice(0, 10)}… (v=${obj.v ?? '?'}, ${total} total)\n`,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function handleDisablePushCtrl(accountId: string, arg: string): void {
|
|
275
|
-
const obj = JSON.parse(arg) as Partial<DisablePushPayload>;
|
|
276
|
-
if (!obj || !isValidPushToken(obj.token)) {
|
|
277
|
-
process.stderr.write(
|
|
278
|
-
`xmtp[${accountId}]: disable-push (ctrl-dm) ignored — bad/short token\n`,
|
|
279
|
-
);
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
const remaining = removePushToken(obj.token);
|
|
283
|
-
process.stderr.write(
|
|
284
|
-
`xmtp[${accountId}]: disable-push (ctrl-dm) token ${obj.token.slice(0, 12)}… ` +
|
|
285
|
-
(remaining === -1
|
|
286
|
-
? 'not found (already gone)\n'
|
|
287
|
-
: `removed (v=${obj.v ?? '?'}, ${remaining} remain)\n`),
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function handleControlDm(
|
|
292
|
-
accountId: string,
|
|
293
|
-
msg: DecodedMessage,
|
|
294
|
-
): boolean {
|
|
295
|
-
const body = msg.content;
|
|
296
|
-
if (!isMetroControlBody(body)) return false;
|
|
297
|
-
const rest = body.slice(METRO_CTRL_PREFIX.length);
|
|
298
|
-
const sep = rest.indexOf(':');
|
|
299
|
-
const verb = sep === -1 ? rest : rest.slice(0, sep);
|
|
300
|
-
const arg = sep === -1 ? '' : rest.slice(sep + 1);
|
|
301
|
-
try {
|
|
302
|
-
if (verb === CTRL_REGISTER_PUSH) {
|
|
303
|
-
handleRegisterPushCtrl(accountId, msg, arg);
|
|
304
|
-
} else if (verb === CTRL_DISABLE_PUSH) {
|
|
305
|
-
handleDisablePushCtrl(accountId, arg);
|
|
306
|
-
} else {
|
|
307
|
-
process.stderr.write(
|
|
308
|
-
`xmtp[${accountId}]: unknown control verb '${verb}' — swallowed\n`,
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
} catch (err) {
|
|
312
|
-
process.stderr.write(
|
|
313
|
-
`xmtp[${accountId}]: control DM '${verb}' FAILED: ${errMsg(err)}\n`,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
return true;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export function pushInbound(
|
|
320
|
-
accountId: string,
|
|
321
|
-
env: Record<string, unknown>,
|
|
322
|
-
msg: DecodedMessage,
|
|
323
|
-
conv?: unknown,
|
|
324
|
-
): void {
|
|
325
|
-
const line = typeof env.line === 'string' ? env.line : '';
|
|
326
|
-
const messageId =
|
|
327
|
-
typeof env.message_id === 'string'
|
|
328
|
-
? env.message_id
|
|
329
|
-
: typeof env.id === 'string'
|
|
330
|
-
? env.id
|
|
331
|
-
: '';
|
|
332
|
-
void (async (): Promise<void> => {
|
|
333
|
-
const data: Record<string, string> = {
|
|
334
|
-
line,
|
|
335
|
-
messageId,
|
|
336
|
-
account: accountId,
|
|
337
|
-
};
|
|
338
|
-
{
|
|
339
|
-
const p = parseLine(line);
|
|
340
|
-
if (p) data.convId = p.convId.toLowerCase();
|
|
341
|
-
}
|
|
342
|
-
if (conv) {
|
|
343
|
-
const isDm =
|
|
344
|
-
typeof (conv as { peerInboxId?: unknown }).peerInboxId === 'function';
|
|
345
|
-
if (!isDm) data.isGroup = 'true';
|
|
346
|
-
}
|
|
347
|
-
await fcmPushToAll(accountId, data, msg.senderInboxId);
|
|
348
|
-
})().catch(() => undefined);
|
|
349
|
-
}
|