@stage-labs/metro 0.1.0-beta.263 → 0.1.0-beta.264
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/runtime-install.js +22 -5
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/marketplace/plugin/bin/policy.mjs +1 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/train-boot.ts +22 -1
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/stations/runtime-deps.ts +23 -7
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +32 -1
- package/runtime/node_modules/@metro-labs/whatsapp/src/app-state.ts +49 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client-history.ts +31 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client-types.ts +46 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +75 -77
- package/runtime/node_modules/@metro-labs/whatsapp/src/history-events.ts +127 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/history-files.ts +39 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/history-identity.ts +58 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/history-message.ts +161 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/history-state.ts +344 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/history-types.ts +91 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/history.ts +195 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/index.ts +1 -1
- package/runtime/node_modules/@metro-labs/whatsapp/src/members.ts +94 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/messages.ts +70 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/send-expiry.ts +65 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +4 -0
- package/runtime/patches/5bca2c07d5852d3f58453cff0448dd85da13d3665b61e3d758905b298d432d7c.patch +31 -0
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/train-boot.ts +22 -1
- package/runtime/sdk-runner/runner.json +1 -1
- package/runtime/stations.json +3 -0
package/dist/runtime-install.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
5
|
import { localStations } from './local.js';
|
|
6
6
|
import { STORE_ENTRY, daemonEntry, findBun, runtimeDir } from './runtime.js';
|
|
7
7
|
const MANIFEST_FILE = 'stations.json';
|
|
@@ -24,6 +24,7 @@ export function readManifest(path) {
|
|
|
24
24
|
return {
|
|
25
25
|
core: ranges(raw.core),
|
|
26
26
|
stations: Object.fromEntries(Object.entries(stations).map(([name, deps]) => [name, ranges(deps)])),
|
|
27
|
+
...(raw.patchedDependencies === undefined ? {} : { patchedDependencies: ranges(raw.patchedDependencies) }),
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
export function dependenciesFor(manifest, stations) {
|
|
@@ -32,7 +33,19 @@ export function dependenciesFor(manifest, stations) {
|
|
|
32
33
|
Object.assign(out, manifest.stations[station] ?? {});
|
|
33
34
|
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
+
function packageText(deps, patches) {
|
|
37
|
+
const patchedDependencies = Object.fromEntries(Object.entries(deps).flatMap(([name, version]) => {
|
|
38
|
+
const key = `${name}@${version}`;
|
|
39
|
+
const path = patches[key];
|
|
40
|
+
return path === undefined ? [] : [[key, path]];
|
|
41
|
+
}));
|
|
42
|
+
return `${JSON.stringify({
|
|
43
|
+
name: 'metro-runtime',
|
|
44
|
+
private: true,
|
|
45
|
+
dependencies: deps,
|
|
46
|
+
...(Object.keys(patchedDependencies).length === 0 ? {} : { patchedDependencies }),
|
|
47
|
+
}, null, 2)}\n`;
|
|
48
|
+
}
|
|
36
49
|
export function readOrNull(path) {
|
|
37
50
|
try {
|
|
38
51
|
return readFileSync(path, 'utf8');
|
|
@@ -63,8 +76,8 @@ function syncSources(sources, store) {
|
|
|
63
76
|
writeFileSync(join(store, 'runtime.json'), stamp);
|
|
64
77
|
return true;
|
|
65
78
|
}
|
|
66
|
-
export function installDependencies(store, deps, bun, log) {
|
|
67
|
-
const wanted = packageText(deps);
|
|
79
|
+
export function installDependencies(store, deps, bun, log, patches = {}) {
|
|
80
|
+
const wanted = packageText(deps, patches);
|
|
68
81
|
const marker = join(store, 'node_modules', '.metro-installed');
|
|
69
82
|
if (existsSync(marker) && readOrNull(join(store, 'package.json')) === wanted)
|
|
70
83
|
return false;
|
|
@@ -92,6 +105,10 @@ export function prepareRuntime(opts = {}) {
|
|
|
92
105
|
});
|
|
93
106
|
mkdirSync(join(store, 'trains'), { recursive: true });
|
|
94
107
|
syncSources(sources, store);
|
|
95
|
-
|
|
108
|
+
for (const path of Object.values(manifest.patchedDependencies ?? {})) {
|
|
109
|
+
mkdirSync(dirname(join(store, path)), { recursive: true });
|
|
110
|
+
copyFileSync(join(sources, path), join(store, path));
|
|
111
|
+
}
|
|
112
|
+
installDependencies(store, dependenciesFor(manifest, localStations(opts.agents)), opts.bun ?? findBun(), log, manifest.patchedDependencies);
|
|
96
113
|
return { dir: store, entry: daemonEntry(store), trains: join(store, 'trains'), manifest: manifestPath };
|
|
97
114
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.264",
|
|
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",
|
|
@@ -36,7 +36,7 @@ function targets(snap, name, input) {
|
|
|
36
36
|
if (Array.isArray(snap.ungated) && snap.ungated.includes(name)) return [];
|
|
37
37
|
const account = text(input.account);
|
|
38
38
|
const line = text(input[LINE_FIELD[name] ?? 'line']);
|
|
39
|
-
if (line) return fromLine(line, name in LINE_FIELD ? '' : account);
|
|
39
|
+
if (line) return fromLine(line, name in LINE_FIELD || name === 'list_members' ? '' : account);
|
|
40
40
|
const owners = isRecord(snap.owners) ? snap.owners : {};
|
|
41
41
|
const station = (STATION_ARG.has(name) ? text(input.station) : text(owners[name])) || (account ? stationOfAccount(snap, account) : undefined);
|
|
42
42
|
if (!station) return [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errMsg } from '../log.js';
|
|
1
|
+
import { errMsg, logFatalSync } from '../log.js';
|
|
2
2
|
import { readCalls } from '../protocol.js';
|
|
3
3
|
import { TrainError } from '../train-error.js';
|
|
4
4
|
import type { CallMsg } from './station-runtime.js';
|
|
@@ -18,12 +18,33 @@ export interface ClientTrain<A extends { id: string }, C> {
|
|
|
18
18
|
loadAccounts: () => A[];
|
|
19
19
|
createClient: (account: A) => C;
|
|
20
20
|
startInbound: (client: C) => Promise<void>;
|
|
21
|
+
disconnectClient?: (client: C) => Promise<void>;
|
|
21
22
|
makeHandleCall: (clientFor: (accountId: string) => C) => (msg: CallMsg) => Promise<void>;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function runClientTrain<A extends { id: string }, C>(train: ClientTrain<A, C>): void {
|
|
25
26
|
const clients = new Map<string, C>();
|
|
27
|
+
let stopping = false;
|
|
28
|
+
if (train.disconnectClient) {
|
|
29
|
+
const disconnect = train.disconnectClient;
|
|
30
|
+
const shutdown = (): void => {
|
|
31
|
+
if (stopping) return;
|
|
32
|
+
stopping = true;
|
|
33
|
+
process.stdin.pause();
|
|
34
|
+
Promise.allSettled([...clients.values()].map(async (client) => { await disconnect(client); })).then((results) => {
|
|
35
|
+
const errors = results.flatMap((result) => result.status === 'rejected' ? [errMsg(result.reason)] : []);
|
|
36
|
+
if (errors.length) logFatalSync({ station: train.station, errors }, 'train: shutdown failed');
|
|
37
|
+
process.exit(errors.length ? 1 : 0);
|
|
38
|
+
}).catch((err: unknown) => {
|
|
39
|
+
logFatalSync({ station: train.station, err: errMsg(err) }, 'train: shutdown failed');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
process.on('SIGTERM', shutdown);
|
|
44
|
+
process.on('SIGINT', shutdown);
|
|
45
|
+
}
|
|
26
46
|
const clientFor = (accountId: string): C => {
|
|
47
|
+
if (stopping) throw new TrainError('train_stopping', `${train.station} is shutting down`);
|
|
27
48
|
const known = clients.get(accountId);
|
|
28
49
|
if (known !== undefined) return known;
|
|
29
50
|
const account = train.accounts.get(accountId);
|
|
@@ -13,9 +13,9 @@ type ChannelTarget = Extract<PolicyTarget, { kind: 'channel' }>;
|
|
|
13
13
|
|
|
14
14
|
export const UNGATED = new Set<string>(['list_accounts', 'create_upload']);
|
|
15
15
|
|
|
16
|
-
function fromLine(line: string, override
|
|
16
|
+
function fromLine(line: string, override = ''): ChannelTarget[] {
|
|
17
17
|
const acct = accountFromLine(line);
|
|
18
|
-
return acct === undefined ? [] : [{ kind: 'channel', station: acct.station, account: override
|
|
18
|
+
return acct === undefined ? [] : [{ kind: 'channel', station: acct.station, account: override || acct.accountId }];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function channelTargets(name: string, a: Record<string, unknown>): ChannelTarget[] {
|
|
@@ -26,7 +26,7 @@ function channelTargets(name: string, a: Record<string, unknown>): ChannelTarget
|
|
|
26
26
|
}
|
|
27
27
|
const account = str(a.account);
|
|
28
28
|
const line = str(a.line);
|
|
29
|
-
if (line) return fromLine(line,
|
|
29
|
+
if (line) return fromLine(line, name === 'list_members' ? undefined : account);
|
|
30
30
|
const station = stationForTool(name, a) ?? (account ? stationOfAccount(account) : undefined);
|
|
31
31
|
if (station === undefined) return [];
|
|
32
32
|
if (account) return [{ kind: 'channel', station, account }];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { log } from '@metro-labs/core/log';
|
|
5
5
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
@@ -11,6 +11,7 @@ const INSTALL_TIMEOUT_MS = 15 * 60_000;
|
|
|
11
11
|
export interface RuntimeManifest {
|
|
12
12
|
core: Record<string, string>;
|
|
13
13
|
stations: Record<string, Record<string, string>>;
|
|
14
|
+
patchedDependencies?: Record<string, string>;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface RuntimeStore {
|
|
@@ -30,6 +31,7 @@ export function readManifest(path: string): RuntimeManifest {
|
|
|
30
31
|
return {
|
|
31
32
|
core: ranges(raw.core),
|
|
32
33
|
stations: Object.fromEntries(Object.entries(stations).map(([name, deps]) => [name, ranges(deps)])),
|
|
34
|
+
...(raw.patchedDependencies === undefined ? {} : { patchedDependencies: ranges(raw.patchedDependencies) }),
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -46,8 +48,21 @@ export function dependenciesFor(manifest: RuntimeManifest, stations: Iterable<st
|
|
|
46
48
|
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
function packageText(deps: Record<string, string>, patches: Record<string, string>): string {
|
|
52
|
+
const patchedDependencies = Object.fromEntries(
|
|
53
|
+
Object.entries(deps).flatMap(([name, version]) => {
|
|
54
|
+
const key = `${name}@${version}`;
|
|
55
|
+
const path = patches[key];
|
|
56
|
+
return path === undefined ? [] : [[key, path]];
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
return `${JSON.stringify({
|
|
60
|
+
name: 'metro-runtime',
|
|
61
|
+
private: true,
|
|
62
|
+
dependencies: deps,
|
|
63
|
+
...(Object.keys(patchedDependencies).length === 0 ? {} : { patchedDependencies }),
|
|
64
|
+
}, null, 2)}\n`;
|
|
65
|
+
}
|
|
51
66
|
|
|
52
67
|
function current(dir: string): string | null {
|
|
53
68
|
try {
|
|
@@ -58,9 +73,10 @@ function current(dir: string): string | null {
|
|
|
58
73
|
}
|
|
59
74
|
|
|
60
75
|
export function installRuntime(store: RuntimeStore, stations: Iterable<string>): boolean {
|
|
61
|
-
const wanted = packageText(dependenciesFor(store.manifest, stations));
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
76
|
+
const wanted = packageText(dependenciesFor(store.manifest, stations), store.manifest.patchedDependencies ?? {});
|
|
77
|
+
const marker = join(store.dir, 'node_modules', '.metro-installed');
|
|
78
|
+
if (existsSync(marker) && current(store.dir) === wanted) return false;
|
|
79
|
+
rmSync(marker, { force: true });
|
|
64
80
|
mkdirSync(store.dir, { recursive: true });
|
|
65
81
|
writeFileSync(join(store.dir, 'package.json'), wanted);
|
|
66
82
|
log.info({ dir: store.dir }, 'runtime: installing the channel SDKs this machine needs');
|
|
@@ -71,7 +87,7 @@ export function installRuntime(store: RuntimeStore, stations: Iterable<string>):
|
|
|
71
87
|
});
|
|
72
88
|
if (run.error !== undefined || run.status !== 0)
|
|
73
89
|
throw new Error(`bun install failed in ${store.dir}: ${run.error?.message ?? `exit ${String(run.status)}`}`);
|
|
74
|
-
writeFileSync(
|
|
90
|
+
writeFileSync(marker, `${new Date().toISOString()}\n`);
|
|
75
91
|
return true;
|
|
76
92
|
}
|
|
77
93
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type StationHandler,
|
|
9
9
|
} from '@metro-labs/core/stations/station-runtime';
|
|
10
10
|
import { messagingAliases } from '@metro-labs/core/stations/messaging-normalize';
|
|
11
|
-
import { accountFor, accounts, targetOf } from './accounts.js';
|
|
11
|
+
import { accountFor, accounts, lineOf, targetOf } from './accounts.js';
|
|
12
12
|
import { phoneNumberOf, senderLookup } from './resolve.js';
|
|
13
13
|
import { assertImage, fieldsOf, parseProfileChange, type ProfileApplied } from '@metro-labs/core/stations/profile';
|
|
14
14
|
import type { WAClient } from './client.js';
|
|
@@ -117,6 +117,35 @@ function makeSend(clientFor: ClientFor): StationHandler {
|
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function makeRead(clientFor: ClientFor): StationHandler {
|
|
121
|
+
return async (id, args) => {
|
|
122
|
+
const { accountId, client, jid } = resolve(args, clientFor);
|
|
123
|
+
const page = await guard(() => client.read(jid, {
|
|
124
|
+
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
125
|
+
before: str(args.before),
|
|
126
|
+
since: str(args.since),
|
|
127
|
+
}));
|
|
128
|
+
const messages = page.messages.map((row) => ({
|
|
129
|
+
id: row.messageId,
|
|
130
|
+
ts: row.timestamp,
|
|
131
|
+
...(row.senderJid ? { from: lineOf(accountId, `user/${row.senderJid}`) } : {}),
|
|
132
|
+
self: row.fromMe,
|
|
133
|
+
text: row.text,
|
|
134
|
+
...(row.truncated ? { truncated: true } : {}),
|
|
135
|
+
...(row.attachments ? { attachments: row.attachments } : {}),
|
|
136
|
+
}));
|
|
137
|
+
respond(id, { result: { ...page, line: lineOf(accountId, jid), account: accountId, messages } });
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function makeListMembers(clientFor: ClientFor): StationHandler {
|
|
142
|
+
return async (id, args) => {
|
|
143
|
+
const { client, jid } = resolve({ line: args.line }, clientFor);
|
|
144
|
+
const result = await guard(() => client.listMembers(jid, typeof args.limit === 'number' ? args.limit : undefined));
|
|
145
|
+
respond(id, { result });
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
120
149
|
function makeReact(clientFor: ClientFor): StationHandler {
|
|
121
150
|
return async (id, args) => {
|
|
122
151
|
const { accountId, client, jid } = resolve(args, clientFor);
|
|
@@ -208,6 +237,8 @@ export function makeHandleCall(
|
|
|
208
237
|
handlers: {
|
|
209
238
|
accounts: makeAccounts(clientFor),
|
|
210
239
|
send: makeSend(clientFor),
|
|
240
|
+
read: makeRead(clientFor),
|
|
241
|
+
listMembers: makeListMembers(clientFor),
|
|
211
242
|
react: makeReact(clientFor),
|
|
212
243
|
typing: makeTyping(clientFor),
|
|
213
244
|
edit: makeEdit(clientFor),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
2
|
+
import { isPnUser, isLidUser, makeEventBuffer, processSyncAction, type BaileysEventEmitter, type Contact } from 'baileys';
|
|
3
|
+
import type { History } from './history.js';
|
|
4
|
+
import { baileysLogger } from './logger.js';
|
|
5
|
+
|
|
6
|
+
interface AppStateHooks {
|
|
7
|
+
history: History;
|
|
8
|
+
current(): boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function supportsHistoryDeletion(processAction = processSyncAction): boolean {
|
|
12
|
+
const ev = makeEventBuffer(baileysLogger('history-support'));
|
|
13
|
+
const id = 'history-support@g.us';
|
|
14
|
+
const messageRange = { lastMessageTimestamp: 1 };
|
|
15
|
+
let received = 0;
|
|
16
|
+
ev.on('chats.clear', (event) => {
|
|
17
|
+
if (event.id === id && event.messageRange === messageRange) received++;
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
for (const initial of [false, true]) {
|
|
21
|
+
for (const type of ['clearChat', 'deleteChat']) {
|
|
22
|
+
const value = type === 'clearChat' ? { clearChatAction: { messageRange } } : { deleteChatAction: { messageRange } };
|
|
23
|
+
const before = received;
|
|
24
|
+
processAction({ index: [type, id], syncAction: { value } }, ev, { id }, initial ? { accountSettings: { unarchiveChats: false } } : undefined);
|
|
25
|
+
if (received !== before + 1) return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
log.warn({ err: errMsg(err) }, 'whatsapp: local history deletion support check failed');
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function noteHistoryContact(history: History, contact: Partial<Contact>): void {
|
|
36
|
+
const pn = contact.phoneNumber ?? (isPnUser(contact.id) ? contact.id : undefined);
|
|
37
|
+
const lid = contact.lid ?? (isLidUser(contact.id) ? contact.id : undefined);
|
|
38
|
+
if (pn && lid) history.alias(pn, lid);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function bindAppState(ev: BaileysEventEmitter, hooks: AppStateHooks): void {
|
|
42
|
+
ev.on('lid-mapping.update', ({ pn, lid }) => {
|
|
43
|
+
if (hooks.current()) hooks.history.alias(pn, lid);
|
|
44
|
+
});
|
|
45
|
+
ev.on('chats.clear', ({ id, messageRange }) => {
|
|
46
|
+
if (!hooks.current()) return;
|
|
47
|
+
hooks.history.clearRange(id, messageRange ?? {});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { log } from '@metro-labs/core/log';
|
|
2
|
+
import { TrainError } from '@metro-labs/core/train-error';
|
|
3
|
+
import { supportsHistoryDeletion } from './app-state.js';
|
|
4
|
+
import { createHistory, type History } from './history.js';
|
|
5
|
+
|
|
6
|
+
const UNAVAILABLE = 'WhatsApp local history is unavailable because this runtime lacks the deletion patch. Restart the Metro service parent to install the patched runtime; a page Restart is not enough. Prior history was invalidated to avoid stale deleted messages.';
|
|
7
|
+
const ignore = (): void => undefined;
|
|
8
|
+
|
|
9
|
+
export function createClientHistory(accountId: string, supported = supportsHistoryDeletion()): History {
|
|
10
|
+
const history = createHistory(accountId);
|
|
11
|
+
if (supported) return history;
|
|
12
|
+
try {
|
|
13
|
+
history.invalidate();
|
|
14
|
+
} finally {
|
|
15
|
+
history.close();
|
|
16
|
+
}
|
|
17
|
+
log.warn({ accountId }, UNAVAILABLE);
|
|
18
|
+
return {
|
|
19
|
+
read() { throw new TrainError('whatsapp_history_unavailable', UNAVAILABLE); },
|
|
20
|
+
ingest: ignore,
|
|
21
|
+
ingestSent: ignore,
|
|
22
|
+
alias: ignore,
|
|
23
|
+
clearRange: ignore,
|
|
24
|
+
invalidate: ignore,
|
|
25
|
+
update: ignore,
|
|
26
|
+
deleteMessages: ignore,
|
|
27
|
+
edit: ignore,
|
|
28
|
+
flush: ignore,
|
|
29
|
+
close: ignore,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type makeWASocket from 'baileys';
|
|
2
|
+
import type { fetchLatestWaWebVersion, WAMessage } from 'baileys';
|
|
3
|
+
import type { MemberList } from '@metro-labs/core/stations/types';
|
|
4
|
+
import type { ProfileChange } from '@metro-labs/core/stations/profile';
|
|
5
|
+
import type { SenderProfile } from '@metro-labs/core/stations/sender-profile';
|
|
6
|
+
import type { WhatsAppAccount } from './types.js';
|
|
7
|
+
import type { InboundMessage, ReactionInput } from './format.js';
|
|
8
|
+
import type { SenderFound } from './resolve.js';
|
|
9
|
+
import type { HistoryOptions, HistoryPage } from './history.js';
|
|
10
|
+
|
|
11
|
+
export interface InboundHandlers {
|
|
12
|
+
onMessage(m: InboundMessage, raw: WAMessage): void;
|
|
13
|
+
onReaction(r: ReactionInput): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WAMedia {
|
|
17
|
+
kind: string;
|
|
18
|
+
path: string;
|
|
19
|
+
mime: string;
|
|
20
|
+
name: string;
|
|
21
|
+
caption?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WAClient {
|
|
25
|
+
account: WhatsAppAccount;
|
|
26
|
+
self(): string | null;
|
|
27
|
+
start(handlers: InboundHandlers): Promise<void>;
|
|
28
|
+
read(jid: string, options?: HistoryOptions): Promise<HistoryPage>;
|
|
29
|
+
listMembers(jid: string, limit?: number): Promise<MemberList>;
|
|
30
|
+
sendText(jid: string, text: string, quotedId?: string): Promise<string>;
|
|
31
|
+
sendMedia(jid: string, media: WAMedia, quotedId?: string): Promise<string>;
|
|
32
|
+
sendReaction(jid: string, messageId: string, emoji: string): Promise<void>;
|
|
33
|
+
showTyping(jid: string): Promise<void>;
|
|
34
|
+
editMessage(jid: string, messageId: string, text: string): Promise<void>;
|
|
35
|
+
deleteMessage(jid: string, messageId: string): Promise<void>;
|
|
36
|
+
reuploadMedia(m: WAMessage): Promise<WAMessage>;
|
|
37
|
+
lookupSender(number: string): Promise<SenderFound>;
|
|
38
|
+
setProfile(change: ProfileChange): Promise<void>;
|
|
39
|
+
senderProfile(jid: string): Promise<SenderProfile | null>;
|
|
40
|
+
disconnect(): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ClientRuntime {
|
|
44
|
+
makeSocket: typeof makeWASocket;
|
|
45
|
+
fetchVersion: typeof fetchLatestWaWebVersion;
|
|
46
|
+
}
|
|
@@ -11,13 +11,17 @@ import makeWASocket, {
|
|
|
11
11
|
import { TrainError } from '@metro-labs/core/train-error';
|
|
12
12
|
import { errMsg } from '@metro-labs/core/log';
|
|
13
13
|
import type { WhatsAppAccount } from './types.js';
|
|
14
|
-
import type {
|
|
15
|
-
|
|
16
|
-
import type { ProfileChange } from '@metro-labs/core/stations/profile';
|
|
14
|
+
import type { InboundHandlers, WAClient, WAMedia, ClientRuntime } from './client-types.js';
|
|
15
|
+
export type { InboundHandlers, WAClient, WAMedia } from './client-types.js';
|
|
17
16
|
import { makeProfileCache, nonEmpty, type SenderProfile } from '@metro-labs/core/stations/sender-profile';
|
|
18
|
-
import
|
|
17
|
+
import type { SelfRef } from './parse.js';
|
|
18
|
+
import type { History } from './history.js';
|
|
19
|
+
import { createClientHistory } from './client-history.js';
|
|
20
|
+
import { bindMessages } from './messages.js';
|
|
21
|
+
import { listMembers } from './members.js';
|
|
22
|
+
import { SendExpiry } from './send-expiry.js';
|
|
19
23
|
import { baileysLogger } from './logger.js';
|
|
20
|
-
import { makeNameBook, nameFiles,
|
|
24
|
+
import { makeNameBook, nameFiles, phoneOf, type NameBook } from './names.js';
|
|
21
25
|
import { useAccountAuthState } from './auth-state.js';
|
|
22
26
|
import { knownKey, makeKeyCache, targetKey, type KeyCache } from './keys.js';
|
|
23
27
|
import { makeOutbox, type Outbox } from './outbox.js';
|
|
@@ -30,41 +34,15 @@ import {
|
|
|
30
34
|
type AckWatch,
|
|
31
35
|
} from './ack.js';
|
|
32
36
|
|
|
33
|
-
export interface InboundHandlers {
|
|
34
|
-
onMessage(m: InboundMessage, raw: WAMessage): void;
|
|
35
|
-
onReaction(r: ReactionInput): void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface WAMedia {
|
|
39
|
-
kind: string;
|
|
40
|
-
path: string;
|
|
41
|
-
mime: string;
|
|
42
|
-
name: string;
|
|
43
|
-
caption?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface WAClient {
|
|
47
|
-
account: WhatsAppAccount;
|
|
48
|
-
self(): string | null;
|
|
49
|
-
start(handlers: InboundHandlers): Promise<void>;
|
|
50
|
-
sendText(jid: string, text: string, quotedId?: string): Promise<string>;
|
|
51
|
-
sendMedia(jid: string, media: WAMedia, quotedId?: string): Promise<string>;
|
|
52
|
-
sendReaction(jid: string, messageId: string, emoji: string): Promise<void>;
|
|
53
|
-
showTyping(jid: string): Promise<void>;
|
|
54
|
-
editMessage(jid: string, messageId: string, text: string): Promise<void>;
|
|
55
|
-
deleteMessage(jid: string, messageId: string): Promise<void>;
|
|
56
|
-
reuploadMedia(m: WAMessage): Promise<WAMessage>;
|
|
57
|
-
lookupSender(number: string): Promise<SenderFound>;
|
|
58
|
-
setProfile(change: ProfileChange): Promise<void>;
|
|
59
|
-
senderProfile(jid: string): Promise<SenderProfile | null>;
|
|
60
|
-
disconnect(): Promise<void>;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
37
|
interface State {
|
|
64
38
|
account: WhatsAppAccount;
|
|
39
|
+
runtime: ClientRuntime;
|
|
40
|
+
history: History;
|
|
41
|
+
expiry: SendExpiry;
|
|
65
42
|
handlers?: InboundHandlers;
|
|
66
43
|
sock?: WASocket;
|
|
67
44
|
closed: boolean;
|
|
45
|
+
closing?: Promise<void>;
|
|
68
46
|
openResolve?: () => void;
|
|
69
47
|
openPromise: Promise<void>;
|
|
70
48
|
keys: KeyCache;
|
|
@@ -91,32 +69,14 @@ function selfRef(st: State, sock: WASocket): SelfRef {
|
|
|
91
69
|
}
|
|
92
70
|
|
|
93
71
|
function bindInbound(st: State, sock: WASocket): void {
|
|
94
|
-
sock
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
sock.ev.on('contacts.upsert', (contacts) => {
|
|
105
|
-
for (const c of contacts) noteContact(st.names, c);
|
|
106
|
-
});
|
|
107
|
-
sock.ev.on('contacts.update', (contacts) => {
|
|
108
|
-
for (const c of contacts) noteContact(st.names, c);
|
|
109
|
-
});
|
|
110
|
-
sock.ev.on('messaging-history.set', ({ contacts, messages }) => {
|
|
111
|
-
for (const c of contacts) noteContact(st.names, c);
|
|
112
|
-
for (const m of messages) if (m.key.fromMe !== true) st.names.note(m.key.participant ?? m.key.remoteJid, m.pushName);
|
|
113
|
-
});
|
|
114
|
-
sock.ev.on('messages.reaction', (events: ReactionEvent[]) => {
|
|
115
|
-
if (!st.handlers) return;
|
|
116
|
-
for (const event of events) {
|
|
117
|
-
const reaction = toReaction(st.account.id, event);
|
|
118
|
-
if (reaction) st.handlers.onReaction(reaction);
|
|
119
|
-
}
|
|
72
|
+
bindMessages(sock, {
|
|
73
|
+
accountId: st.account.id,
|
|
74
|
+
keys: st.keys,
|
|
75
|
+
names: st.names,
|
|
76
|
+
history: st.history,
|
|
77
|
+
current: () => !st.closed && st.sock === sock,
|
|
78
|
+
handlers: () => st.handlers,
|
|
79
|
+
self: () => selfRef(st, sock),
|
|
120
80
|
});
|
|
121
81
|
}
|
|
122
82
|
|
|
@@ -197,19 +157,22 @@ async function connect(st: State): Promise<void> {
|
|
|
197
157
|
st.account.credentials,
|
|
198
158
|
st.account.id,
|
|
199
159
|
);
|
|
200
|
-
|
|
160
|
+
if (st.closed) return;
|
|
161
|
+
const { version, error } = await st.runtime.fetchVersion({});
|
|
162
|
+
if (st.closed) return;
|
|
201
163
|
if (error) {
|
|
202
164
|
throw new TrainError(
|
|
203
165
|
'whatsapp_connect',
|
|
204
166
|
`failed to fetch WhatsApp web version: ${errMsg(error)}`,
|
|
205
167
|
);
|
|
206
168
|
}
|
|
207
|
-
const sock =
|
|
169
|
+
const sock = st.runtime.makeSocket({
|
|
208
170
|
version,
|
|
209
171
|
auth: state,
|
|
210
172
|
browser: Browsers.macOS('Safari'),
|
|
211
173
|
markOnlineOnConnect: false,
|
|
212
174
|
syncFullHistory: false,
|
|
175
|
+
emitOwnEvents: false,
|
|
213
176
|
logger: baileysLogger(st.account.id),
|
|
214
177
|
getMessage: (key) => Promise.resolve(servedFromOutbox(st, key)),
|
|
215
178
|
});
|
|
@@ -217,6 +180,7 @@ async function connect(st: State): Promise<void> {
|
|
|
217
180
|
sock.ev.on('creds.update', () => void saveCreds());
|
|
218
181
|
bindConnection(st, sock);
|
|
219
182
|
bindInbound(st, sock);
|
|
183
|
+
st.expiry.bind(sock, () => !st.closed && st.sock === sock);
|
|
220
184
|
bindDelivery(st, sock);
|
|
221
185
|
}
|
|
222
186
|
|
|
@@ -233,7 +197,7 @@ async function lidFor(st: State, sock: WASocket, jid: string): Promise<string |
|
|
|
233
197
|
|
|
234
198
|
async function ready(st: State): Promise<WASocket> {
|
|
235
199
|
await st.openPromise;
|
|
236
|
-
if (!st.sock) throw new TrainError('whatsapp_call', 'socket not connected');
|
|
200
|
+
if (st.closed || !st.sock) throw new TrainError('whatsapp_call', 'socket not connected');
|
|
237
201
|
return st.sock;
|
|
238
202
|
}
|
|
239
203
|
|
|
@@ -244,10 +208,13 @@ async function send(
|
|
|
244
208
|
opts?: SendOpts,
|
|
245
209
|
): Promise<string> {
|
|
246
210
|
const sock = await ready(st);
|
|
247
|
-
const
|
|
248
|
-
st.
|
|
249
|
-
|
|
250
|
-
const
|
|
211
|
+
const expiration = await st.expiry.forSend(sock, jid, content);
|
|
212
|
+
if (st.closed || st.sock !== sock) throw new TrainError('whatsapp_call', 'socket not connected');
|
|
213
|
+
const sent = await sock.sendMessage(jid, content, expiration === undefined ? opts : { ...opts, ephemeralExpiration: expiration });
|
|
214
|
+
const key = sent?.key;
|
|
215
|
+
st.keys.remember(key);
|
|
216
|
+
st.outbox.remember(key, sent?.message);
|
|
217
|
+
const messageId = key?.id;
|
|
251
218
|
if (!messageId)
|
|
252
219
|
throw new TrainError(
|
|
253
220
|
'whatsapp_call',
|
|
@@ -256,9 +223,15 @@ async function send(
|
|
|
256
223
|
const ack = await st.acks.wait(messageId, ACK_WAIT_MS);
|
|
257
224
|
const refused = ack ? rejection(ack) : undefined;
|
|
258
225
|
if (refused) throw refused;
|
|
226
|
+
recordSent(st, sock, sent, expiration);
|
|
259
227
|
return messageId;
|
|
260
228
|
}
|
|
261
229
|
|
|
230
|
+
function recordSent(st: State, sock: WASocket, sent: WAMessage | undefined, expiration?: number): void {
|
|
231
|
+
if (!sent || st.closed) return;
|
|
232
|
+
st.history.ingestSent([sent], sock.user?.id ? jidNormalizedUser(sock.user.id) : undefined, expiration);
|
|
233
|
+
}
|
|
234
|
+
|
|
262
235
|
function quotedOpts(st: State, jid: string, quotedId: string): SendOpts {
|
|
263
236
|
return {
|
|
264
237
|
quoted: {
|
|
@@ -300,9 +273,24 @@ async function readProfile(st: State, jid: string): Promise<SenderProfile> {
|
|
|
300
273
|
};
|
|
301
274
|
}
|
|
302
275
|
|
|
303
|
-
|
|
276
|
+
async function disconnect(st: State): Promise<void> {
|
|
277
|
+
st.closed = true;
|
|
278
|
+
st.openResolve?.();
|
|
279
|
+
try {
|
|
280
|
+
st.history.close();
|
|
281
|
+
} finally {
|
|
282
|
+
const sock = st.sock;
|
|
283
|
+
st.sock = undefined;
|
|
284
|
+
await sock?.end(undefined);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function initialState(account: WhatsAppAccount, runtime: ClientRuntime): State {
|
|
304
289
|
const st: State = {
|
|
305
290
|
account,
|
|
291
|
+
runtime,
|
|
292
|
+
history: createClientHistory(account.id),
|
|
293
|
+
expiry: new SendExpiry(account.id),
|
|
306
294
|
closed: false,
|
|
307
295
|
openPromise: Promise.resolve(),
|
|
308
296
|
keys: makeKeyCache(),
|
|
@@ -311,6 +299,14 @@ export function createClient(account: WhatsAppAccount): WAClient {
|
|
|
311
299
|
names: makeNameBook(nameFiles.path(account.id)),
|
|
312
300
|
};
|
|
313
301
|
resetGate(st);
|
|
302
|
+
return st;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function createClient(
|
|
306
|
+
account: WhatsAppAccount,
|
|
307
|
+
runtime: ClientRuntime = { makeSocket: makeWASocket, fetchVersion: fetchLatestWaWebVersion },
|
|
308
|
+
): WAClient {
|
|
309
|
+
const st = initialState(account, runtime);
|
|
314
310
|
const senders = makeProfileCache<SenderProfile>((jid) => readProfile(st, jid), {
|
|
315
311
|
onError: (jid, err) => process.stderr.write(`whatsapp[${st.account.id}] could not read the profile of ${jid}: ${errMsg(err)}\n`),
|
|
316
312
|
});
|
|
@@ -331,6 +327,14 @@ export function createClient(account: WhatsAppAccount): WAClient {
|
|
|
331
327
|
);
|
|
332
328
|
}
|
|
333
329
|
},
|
|
330
|
+
read(jid, options) {
|
|
331
|
+
const page = st.history.read(jid, options);
|
|
332
|
+
for (const message of page.messages) st.keys.remember(message.key);
|
|
333
|
+
return Promise.resolve(page);
|
|
334
|
+
},
|
|
335
|
+
listMembers(jid, limit) {
|
|
336
|
+
return listMembers(jid, async (node): Promise<unknown> => await (await ready(st)).query(node), st.names, limit);
|
|
337
|
+
},
|
|
334
338
|
sendText(jid, text, quotedId) {
|
|
335
339
|
return send(
|
|
336
340
|
st,
|
|
@@ -365,6 +369,7 @@ export function createClient(account: WhatsAppAccount): WAClient {
|
|
|
365
369
|
await send(st, jid, {
|
|
366
370
|
delete: knownKey(st.keys, jid, messageId, true),
|
|
367
371
|
});
|
|
372
|
+
if (!st.closed) st.history.deleteMessages({ keys: [{ remoteJid: jid, id: messageId }] });
|
|
368
373
|
},
|
|
369
374
|
async reuploadMedia(m) {
|
|
370
375
|
const sock = await ready(st);
|
|
@@ -388,13 +393,6 @@ export function createClient(account: WhatsAppAccount): WAClient {
|
|
|
388
393
|
}
|
|
389
394
|
},
|
|
390
395
|
senderProfile: (jid) => senders.get(jid),
|
|
391
|
-
|
|
392
|
-
st.closed = true;
|
|
393
|
-
try {
|
|
394
|
-
await st.sock?.end(undefined);
|
|
395
|
-
} catch {
|
|
396
|
-
st.sock = undefined;
|
|
397
|
-
}
|
|
398
|
-
},
|
|
396
|
+
disconnect: () => st.closing ??= disconnect(st),
|
|
399
397
|
};
|
|
400
398
|
}
|