@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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { proto, WAMessageStubType, type WAMessage, type WAMessageKey, type WAMessageUpdate } from 'baileys';
|
|
2
|
+
import { clipped, historyContent, historyKey, numberOf, projectMessage } from './history-message.js';
|
|
3
|
+
import { isGroupJid } from './parse.js';
|
|
4
|
+
import { HISTORY_LIMITS, type StoredRow } from './history-types.js';
|
|
5
|
+
import type { HistoryState } from './history-state.js';
|
|
6
|
+
|
|
7
|
+
const earlierExpiry = (a?: number, b?: number): number | undefined => a === undefined ? b : b === undefined ? a : Math.min(a, b);
|
|
8
|
+
const timestamp = (message: WAMessage): number | undefined => {
|
|
9
|
+
const seconds = numberOf(message.messageTimestamp);
|
|
10
|
+
return seconds === undefined ? undefined : seconds * 1000;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function removed(event: WAMessageUpdate): boolean {
|
|
14
|
+
return event.key.isViewOnce === true || event.update.message === null || event.update.messageStubType === WAMessageStubType.REVOKE;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function acceptedSent(row: StoredRow, sent?: { expiration?: number }): boolean {
|
|
18
|
+
return !sent || (row.key.fromMe === true && (row.expiresAt !== undefined || sent.expiration === 0));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function editedRow(old: StoredRow, row: StoredRow, revision: number): StoredRow {
|
|
22
|
+
const expiresAt = earlierExpiry(old.expiresAt, row.expiresAt);
|
|
23
|
+
return { ...old, ...row, truncated: row.truncated ?? false, editedAt: revision, ...(expiresAt === undefined ? {} : { expiresAt }) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class HistoryEvents {
|
|
27
|
+
constructor(private readonly state: HistoryState) {}
|
|
28
|
+
|
|
29
|
+
private author(old: WAMessageKey, editor: WAMessageKey): boolean {
|
|
30
|
+
if ((old.fromMe === true) !== (editor.fromMe === true)) return false;
|
|
31
|
+
if (!isGroupJid(old.remoteJid ?? '')) return true;
|
|
32
|
+
if (old.participant && editor.participant) return this.state.same(old.participant, editor.participant);
|
|
33
|
+
return old.fromMe === true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private edited(key: WAMessageKey, message: proto.IMessage, revision: number, now: number, selfJid?: string): void {
|
|
37
|
+
const old = this.state.get(key);
|
|
38
|
+
if (!old) {
|
|
39
|
+
this.state.remove(key, now);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!this.author(old.key, key)) return;
|
|
43
|
+
const row = projectMessage({ key: old.key, message, messageTimestamp: old.at / 1000 }, now, selfJid);
|
|
44
|
+
if (row === 'omit') this.state.remove(old.key, now, old.at);
|
|
45
|
+
if (!row || row === 'omit' || revision > now || revision <= (old.editedAt ?? old.at)) return;
|
|
46
|
+
this.state.put(editedRow(old, row, revision));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private protocol(message: WAMessage, now: number, selfJid?: string): boolean {
|
|
50
|
+
const protocol = historyContent(message.message).message?.protocolMessage;
|
|
51
|
+
if (!protocol) return false;
|
|
52
|
+
if (!protocol.key?.id) return true;
|
|
53
|
+
if (protocol.key.remoteJid && !this.state.same(protocol.key.remoteJid, message.key.remoteJid ?? '')) return true;
|
|
54
|
+
const key = historyKey({ ...message.key, id: protocol.key.id });
|
|
55
|
+
if (key) this.applyProtocol(key, protocol, message, now, selfJid);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private applyProtocol(key: WAMessageKey, protocol: proto.Message.IProtocolMessage, message: WAMessage, now: number, selfJid?: string): void {
|
|
60
|
+
if (protocol.type === proto.Message.ProtocolMessage.Type.REVOKE) this.state.remove(key, now);
|
|
61
|
+
if (protocol.type === proto.Message.ProtocolMessage.Type.MESSAGE_EDIT && protocol.editedMessage) {
|
|
62
|
+
const revision = numberOf(protocol.timestampMs) ?? timestamp(message) ?? 0;
|
|
63
|
+
this.edited(key, protocol.editedMessage, revision, now, selfJid);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ingest(message: WAMessage, now: number, selfJid?: string, sent?: { expiration?: number }): void {
|
|
68
|
+
if (!historyKey(message.key)) return;
|
|
69
|
+
const at = timestamp(message);
|
|
70
|
+
if (at !== undefined && this.state.outside(message.key, at, now)) return;
|
|
71
|
+
this.state.observe(message.key, now);
|
|
72
|
+
if (message.messageStubType === WAMessageStubType.REVOKE) {
|
|
73
|
+
this.state.remove(message.key, now, at);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!this.protocol(message, now, selfJid)) this.content(message, now, selfJid, sent);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private content(message: WAMessage, now: number, selfJid?: string, sent?: { expiration?: number }): void {
|
|
80
|
+
const row = projectMessage(message, now, selfJid, sent?.expiration);
|
|
81
|
+
if (row === 'omit') this.state.remove(message.key, now, timestamp(message));
|
|
82
|
+
if (!row || row === 'omit' || this.state.blocked(row.key, row.at) || !acceptedSent(row, sent)) return;
|
|
83
|
+
const old = this.state.get(row.key);
|
|
84
|
+
if (old) this.duplicate(old, row);
|
|
85
|
+
else this.state.put(row);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private duplicate(old: StoredRow, row: StoredRow): void {
|
|
89
|
+
const expiresAt = earlierExpiry(old.expiresAt, row.expiresAt);
|
|
90
|
+
if (expiresAt !== undefined) old.expiresAt = expiresAt;
|
|
91
|
+
if (old.key.fromMe === true && old.senderJid === undefined && row.key.fromMe === true && row.senderJid) old.senderJid = row.senderJid;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
update(event: WAMessageUpdate, now: number, selfJid?: string): void {
|
|
95
|
+
const { key, update } = event;
|
|
96
|
+
if (!historyKey(key)) return;
|
|
97
|
+
const originalAt = this.state.get(key)?.at ?? timestamp({ ...update, key });
|
|
98
|
+
if (removed(event) || update.ephemeralDuration || update.ephemeralStartTimestamp) {
|
|
99
|
+
this.state.remove(key, now, originalAt);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.state.observe(key, now);
|
|
103
|
+
if (!update.message) return;
|
|
104
|
+
const full = { ...update, key };
|
|
105
|
+
if (this.protocol(full, now, selfJid)) return;
|
|
106
|
+
const revision = timestamp(full);
|
|
107
|
+
if (revision) this.edited(key, update.message, revision, now, selfJid);
|
|
108
|
+
else this.refresh(full, now, selfJid);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private refresh(message: WAMessage, now: number, selfJid?: string): void {
|
|
112
|
+
const old = this.state.get(message.key);
|
|
113
|
+
const row = projectMessage({ ...message, messageTimestamp: old ? old.at / 1000 : undefined }, now, selfJid);
|
|
114
|
+
if (row === 'omit') this.state.remove(message.key, now, old?.at);
|
|
115
|
+
else if (row && old) this.duplicate(old, row);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
edit(jid: string, messageId: string, source: string, now: number): void {
|
|
119
|
+
const old = this.state.get({ remoteJid: jid, id: messageId });
|
|
120
|
+
if (!old) {
|
|
121
|
+
this.state.remove({ remoteJid: jid, id: messageId }, now);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const text = clipped(source, HISTORY_LIMITS.textBytes);
|
|
125
|
+
this.state.put({ ...old, text, truncated: text !== source, editedAt: now });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
import { accountFiles, type AccountFiles } from '@metro-labs/core/stations/account-files';
|
|
4
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
5
|
+
|
|
6
|
+
const files = accountFiles('WHATSAPP_TOKEN_DIR', 'whatsapp-history-');
|
|
7
|
+
const TEMP_NAME = /^(whatsapp-history-[A-Za-z0-9_-]*\.json)\.tmp-[1-9]\d*$/;
|
|
8
|
+
|
|
9
|
+
function cleanTemps(dir: string, remove: (name: string) => boolean): void {
|
|
10
|
+
let names: string[];
|
|
11
|
+
try {
|
|
12
|
+
names = readdirSync(dir);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (isRecord(err) && err.code === 'ENOENT') return;
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
const original = TEMP_NAME.exec(name)?.[1];
|
|
19
|
+
if (original && remove(original)) rmSync(join(dir, name), { force: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function clearHistoryTemps(path: string): void {
|
|
24
|
+
const name = basename(path);
|
|
25
|
+
cleanTemps(dirname(path), (original) => original === name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const historyFiles: AccountFiles = {
|
|
29
|
+
path: files.path,
|
|
30
|
+
forget(accountId) {
|
|
31
|
+
files.forget(accountId);
|
|
32
|
+
clearHistoryTemps(files.path(accountId));
|
|
33
|
+
},
|
|
34
|
+
forgetExcept(keptAccountIds) {
|
|
35
|
+
files.forgetExcept(keptAccountIds);
|
|
36
|
+
const kept = new Set(keptAccountIds.map((id) => basename(files.path(id))));
|
|
37
|
+
cleanTemps(dirname(files.path('')), (original) => !kept.has(original));
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jidNormalizedUser, type WAMessageKey } from 'baileys';
|
|
2
|
+
import { TrainError } from '@metro-labs/core/train-error';
|
|
3
|
+
import { HISTORY_LIMITS } from './history-types.js';
|
|
4
|
+
|
|
5
|
+
export const normalizedJid = (jid: string): string => jidNormalizedUser(jid);
|
|
6
|
+
const phone = (jid: string): boolean => jid.endsWith('@s.whatsapp.net');
|
|
7
|
+
const lid = (jid: string): boolean => jid.endsWith('@lid');
|
|
8
|
+
const identity = (v: unknown): v is string => typeof v === 'string' && v.length <= 256 && (phone(v) || lid(v));
|
|
9
|
+
const invalid = (): never => { throw new TrainError('whatsapp_history_invalid', 'WhatsApp history aliases are invalid; refusing to discard replay protection'); };
|
|
10
|
+
|
|
11
|
+
export class HistoryIdentity {
|
|
12
|
+
private readonly groups = new Map<string, readonly string[]>();
|
|
13
|
+
|
|
14
|
+
constructor(value: unknown = []) {
|
|
15
|
+
if (!Array.isArray(value)) return invalid();
|
|
16
|
+
for (const group of value) this.restore(group);
|
|
17
|
+
if (this.groups.size > HISTORY_LIMITS.aliases) return invalid();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private restore(group: unknown): void {
|
|
21
|
+
if (!Array.isArray(group) || !group.every(identity)) return invalid();
|
|
22
|
+
const pn = group.find(phone);
|
|
23
|
+
const alternate = group.find(lid);
|
|
24
|
+
if (!pn || !alternate) return invalid();
|
|
25
|
+
for (const jid of group) this.alias(jid, phone(jid) ? alternate : pn);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get size(): number { return this.groups.size; }
|
|
29
|
+
|
|
30
|
+
known(jid: string): readonly string[] {
|
|
31
|
+
const normalized = normalizedJid(jid);
|
|
32
|
+
return this.groups.get(normalized) ?? [normalized];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
canonical(jid: string): string { return this.known(jid)[0] ?? normalizedJid(jid); }
|
|
36
|
+
|
|
37
|
+
same(left: string, right: string): boolean { return this.canonical(left) === this.canonical(right); }
|
|
38
|
+
|
|
39
|
+
alias(left: string, right: string): boolean {
|
|
40
|
+
if (!identity(left) || !identity(right)) return false;
|
|
41
|
+
const a = normalizedJid(left);
|
|
42
|
+
const b = normalizedJid(right);
|
|
43
|
+
if (phone(a) === phone(b) || this.same(a, b)) return false;
|
|
44
|
+
const group = [...new Set([...this.known(a), ...this.known(b)])].sort();
|
|
45
|
+
for (const jid of group) this.groups.set(jid, group);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
remember(key: WAMessageKey): boolean {
|
|
50
|
+
const chat = this.alias(key.remoteJid ?? '', key.remoteJidAlt ?? '');
|
|
51
|
+
const sender = this.alias(key.participant ?? '', key.participantAlt ?? '');
|
|
52
|
+
return chat || sender;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
clear(): void { this.groups.clear(); }
|
|
56
|
+
|
|
57
|
+
serialize(): readonly (readonly string[])[] { return [...new Set(this.groups.values())]; }
|
|
58
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
2
|
+
import type { WAMessage, WAMessageKey, proto } from 'baileys';
|
|
3
|
+
import { mediaRefIn, type WAMediaRef } from './media.js';
|
|
4
|
+
import { extractText, isGroupJid, isPrivateJid } from './parse.js';
|
|
5
|
+
import { HISTORY_LIMITS, type StoredRow } from './history-types.js';
|
|
6
|
+
|
|
7
|
+
const KEY_STRINGS = [
|
|
8
|
+
'remoteJid', 'id', 'participant', 'remoteJidAlt', 'remoteJidUsername',
|
|
9
|
+
'participantAlt', 'participantUsername', 'server_id', 'addressingMode',
|
|
10
|
+
] as const;
|
|
11
|
+
const CONTEXT_NODES = [
|
|
12
|
+
'extendedTextMessage', 'imageMessage', 'videoMessage', 'audioMessage',
|
|
13
|
+
'documentMessage', 'stickerMessage',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
type Timestamp = number | { toNumber(): number } | null | undefined;
|
|
17
|
+
|
|
18
|
+
export function numberOf(value: Timestamp): number | undefined {
|
|
19
|
+
const n = typeof value === 'number' ? value : typeof value?.toNumber === 'function' ? value.toNumber() : undefined;
|
|
20
|
+
return n !== undefined && Number.isSafeInteger(n) && n > 0 ? n : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function clipped(text: string, bytes: number): string {
|
|
24
|
+
if (Buffer.byteLength(text) <= bytes) return text;
|
|
25
|
+
const buffer = Buffer.from(text);
|
|
26
|
+
let end = bytes;
|
|
27
|
+
while (end > 0 && ((buffer[end] ?? 0) & 0xc0) === 0x80) end--;
|
|
28
|
+
return buffer.subarray(0, end).toString('utf8');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function keyStrings(value: Record<string, unknown>): [string, string | boolean | null][] | undefined {
|
|
32
|
+
const fields: [string, string | boolean | null][] = [];
|
|
33
|
+
for (const field of KEY_STRINGS) {
|
|
34
|
+
const v = value[field];
|
|
35
|
+
if (v === undefined) continue;
|
|
36
|
+
if (v !== null && (typeof v !== 'string' || v.length > 256)) return undefined;
|
|
37
|
+
fields.push([field, v]);
|
|
38
|
+
}
|
|
39
|
+
return fields;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const historyJid = (jid: string): boolean => jid.length <= 256 && (isPrivateJid(jid) || isGroupJid(jid));
|
|
43
|
+
|
|
44
|
+
function identified(key: WAMessageKey): boolean {
|
|
45
|
+
return Boolean(key.id) && historyJid(key.remoteJid ?? '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function historyKey(value: unknown): WAMessageKey | undefined {
|
|
49
|
+
if (!isRecord(value)) return undefined;
|
|
50
|
+
const fields = keyStrings(value);
|
|
51
|
+
if (!fields) return undefined;
|
|
52
|
+
for (const field of ['fromMe', 'isViewOnce']) {
|
|
53
|
+
const v = value[field];
|
|
54
|
+
if (v === undefined) continue;
|
|
55
|
+
if (v !== null && typeof v !== 'boolean') return undefined;
|
|
56
|
+
fields.push([field, v]);
|
|
57
|
+
}
|
|
58
|
+
const key: WAMessageKey = Object.fromEntries(fields);
|
|
59
|
+
return identified(key) ? key : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface Content {
|
|
63
|
+
message?: proto.IMessage;
|
|
64
|
+
ephemeral: boolean;
|
|
65
|
+
forbidden: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function privateContent(message: proto.IMessage): boolean {
|
|
69
|
+
return [message.viewOnceMessage, message.viewOnceMessageV2, message.viewOnceMessageV2Extension,
|
|
70
|
+
message.imageMessage?.viewOnce, message.videoMessage?.viewOnce, message.audioMessage?.viewOnce,
|
|
71
|
+
message.messageContextInfo?.messageAddOnExpiryType].some(Boolean);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function innerOf(message: proto.IMessage): proto.IMessage | undefined {
|
|
75
|
+
const wrappers = [message.ephemeralMessage, message.deviceSentMessage, message.documentWithCaptionMessage, message.editedMessage];
|
|
76
|
+
return wrappers.find((wrapper) => wrapper?.message)?.message ?? undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function historyContent(message: proto.IMessage | null | undefined): Content {
|
|
80
|
+
if (!message) return { ephemeral: false, forbidden: false };
|
|
81
|
+
let current = message;
|
|
82
|
+
let ephemeral = false;
|
|
83
|
+
for (let depth = 0; current && depth < 12; depth++) {
|
|
84
|
+
if (privateContent(current)) return { ephemeral, forbidden: true };
|
|
85
|
+
ephemeral ||= Boolean(current.ephemeralMessage);
|
|
86
|
+
const inner = innerOf(current);
|
|
87
|
+
if (!inner) return { message: current, ephemeral, forbidden: false };
|
|
88
|
+
current = inner;
|
|
89
|
+
}
|
|
90
|
+
return { ephemeral, forbidden: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function contextOf(message: proto.IMessage): proto.IContextInfo | undefined {
|
|
94
|
+
for (const field of CONTEXT_NODES) {
|
|
95
|
+
const context = message[field]?.contextInfo;
|
|
96
|
+
if (context) return context;
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const durationGiven = (n: number | null | undefined): n is number => n !== undefined && n !== null && n !== 0;
|
|
102
|
+
|
|
103
|
+
function expiry(message: WAMessage, content: Content, at: number, expiration?: number): number | false | undefined {
|
|
104
|
+
const ctx = content.message ? contextOf(content.message) : undefined;
|
|
105
|
+
const duration = [message.ephemeralDuration, ctx?.expiration, expiration].filter(durationGiven);
|
|
106
|
+
if (duration.some((n) => !Number.isFinite(n) || n < 0)) return false;
|
|
107
|
+
const ephemeral = [content.ephemeral, message.ephemeralStartTimestamp, ctx?.disappearingMode, ctx?.ephemeralSettingTimestamp].some(Boolean);
|
|
108
|
+
if (!duration.length) return ephemeral ? false : undefined;
|
|
109
|
+
const start = numberOf(message.ephemeralStartTimestamp);
|
|
110
|
+
const base = start === undefined ? at : Math.min(at, start * 1000);
|
|
111
|
+
const end = base + Math.min(...duration) * 1000;
|
|
112
|
+
return Number.isSafeInteger(end) ? end : false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function attachment(message: proto.IMessage): WAMediaRef | undefined {
|
|
116
|
+
const media = mediaRefIn(message);
|
|
117
|
+
if (!media) return undefined;
|
|
118
|
+
return {
|
|
119
|
+
kind: media.kind,
|
|
120
|
+
...(media.mime ? { mime: clipped(media.mime, 128) } : {}),
|
|
121
|
+
...(media.name ? { name: clipped(media.name, 256) } : {}),
|
|
122
|
+
...(Number.isSafeInteger(media.bytes) && (media.bytes ?? -1) >= 0 ? { bytes: media.bytes } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function senderOf(key: WAMessageKey, selfJid?: string): string | undefined {
|
|
127
|
+
const sender = key.fromMe ? selfJid : isGroupJid(key.remoteJid ?? '') ? key.participant : key.remoteJid;
|
|
128
|
+
return typeof sender === 'string' && sender.length <= 256 ? sender : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function retainedExpiry(value: number | false | undefined, now: number): value is number | undefined {
|
|
132
|
+
return value !== false && (value === undefined || value > now);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function projectMessage(message: WAMessage, now: number, selfJid?: string, expiration?: number): StoredRow | 'omit' | undefined {
|
|
136
|
+
const key = historyKey(message.key);
|
|
137
|
+
if (!key) return undefined;
|
|
138
|
+
const content = historyContent(message.message);
|
|
139
|
+
if (key.isViewOnce || content.forbidden) return 'omit';
|
|
140
|
+
const at = (numberOf(message.messageTimestamp) ?? 0) * 1000;
|
|
141
|
+
if (!at || at > now) return undefined;
|
|
142
|
+
const expiresAt = expiry(message, content, at, expiration);
|
|
143
|
+
if (!retainedExpiry(expiresAt, now)) return 'omit';
|
|
144
|
+
if (!content.message) return undefined;
|
|
145
|
+
return projectBody(key, content.message, at, selfJid, expiresAt);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function projectBody(key: WAMessageKey, message: proto.IMessage, at: number, selfJid?: string, expiresAt?: number): StoredRow | undefined {
|
|
149
|
+
const source = extractText(message);
|
|
150
|
+
const media = attachment(message);
|
|
151
|
+
if (!source && !media) return undefined;
|
|
152
|
+
const text = clipped(source, HISTORY_LIMITS.textBytes);
|
|
153
|
+
const senderJid = senderOf(key, selfJid);
|
|
154
|
+
return {
|
|
155
|
+
key, at, text,
|
|
156
|
+
...(senderJid ? { senderJid } : {}),
|
|
157
|
+
...(text !== source ? { truncated: true } : {}),
|
|
158
|
+
...(media ? { attachments: [media] } : {}),
|
|
159
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
160
|
+
};
|
|
161
|
+
}
|