@manybot/manybot 5.9.1 → 5.10.0
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.
|
@@ -178,6 +178,37 @@ function getMsgSenderPn(msg) {
|
|
|
178
178
|
return normalizeJid(msg.chatId);
|
|
179
179
|
return null;
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Split a quoted message's `contextInfo.participant` into `fromLid`/`fromPn`
|
|
183
|
+
* the same way the adapter's `toBotMessage()` splits `key.participant` /
|
|
184
|
+
* `key.participantAlt` via `splitLidPn()` — see the comment there. Unlike
|
|
185
|
+
* the key, `contextInfo` carries only a single `participant` field (no
|
|
186
|
+
* companion "Alt"), and it holds whichever form matches the chat's current
|
|
187
|
+
* addressing mode: under the modern default "lid" mode that's the @lid
|
|
188
|
+
* form, under legacy "pn" mode it's the phone-number form. Blindly
|
|
189
|
+
* assigning it to `fromPn` (as the quoted-message synthetic used to)
|
|
190
|
+
* leaves `fromLid` unset — so `getMsgSender()` (which only reads
|
|
191
|
+
* `participantAlt`/`fromLid`) falls through to `null` — while
|
|
192
|
+
* `getMsgSenderPn()` happily normalizes and returns the @lid value
|
|
193
|
+
* verbatim, since it never checks the suffix. Route by suffix here, and
|
|
194
|
+
* fill in the other side from the store's learned LID<->PN mapping when
|
|
195
|
+
* one is known.
|
|
196
|
+
*/
|
|
197
|
+
function splitQuotedParticipant(store, participant) {
|
|
198
|
+
if (!participant)
|
|
199
|
+
return {};
|
|
200
|
+
if (participant.endsWith("@lid")) {
|
|
201
|
+
const resolved = store.resolveJid(participant);
|
|
202
|
+
return {
|
|
203
|
+
fromLid: participant,
|
|
204
|
+
fromPn: resolved !== participant ? resolved : undefined,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
fromLid: store.resolvePn(participant) ?? undefined,
|
|
209
|
+
fromPn: participant,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
181
212
|
/** Quoted-message metadata as the rest of the api uses it. */
|
|
182
213
|
function getQuotedContext(msg) {
|
|
183
214
|
if (!msg.quotedKey)
|
|
@@ -935,16 +966,33 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
935
966
|
const quotedRaw = contextInfo?.quotedMessage
|
|
936
967
|
? (() => {
|
|
937
968
|
const decoded = decodeContent(contextInfo.quotedMessage);
|
|
969
|
+
const { fromLid: quotedFromLid, fromPn: quotedFromPn } = splitQuotedParticipant(store, contextInfo.participant);
|
|
970
|
+
// contextInfo itself never carries the quoted message's original
|
|
971
|
+
// send time (WhatsApp's ContextInfo proto has no timestamp field —
|
|
972
|
+
// only stanzaId/participant/quotedMessage). Best-effort recover it
|
|
973
|
+
// from the message store, keyed by the same chatId+stanzaId, when
|
|
974
|
+
// the original is still cached (it usually is, since a reply
|
|
975
|
+
// almost always quotes a recent message in the same chat). Stays
|
|
976
|
+
// 0 — same as before — only when the original has aged out of the
|
|
977
|
+
// store's per-chat cap or the chat history hasn't been seen yet.
|
|
978
|
+
const quotedOriginal = contextInfo.stanzaId
|
|
979
|
+
? store.messages.get(msg.chatId)?.get(contextInfo.stanzaId)
|
|
980
|
+
: undefined;
|
|
981
|
+
const quotedTimestamp = quotedOriginal
|
|
982
|
+
? Number(quotedOriginal.messageTimestamp ?? 0) * 1000
|
|
983
|
+
: 0;
|
|
938
984
|
return {
|
|
939
985
|
id: contextInfo.stanzaId ?? "",
|
|
940
986
|
chatId: msg.chatId,
|
|
941
987
|
fromMe: false,
|
|
942
988
|
type: decoded.type,
|
|
943
989
|
contentHash: "",
|
|
944
|
-
timestamp:
|
|
990
|
+
timestamp: quotedTimestamp,
|
|
945
991
|
body: decoded.body,
|
|
946
992
|
mimetype: decoded.mimetype,
|
|
947
|
-
|
|
993
|
+
fromLid: quotedFromLid,
|
|
994
|
+
fromPn: quotedFromPn,
|
|
995
|
+
participantAlt: quotedFromLid,
|
|
948
996
|
pushName: lookupPushName(contextInfo.participant),
|
|
949
997
|
_raw: {
|
|
950
998
|
contextInfo: {
|
|
@@ -962,16 +1010,27 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
962
1010
|
// key-only synthetic so hasReply()/getReply() still work, but
|
|
963
1011
|
// hasMedia/downloadMedia on the result will degrade gracefully
|
|
964
1012
|
// (type=other, mimetype=undefined).
|
|
965
|
-
? {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1013
|
+
? (() => {
|
|
1014
|
+
const { fromLid: quotedFromLid, fromPn: quotedFromPn } = splitQuotedParticipant(store, msg.quotedKey.participant);
|
|
1015
|
+
const quotedOriginal = msg.quotedKey.id
|
|
1016
|
+
? store.messages.get(msg.chatId)?.get(msg.quotedKey.id)
|
|
1017
|
+
: undefined;
|
|
1018
|
+
const quotedTimestamp = quotedOriginal
|
|
1019
|
+
? Number(quotedOriginal.messageTimestamp ?? 0) * 1000
|
|
1020
|
+
: 0;
|
|
1021
|
+
return {
|
|
1022
|
+
id: msg.quotedKey.id ?? "",
|
|
1023
|
+
chatId: msg.chatId,
|
|
1024
|
+
fromMe: false,
|
|
1025
|
+
type: "other",
|
|
1026
|
+
contentHash: "",
|
|
1027
|
+
timestamp: quotedTimestamp,
|
|
1028
|
+
fromLid: quotedFromLid,
|
|
1029
|
+
fromPn: quotedFromPn,
|
|
1030
|
+
participantAlt: quotedFromLid,
|
|
1031
|
+
pushName: lookupPushName(msg.quotedKey.participant),
|
|
1032
|
+
};
|
|
1033
|
+
})()
|
|
975
1034
|
: null;
|
|
976
1035
|
return {
|
|
977
1036
|
id: msg.id,
|
|
@@ -1818,50 +1877,69 @@ function buildAdminApi(contract, store, chatJid) {
|
|
|
1818
1877
|
},
|
|
1819
1878
|
};
|
|
1820
1879
|
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Same `.to(target)` shape as `createTargetableAction`, for group-level
|
|
1882
|
+
* ops that take no `memberIds` (setSubject/setDescription/setProfilePic/
|
|
1883
|
+
* revokeInvite) — no participant resolution needed, just a group jid.
|
|
1884
|
+
*/
|
|
1885
|
+
function createTargetableGroupAction(action) {
|
|
1886
|
+
const executeCurrent = async () => {
|
|
1887
|
+
requireChat();
|
|
1888
|
+
return action(chatJid);
|
|
1889
|
+
};
|
|
1890
|
+
return {
|
|
1891
|
+
async to(targetJid) {
|
|
1892
|
+
await getGroup(targetJid);
|
|
1893
|
+
return action(targetJid);
|
|
1894
|
+
},
|
|
1895
|
+
then(onfulfilled, onrejected) {
|
|
1896
|
+
return executeCurrent().then(onfulfilled, onrejected);
|
|
1897
|
+
},
|
|
1898
|
+
catch(onrejected) {
|
|
1899
|
+
return executeCurrent().catch(onrejected);
|
|
1900
|
+
},
|
|
1901
|
+
finally(onfinally) {
|
|
1902
|
+
return executeCurrent().finally(onfinally);
|
|
1903
|
+
},
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
function isSelf(u) {
|
|
1907
|
+
const n = normalizeJid(u);
|
|
1908
|
+
return (botJid && n === botJid) || (botLid && n === botLid) || false;
|
|
1909
|
+
}
|
|
1821
1910
|
return {
|
|
1822
1911
|
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
1823
1912
|
add(memberIds) {
|
|
1824
1913
|
return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "add"), memberIds, "newMember");
|
|
1825
1914
|
},
|
|
1826
1915
|
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
})) {
|
|
1834
|
-
throw new Error(t("driver.cannotKickSelf"));
|
|
1835
|
-
}
|
|
1836
|
-
return runParticipantsUpdate(chatJid, users, "remove");
|
|
1916
|
+
kick(memberIds) {
|
|
1917
|
+
return createTargetableAction(async (jid, users) => {
|
|
1918
|
+
if (users.some(isSelf))
|
|
1919
|
+
throw new Error(t("driver.cannotKickSelf"));
|
|
1920
|
+
return runParticipantsUpdate(jid, users, "remove");
|
|
1921
|
+
}, memberIds);
|
|
1837
1922
|
},
|
|
1838
1923
|
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
|
|
1842
|
-
return runParticipantsUpdate(chatJid, users, "promote");
|
|
1924
|
+
promote(memberIds) {
|
|
1925
|
+
return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "promote"), memberIds);
|
|
1843
1926
|
},
|
|
1844
1927
|
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
|
|
1848
|
-
return runParticipantsUpdate(chatJid, users, "demote");
|
|
1928
|
+
demote(memberIds) {
|
|
1929
|
+
return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "demote"), memberIds);
|
|
1849
1930
|
},
|
|
1850
1931
|
/** @param {string} name */
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
return contract.groupUpdateSubject(chatJid, name);
|
|
1932
|
+
setSubject(name) {
|
|
1933
|
+
return createTargetableGroupAction((jid) => contract.groupUpdateSubject(jid, name));
|
|
1854
1934
|
},
|
|
1855
1935
|
/** @param {string} text */
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
return contract.groupUpdateDescription(chatJid, text);
|
|
1936
|
+
setDescription(text) {
|
|
1937
|
+
return createTargetableGroupAction((jid) => contract.groupUpdateDescription(jid, text));
|
|
1859
1938
|
},
|
|
1860
1939
|
/** @param {string|Buffer} source */
|
|
1861
|
-
|
|
1862
|
-
requireChat();
|
|
1940
|
+
setProfilePic(source) {
|
|
1863
1941
|
const buffer = Buffer.isBuffer(source) ? source : readFileSync(source);
|
|
1864
|
-
return contract.updateProfilePicture(
|
|
1942
|
+
return createTargetableGroupAction((jid) => contract.updateProfilePicture(jid, buffer));
|
|
1865
1943
|
},
|
|
1866
1944
|
async getInviteLink(groupId) {
|
|
1867
1945
|
const jid = groupId ?? chatJid;
|
|
@@ -1870,9 +1948,8 @@ function buildAdminApi(contract, store, chatJid) {
|
|
|
1870
1948
|
const code = await contract.groupInviteCode(jid);
|
|
1871
1949
|
return `https://chat.whatsapp.com/${code}`;
|
|
1872
1950
|
},
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
return contract.groupRevokeInvite(chatJid);
|
|
1951
|
+
revokeInvite() {
|
|
1952
|
+
return createTargetableGroupAction((jid) => contract.groupRevokeInvite(jid));
|
|
1876
1953
|
},
|
|
1877
1954
|
};
|
|
1878
1955
|
}
|
|
@@ -25,6 +25,10 @@ function createMockContract() {
|
|
|
25
25
|
deletes: [],
|
|
26
26
|
blockUpdates: [],
|
|
27
27
|
groupParticipantUpdates: [],
|
|
28
|
+
subjectUpdates: [],
|
|
29
|
+
descriptionUpdates: [],
|
|
30
|
+
profilePicUpdates: [],
|
|
31
|
+
revokeInvites: [],
|
|
28
32
|
nameUpdates: [],
|
|
29
33
|
statusUpdates: [],
|
|
30
34
|
};
|
|
@@ -189,11 +193,20 @@ function createMockContract() {
|
|
|
189
193
|
calls.groupParticipantUpdates.push({ jid, users, action });
|
|
190
194
|
return users.map((u) => ({ status: "200", jid: u }));
|
|
191
195
|
},
|
|
192
|
-
groupUpdateSubject: async () => {
|
|
193
|
-
|
|
196
|
+
groupUpdateSubject: async (jid, subject) => {
|
|
197
|
+
calls.subjectUpdates.push({ jid, subject });
|
|
198
|
+
},
|
|
199
|
+
groupUpdateDescription: async (jid, description) => {
|
|
200
|
+
calls.descriptionUpdates.push({ jid, description });
|
|
201
|
+
},
|
|
194
202
|
groupInviteCode: async () => "mock-invite-code-123",
|
|
195
|
-
groupRevokeInvite: async () =>
|
|
196
|
-
|
|
203
|
+
groupRevokeInvite: async (jid) => {
|
|
204
|
+
calls.revokeInvites.push(jid);
|
|
205
|
+
return "mock-new-invite-code-456";
|
|
206
|
+
},
|
|
207
|
+
updateProfilePicture: async (jid) => {
|
|
208
|
+
calls.profilePicUpdates.push({ jid });
|
|
209
|
+
},
|
|
197
210
|
updateProfileName: async (name) => {
|
|
198
211
|
calls.nameUpdates.push(name);
|
|
199
212
|
},
|
|
@@ -301,6 +314,44 @@ describe("kernel/pluginApi — buildSetupApi with Mock WaContract", () => {
|
|
|
301
314
|
assert.equal(calls.groupParticipantUpdates[0].action, "add");
|
|
302
315
|
assert.equal(calls.groupParticipantUpdates[0].jid, "120363000000000@g.us");
|
|
303
316
|
});
|
|
317
|
+
test("setup admin.kick/promote/demote().to() target the explicit group", async () => {
|
|
318
|
+
const ctx = buildSetupApi(mockContract, store, pluginRegistry, "test_plugin");
|
|
319
|
+
const groupJid = "120363000000000@g.us";
|
|
320
|
+
await ctx.admin.promote("5516777777777@s.whatsapp.net").to(groupJid);
|
|
321
|
+
assert.equal(calls.groupParticipantUpdates.at(-1)?.action, "promote");
|
|
322
|
+
assert.equal(calls.groupParticipantUpdates.at(-1)?.jid, groupJid);
|
|
323
|
+
await ctx.admin.demote("5516777777777@s.whatsapp.net").to(groupJid);
|
|
324
|
+
assert.equal(calls.groupParticipantUpdates.at(-1)?.action, "demote");
|
|
325
|
+
assert.equal(calls.groupParticipantUpdates.at(-1)?.jid, groupJid);
|
|
326
|
+
await ctx.admin.kick("5516777777777@s.whatsapp.net").to(groupJid);
|
|
327
|
+
assert.equal(calls.groupParticipantUpdates.at(-1)?.action, "remove");
|
|
328
|
+
assert.equal(calls.groupParticipantUpdates.at(-1)?.jid, groupJid);
|
|
329
|
+
// self-kick guard still applies when targeting an explicit group
|
|
330
|
+
await assert.rejects(async () => { await ctx.admin.kick("5516999999999@s.whatsapp.net").to(groupJid); });
|
|
331
|
+
});
|
|
332
|
+
test("setup admin.setSubject/setDescription/setProfilePic/revokeInvite().to() target the explicit group", async () => {
|
|
333
|
+
const ctx = buildSetupApi(mockContract, store, pluginRegistry, "test_plugin");
|
|
334
|
+
const groupJid = "120363000000000@g.us";
|
|
335
|
+
await ctx.admin.setSubject("New Subject").to(groupJid);
|
|
336
|
+
assert.deepEqual(calls.subjectUpdates, [{ jid: groupJid, subject: "New Subject" }]);
|
|
337
|
+
await ctx.admin.setDescription("New Description").to(groupJid);
|
|
338
|
+
assert.deepEqual(calls.descriptionUpdates, [{ jid: groupJid, description: "New Description" }]);
|
|
339
|
+
await ctx.admin.setProfilePic(Buffer.from("pic-data")).to(groupJid);
|
|
340
|
+
assert.deepEqual(calls.profilePicUpdates, [{ jid: groupJid }]);
|
|
341
|
+
const invite = await ctx.admin.revokeInvite().to(groupJid);
|
|
342
|
+
assert.match(String(invite), /mock-new-invite-code-456/);
|
|
343
|
+
assert.deepEqual(calls.revokeInvites, [groupJid]);
|
|
344
|
+
});
|
|
345
|
+
test("setup admin.* without .to() throws (no current chat bound)", async () => {
|
|
346
|
+
const ctx = buildSetupApi(mockContract, store, pluginRegistry, "test_plugin");
|
|
347
|
+
await assert.rejects(async () => { await ctx.admin.kick("5516777777777@s.whatsapp.net"); }, /runtime group context/);
|
|
348
|
+
await assert.rejects(async () => { await ctx.admin.promote("5516777777777@s.whatsapp.net"); }, /runtime group context/);
|
|
349
|
+
await assert.rejects(async () => { await ctx.admin.demote("5516777777777@s.whatsapp.net"); }, /runtime group context/);
|
|
350
|
+
await assert.rejects(async () => { await ctx.admin.setSubject("X"); }, /runtime group context/);
|
|
351
|
+
await assert.rejects(async () => { await ctx.admin.setDescription("X"); }, /runtime group context/);
|
|
352
|
+
await assert.rejects(async () => { await ctx.admin.setProfilePic(Buffer.from("x")); }, /runtime group context/);
|
|
353
|
+
await assert.rejects(async () => { await ctx.admin.revokeInvite(); }, /runtime group context/);
|
|
354
|
+
});
|
|
304
355
|
});
|
|
305
356
|
describe("kernel/pluginApi — buildApi (Runtime) with Mock WaContract", () => {
|
|
306
357
|
let store;
|
|
@@ -371,7 +422,7 @@ describe("kernel/pluginApi — buildApi (Runtime) with Mock WaContract", () => {
|
|
|
371
422
|
await ctx.admin.kick("5516777777777@s.whatsapp.net");
|
|
372
423
|
assert.equal(calls.groupParticipantUpdates.length, 2);
|
|
373
424
|
assert.equal(calls.groupParticipantUpdates[1].action, "remove");
|
|
374
|
-
await assert.rejects(() => ctx.admin.kick("5516999999999@s.whatsapp.net"));
|
|
425
|
+
await assert.rejects(async () => { await ctx.admin.kick("5516999999999@s.whatsapp.net"); });
|
|
375
426
|
assert.equal(calls.groupParticipantUpdates.length, 2);
|
|
376
427
|
const inviteLink = await ctx.admin.getInviteLink();
|
|
377
428
|
assert.match(inviteLink, /chat\.whatsapp\.com\/mock-invite-code-123/);
|