@oxidezap/baileyrs 0.1.0 → 0.1.1
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/README.md +70 -0
- package/lib/Compatibility/legacy-store/namespaces.d.ts +20 -0
- package/lib/Compatibility/legacy-store/namespaces.js +27 -0
- package/lib/Compatibility/newsletter-results.d.ts +15 -0
- package/lib/Compatibility/newsletter-results.js +38 -0
- package/lib/Socket/business.d.ts +29 -0
- package/lib/Socket/business.js +104 -0
- package/lib/Socket/chat-actions.d.ts +20 -11
- package/lib/Socket/chat-actions.js +171 -83
- package/lib/Socket/events.js +5 -5
- package/lib/Socket/index.d.ts +85 -14
- package/lib/Socket/index.js +36 -26
- package/lib/Socket/internals.d.ts +88 -0
- package/lib/Socket/internals.js +145 -0
- package/lib/Socket/messages.d.ts +1 -15
- package/lib/Socket/messages.js +3 -22
- package/lib/Socket/newsletter.d.ts +61 -6
- package/lib/Socket/newsletter.js +125 -7
- package/lib/Socket/privacy.d.ts +25 -0
- package/lib/Socket/privacy.js +54 -0
- package/lib/Socket/server-queries.d.ts +38 -0
- package/lib/Socket/server-queries.js +121 -0
- package/lib/Socket/types.d.ts +6 -0
- package/lib/Types/Product.d.ts +9 -0
- package/lib/Utils/index.d.ts +1 -0
- package/lib/Utils/index.js +3 -0
- package/lib/Utils/link-preview.d.ts +60 -0
- package/lib/Utils/link-preview.js +357 -0
- package/lib/Utils/messages.d.ts +20 -7
- package/lib/Utils/messages.js +14 -3
- package/lib/Utils/wrap-legacy-store.d.ts +1 -0
- package/lib/Utils/wrap-legacy-store.js +1 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -220,6 +220,76 @@ A few behaviors that differ from upstream — almost always to your advantage:
|
|
|
220
220
|
`(err as Boom).output.statusCode` pattern works unchanged. If your
|
|
221
221
|
`package.json` was pulling `@hapi/boom` only for baileys, you can drop
|
|
222
222
|
the dependency.
|
|
223
|
+
- **Your key store also holds bridge state, so "empty" is not "unpaired".**
|
|
224
|
+
See [Bridge state in your key store](#bridge-state-in-your-key-store) — this
|
|
225
|
+
one can break a boot path, so it has its own section.
|
|
226
|
+
|
|
227
|
+
### Bridge state in your key store
|
|
228
|
+
|
|
229
|
+
Upstream Baileys keeps engine state in `creds` (persisted by `saveCreds`) and
|
|
230
|
+
puts only Signal key material in `keys`. baileyrs uses that same `keys` store
|
|
231
|
+
as the persistence channel for the Rust core's own state as well: its device
|
|
232
|
+
record, and the byte-level records the Signal namespaces are projected from.
|
|
233
|
+
Those live under namespaces reserved with the **`bridge-` prefix**:
|
|
234
|
+
|
|
235
|
+
| namespace | what it holds |
|
|
236
|
+
| --- | --- |
|
|
237
|
+
| `bridge-native-*` | the core's own encoding of a namespace that also has a Baileys projection (`bridge-native-session`, `bridge-native-prekey`, `bridge-native-device`, …) |
|
|
238
|
+
| `bridge-*` | core records with no Baileys equivalent (`bridge-signed-prekey`, `bridge-sent-message`, `bridge-msg-secret`, `bridge-meta`, …) |
|
|
239
|
+
|
|
240
|
+
No upstream Baileys namespace starts with `bridge-`, and none of your data is
|
|
241
|
+
stored under one. Which of them you actually see depends on what the engine
|
|
242
|
+
touches; `bridge-native-device` shows up first, because the core reads its
|
|
243
|
+
device record while the socket is still `connecting` — **before any QR, before
|
|
244
|
+
any pairing**.
|
|
245
|
+
|
|
246
|
+
That last point is the one that bites. A store that counted rows to decide
|
|
247
|
+
whether this was a first run reports a session that does not exist:
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
// WRONG on baileyrs: bridge-native-device is already in the table before pairing,
|
|
251
|
+
// so this never reports empty again — the bot skips its pairing flow and hangs.
|
|
252
|
+
const isEmpty = () => !creds.registered && !creds.me?.id && countKeys() === 0
|
|
253
|
+
|
|
254
|
+
// Right, on baileyrs and upstream alike: creds are the pairing record.
|
|
255
|
+
const isEmpty = () => !creds.registered && !creds.me?.id
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**A non-empty key store is not evidence of a session.** `creds.registered` and
|
|
259
|
+
`creds.me?.id` are, and they are the only thing to check.
|
|
260
|
+
|
|
261
|
+
When you need to present a store the way upstream would — counting rows,
|
|
262
|
+
listing the Signal namespaces, exporting an upstream-shaped dump — filter the
|
|
263
|
+
bridge rows out with the exported classifier rather than matching the prefix
|
|
264
|
+
yourself. It is derived from the internal routing catalog, so a namespace added
|
|
265
|
+
in a later release is covered without you changing anything:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// Aliased install? Import from '@whiskeysockets/baileys' instead; it resolves here.
|
|
269
|
+
import { BRIDGE_INTERNAL_KEY_TYPES, isBridgeInternalKeyType } from '@oxidezap/baileyrs'
|
|
270
|
+
|
|
271
|
+
isBridgeInternalKeyType('bridge-native-device') // true
|
|
272
|
+
isBridgeInternalKeyType('pre-key') // false
|
|
273
|
+
|
|
274
|
+
// Every bridge-internal namespace, e.g. for a SQL `NOT IN (...)` clause. The
|
|
275
|
+
// store also holds the Baileys namespaces the engine projects into it.
|
|
276
|
+
BRIDGE_INTERNAL_KEY_TYPES
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
> **Do not drop these rows from a backup or a store-to-store move.** They are
|
|
280
|
+
> effective state, not metadata. `bridge-signed-prekey`, `bridge-sender-key-devices`,
|
|
281
|
+
> `bridge-base-key`, `bridge-sent-message`, `bridge-msg-secret`,
|
|
282
|
+
> `bridge-mutation-mac` and `bridge-meta` have no Baileys projection at all, and
|
|
283
|
+
> a Signal session that turned native-only lives under `bridge-native-session`
|
|
284
|
+
> alone. Restoring only the non-bridge rows rolls those sessions back or loses
|
|
285
|
+
> the state outright. Filter the bridge rows only where the destination is an
|
|
286
|
+
> upstream-shaped view that cannot represent them; a full backup, or a move
|
|
287
|
+
> between two baileyrs stores, carries every `bridge-` row across.
|
|
288
|
+
|
|
289
|
+
This applies to stores you own — the upstream `{ creds, keys }` shape that
|
|
290
|
+
baileyrs auto-wraps, including `useLegacyMultiFileAuthState`. It does not apply
|
|
291
|
+
to `useMultiFileAuthState`, whose `keys` is a projection over the engine's own
|
|
292
|
+
store; the `bridge-` namespaces never surface through it.
|
|
223
293
|
|
|
224
294
|
## Disclaimer
|
|
225
295
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespace prefix reserved for bridge-internal state. Every namespace the
|
|
3
|
+
* routing catalog writes back to a legacy key store starts with it, and no
|
|
4
|
+
* Baileys Signal namespace does, so the prefix is treated as reserved: a
|
|
5
|
+
* namespace added to the catalog later is classified without a code change.
|
|
6
|
+
*/
|
|
7
|
+
export declare const BRIDGE_INTERNAL_KEY_PREFIX = "bridge-";
|
|
8
|
+
/**
|
|
9
|
+
* Every namespace `wrapLegacyStore` can write that is not a Signal key.
|
|
10
|
+
* Derived from the routing catalog so it cannot drift from what is written.
|
|
11
|
+
*/
|
|
12
|
+
export declare const BRIDGE_INTERNAL_KEY_TYPES: readonly string[];
|
|
13
|
+
/**
|
|
14
|
+
* Whether a `SignalKeyStore` namespace holds bridge-internal state rather than
|
|
15
|
+
* Signal key material. Custom stores use it to skip those rows when they
|
|
16
|
+
* enumerate, count or migrate. Never throws: anything that is not a known
|
|
17
|
+
* namespace, including a non-string, is reported as not internal.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isBridgeInternalKeyType(type: unknown): boolean;
|
|
20
|
+
//# sourceMappingURL=namespaces.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Public classifier for the namespaces the bridge parks in a consumer's key store.
|
|
2
|
+
import { StoreCatalog } from './constants.js';
|
|
3
|
+
/**
|
|
4
|
+
* Namespace prefix reserved for bridge-internal state. Every namespace the
|
|
5
|
+
* routing catalog writes back to a legacy key store starts with it, and no
|
|
6
|
+
* Baileys Signal namespace does, so the prefix is treated as reserved: a
|
|
7
|
+
* namespace added to the catalog later is classified without a code change.
|
|
8
|
+
*/
|
|
9
|
+
export const BRIDGE_INTERNAL_KEY_PREFIX = 'bridge-';
|
|
10
|
+
/**
|
|
11
|
+
* Every namespace `wrapLegacyStore` can write that is not a Signal key.
|
|
12
|
+
* Derived from the routing catalog so it cannot drift from what is written.
|
|
13
|
+
*/
|
|
14
|
+
export const BRIDGE_INTERNAL_KEY_TYPES = Object.freeze([...new Set(Object.values(StoreCatalog).map(route => route.nativeType))].toSorted());
|
|
15
|
+
const internalTypes = new Set(BRIDGE_INTERNAL_KEY_TYPES);
|
|
16
|
+
/**
|
|
17
|
+
* Whether a `SignalKeyStore` namespace holds bridge-internal state rather than
|
|
18
|
+
* Signal key material. Custom stores use it to skip those rows when they
|
|
19
|
+
* enumerate, count or migrate. Never throws: anything that is not a known
|
|
20
|
+
* namespace, including a non-string, is reported as not internal.
|
|
21
|
+
*/
|
|
22
|
+
export function isBridgeInternalKeyType(type) {
|
|
23
|
+
if (typeof type !== 'string' || type.length === 0)
|
|
24
|
+
return false;
|
|
25
|
+
return type.startsWith(BRIDGE_INTERNAL_KEY_PREFIX) || internalTypes.has(type);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=namespaces.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { NewsletterMetadataResult } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
|
+
import type { NewsletterMetadata, NewsletterViewRole } from '../Types/Newsletter.js';
|
|
3
|
+
/** Upstream's `NewsletterViewRole`, or undefined for a role it does not name. */
|
|
4
|
+
export declare const bridgeNewsletterRoleToBaileys: (role: string | undefined) => NewsletterViewRole | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Neutral newsletter metadata in upstream's shape.
|
|
7
|
+
*
|
|
8
|
+
* Four upstream fields have no source in the bridge result and stay absent
|
|
9
|
+
* rather than being invented: `owner` (the result carries the viewer's role,
|
|
10
|
+
* not the owner's jid), `mute_state` (the result's `state` is the newsletter's
|
|
11
|
+
* lifecycle, Active/Suspended, not a mute), `reaction_codes`, and
|
|
12
|
+
* `thread_metadata`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const bridgeNewsletterMetadataToBaileys: (result: NewsletterMetadataResult) => NewsletterMetadata;
|
|
15
|
+
//# sourceMappingURL=newsletter-results.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge spells these the way the core's enums are named, and upstream
|
|
3
|
+
* spells them in screaming case. Written out rather than upper-cased blindly so
|
|
4
|
+
* a variant the core adds later shows up as `undefined` instead of as a string
|
|
5
|
+
* nobody declared.
|
|
6
|
+
*/
|
|
7
|
+
const VERIFICATION = {
|
|
8
|
+
Verified: 'VERIFIED',
|
|
9
|
+
Unverified: 'UNVERIFIED'
|
|
10
|
+
};
|
|
11
|
+
const VIEW_ROLE = {
|
|
12
|
+
Owner: 'OWNER',
|
|
13
|
+
Admin: 'ADMIN',
|
|
14
|
+
Subscriber: 'SUBSCRIBER',
|
|
15
|
+
Guest: 'GUEST'
|
|
16
|
+
};
|
|
17
|
+
/** Upstream's `NewsletterViewRole`, or undefined for a role it does not name. */
|
|
18
|
+
export const bridgeNewsletterRoleToBaileys = (role) => role === undefined ? undefined : VIEW_ROLE[role];
|
|
19
|
+
/**
|
|
20
|
+
* Neutral newsletter metadata in upstream's shape.
|
|
21
|
+
*
|
|
22
|
+
* Four upstream fields have no source in the bridge result and stay absent
|
|
23
|
+
* rather than being invented: `owner` (the result carries the viewer's role,
|
|
24
|
+
* not the owner's jid), `mute_state` (the result's `state` is the newsletter's
|
|
25
|
+
* lifecycle, Active/Suspended, not a mute), `reaction_codes`, and
|
|
26
|
+
* `thread_metadata`.
|
|
27
|
+
*/
|
|
28
|
+
export const bridgeNewsletterMetadataToBaileys = (result) => ({
|
|
29
|
+
id: result.jid,
|
|
30
|
+
name: result.name,
|
|
31
|
+
...(result.description !== undefined ? { description: result.description } : {}),
|
|
32
|
+
...(result.inviteCode !== undefined ? { invite: result.inviteCode } : {}),
|
|
33
|
+
...(result.creationTime !== undefined ? { creation_time: result.creationTime } : {}),
|
|
34
|
+
subscribers: result.subscriberCount,
|
|
35
|
+
...(VERIFICATION[result.verification] !== undefined ? { verification: VERIFICATION[result.verification] } : {}),
|
|
36
|
+
...(result.pictureUrl !== undefined ? { picture: { url: result.pictureUrl } } : {})
|
|
37
|
+
});
|
|
38
|
+
//# sourceMappingURL=newsletter-results.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OrderResult } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
|
+
import type { CatalogPage, CollectionsPage, GetCatalogOptions, ProductCreate, ProductUpdate } from '../Types/Product.js';
|
|
3
|
+
import type { UpdateBussinesProfileProps } from '../Types/Bussines.js';
|
|
4
|
+
import type { WAMediaUpload } from '../Types/Message.js';
|
|
5
|
+
import type { SocketContext } from './types.js';
|
|
6
|
+
export declare const makeBusinessMethods: (ctx: SocketContext) => {
|
|
7
|
+
getCatalog: ({ jid, limit, cursor }: GetCatalogOptions) => Promise<CatalogPage>;
|
|
8
|
+
getCollections: (jid?: string, limit?: number) => Promise<CollectionsPage>;
|
|
9
|
+
/**
|
|
10
|
+
* `sellerJid` is not in upstream's signature because upstream reads orders
|
|
11
|
+
* over the legacy `fb:thrift_iq` route, which addresses the server. The
|
|
12
|
+
* route the real client uses is a MEX query keyed by the seller, so the JID
|
|
13
|
+
* is required here. It is on the order message the token came from.
|
|
14
|
+
*/
|
|
15
|
+
getOrderDetails: (orderId: string, tokenBase64: string, sellerJid?: string) => Promise<OrderResult>;
|
|
16
|
+
updateBussinesProfile: (args: UpdateBussinesProfileProps) => Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Declared but not callable end to end. The core and the bridge take the
|
|
19
|
+
* `{fbid, meta_hmac, ts}` receipt of a cover photo upload, and this
|
|
20
|
+
* package's upload path cannot produce one: it requires a url and a
|
|
21
|
+
* direct path, which that endpoint does not return.
|
|
22
|
+
*/
|
|
23
|
+
updateCoverPhoto: (photo: WAMediaUpload) => Promise<never>;
|
|
24
|
+
removeCoverPhoto: (id: string) => Promise<void>;
|
|
25
|
+
productCreate: (create: ProductCreate) => Promise<never>;
|
|
26
|
+
productUpdate: (productId: string, update: ProductUpdate) => Promise<never>;
|
|
27
|
+
productDelete: (productIds: string[]) => Promise<never>;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=business.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Boom } from '../Utils/boom.js';
|
|
2
|
+
import { jidNormalizedUser } from '../WABinary/jid-utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Product create, update and delete exist only in Baileys: the operations they
|
|
5
|
+
* send appear in no WhatsApp Web bundle. Declared so a migrating caller reads
|
|
6
|
+
* why instead of a bare TypeError, and rejecting because there is nothing to
|
|
7
|
+
* call.
|
|
8
|
+
*/
|
|
9
|
+
const noProductWriteRoute = (method, ...ignored) => {
|
|
10
|
+
void ignored;
|
|
11
|
+
throw new Boom(`${method} is not supported: the WhatsApp Web client has no product create, edit or delete operation, so there is nothing for this to call. Manage the catalog from the WhatsApp Business app.`, { statusCode: 501 });
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Upstream types these as strings, so an empty one is reachable from an
|
|
15
|
+
* unfilled form. `Number('')` is midnight, which would quietly rewrite the
|
|
16
|
+
* schedule instead of being refused.
|
|
17
|
+
*/
|
|
18
|
+
const minutesPastMidnight = (value, which) => {
|
|
19
|
+
const minutes = Number(value);
|
|
20
|
+
if (value.trim() === '' || !Number.isInteger(minutes) || minutes < 0 || minutes > 1440) {
|
|
21
|
+
throw new Boom(`updateBussinesProfile: ${which} time '${value}' is not a count of minutes past midnight`, {
|
|
22
|
+
statusCode: 400
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return minutes;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Both catalog reads take an optional jid and mean "mine" without one, as
|
|
29
|
+
* upstream does. `getMe` rather than `getUser`, because it reads through to the
|
|
30
|
+
* persisted credentials, which is the same place upstream reads from and is
|
|
31
|
+
* populated before the socket finishes its own initialisation.
|
|
32
|
+
*/
|
|
33
|
+
const catalogSubject = (method, ctx, jid) => {
|
|
34
|
+
const subject = jidNormalizedUser(jid || ctx.getMe()?.id);
|
|
35
|
+
if (!subject) {
|
|
36
|
+
throw new Boom(`${method}: no jid was given and there is no authenticated account, so there is no own catalog to read`, {
|
|
37
|
+
statusCode: 400
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return subject;
|
|
41
|
+
};
|
|
42
|
+
export const makeBusinessMethods = (ctx) => ({
|
|
43
|
+
getCatalog: async ({ jid, limit, cursor }) => {
|
|
44
|
+
const subject = catalogSubject('getCatalog', ctx, jid);
|
|
45
|
+
return await (await ctx.getClient()).getCatalog(subject, { limit, after: cursor });
|
|
46
|
+
},
|
|
47
|
+
getCollections: async (jid, limit) => {
|
|
48
|
+
const subject = catalogSubject('getCollections', ctx, jid);
|
|
49
|
+
return await (await ctx.getClient()).getCollections(subject, { collectionLimit: limit });
|
|
50
|
+
},
|
|
51
|
+
/**
|
|
52
|
+
* `sellerJid` is not in upstream's signature because upstream reads orders
|
|
53
|
+
* over the legacy `fb:thrift_iq` route, which addresses the server. The
|
|
54
|
+
* route the real client uses is a MEX query keyed by the seller, so the JID
|
|
55
|
+
* is required here. It is on the order message the token came from.
|
|
56
|
+
*/
|
|
57
|
+
getOrderDetails: async (orderId, tokenBase64, sellerJid) => {
|
|
58
|
+
if (!sellerJid) {
|
|
59
|
+
throw new Boom('getOrderDetails: a third argument with the seller jid is required, because orders are read through a query keyed by the business rather than the legacy server-addressed one', { statusCode: 400 });
|
|
60
|
+
}
|
|
61
|
+
return await (await ctx.getClient()).getOrder(sellerJid, orderId, tokenBase64);
|
|
62
|
+
},
|
|
63
|
+
updateBussinesProfile: async (args) => {
|
|
64
|
+
const update = {
|
|
65
|
+
address: args.address,
|
|
66
|
+
description: args.description,
|
|
67
|
+
email: args.email,
|
|
68
|
+
websites: args.websites,
|
|
69
|
+
...(args.hours !== undefined
|
|
70
|
+
? {
|
|
71
|
+
businessHours: {
|
|
72
|
+
timezone: args.hours.timezone,
|
|
73
|
+
// Minutes past midnight as a number; upstream types the two
|
|
74
|
+
// as strings and the core rejects them on the other modes.
|
|
75
|
+
config: args.hours.days.map(day => ({
|
|
76
|
+
dayOfWeek: day.day,
|
|
77
|
+
mode: day.mode,
|
|
78
|
+
openTime: day.mode === 'specific_hours' ? minutesPastMidnight(day.openTimeInMinutes, 'open') : undefined,
|
|
79
|
+
closeTime: day.mode === 'specific_hours' ? minutesPastMidnight(day.closeTimeInMinutes, 'close') : undefined
|
|
80
|
+
}))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
: {})
|
|
84
|
+
};
|
|
85
|
+
await (await ctx.getClient()).updateBusinessProfile(update);
|
|
86
|
+
},
|
|
87
|
+
/**
|
|
88
|
+
* Declared but not callable end to end. The core and the bridge take the
|
|
89
|
+
* `{fbid, meta_hmac, ts}` receipt of a cover photo upload, and this
|
|
90
|
+
* package's upload path cannot produce one: it requires a url and a
|
|
91
|
+
* direct path, which that endpoint does not return.
|
|
92
|
+
*/
|
|
93
|
+
updateCoverPhoto: async (photo) => {
|
|
94
|
+
void photo;
|
|
95
|
+
throw new Boom('updateCoverPhoto is not available yet: the cover photo upload returns an {fbid, meta_hmac, ts} receipt that this package cannot obtain. removeCoverPhoto works.', { statusCode: 501 });
|
|
96
|
+
},
|
|
97
|
+
removeCoverPhoto: async (id) => {
|
|
98
|
+
await (await ctx.getClient()).removeBusinessCoverPhoto(id);
|
|
99
|
+
},
|
|
100
|
+
productCreate: async (create) => noProductWriteRoute('productCreate', create),
|
|
101
|
+
productUpdate: async (productId, update) => noProductWriteRoute('productUpdate', productId, update),
|
|
102
|
+
productDelete: async (productIds) => noProductWriteRoute('productDelete', productIds)
|
|
103
|
+
});
|
|
104
|
+
//# sourceMappingURL=business.js.map
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { proto } from '../WAProto/runtime.js';
|
|
2
|
+
import type { QuickReplyAction } from '../Types/Bussines.js';
|
|
3
|
+
import type { LabelActionBody } from '../Types/Label.js';
|
|
4
|
+
import type { ChatModification } from '../Types/index.js';
|
|
2
5
|
import type { SocketContext } from './types.js';
|
|
3
6
|
export declare const makeChatActionMethods: (ctx: SocketContext) => {
|
|
4
7
|
pinChat: (jid: string, pin: boolean) => Promise<void>;
|
|
@@ -9,17 +12,23 @@ export declare const makeChatActionMethods: (ctx: SocketContext) => {
|
|
|
9
12
|
* Compatibility wrapper for original Baileys chatModify API.
|
|
10
13
|
* Routes to the appropriate bridge method based on the modification type.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
15
|
+
* Every variant either runs or throws. A variant that resolved without
|
|
16
|
+
* doing anything told the caller their chat was labelled when nothing was
|
|
17
|
+
* synced, and no signature or type catches that.
|
|
14
18
|
*/
|
|
15
19
|
chatModify: (mod: ChatModification, jid: string) => Promise<void>;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
addOrEditContact: (jid: string, contact: proto.SyncActionValue.IContactAction) => Promise<void>;
|
|
21
|
+
removeContact: (jid: string) => Promise<void>;
|
|
22
|
+
addLabel: (jid: string, labels: LabelActionBody) => Promise<void>;
|
|
23
|
+
addChatLabel: (jid: string, labelId: string) => Promise<void>;
|
|
24
|
+
removeChatLabel: (jid: string, labelId: string) => Promise<void>;
|
|
25
|
+
addMessageLabel: (jid: string, messageId: string, labelId: string) => Promise<void>;
|
|
26
|
+
removeMessageLabel: (jid: string, messageId: string, labelId: string) => Promise<void>;
|
|
27
|
+
star: (jid: string, messages: {
|
|
28
|
+
id: string;
|
|
29
|
+
fromMe?: boolean;
|
|
30
|
+
}[], star: boolean) => Promise<void>;
|
|
31
|
+
addOrEditQuickReply: (quickReply: QuickReplyAction) => Promise<void>;
|
|
32
|
+
removeQuickReply: (timestamp: string) => Promise<void>;
|
|
24
33
|
};
|
|
25
34
|
//# sourceMappingURL=chat-actions.d.ts.map
|
|
@@ -1,89 +1,177 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
import { Boom } from '../Utils/boom.js';
|
|
2
|
+
/**
|
|
3
|
+
* Refuse a field the mutation carries but the wire call cannot. Dropping it
|
|
4
|
+
* would report success for a change that never happened, which is the failure
|
|
5
|
+
* this whole path exists to stop.
|
|
6
|
+
*/
|
|
7
|
+
const rejectUnsupportedFields = (where, value, fields) => {
|
|
8
|
+
const present = fields.filter(field => value[field] != null);
|
|
9
|
+
if (present.length) {
|
|
10
|
+
throw new Boom(`${where}: ${present.join(', ')} cannot be set through this client`, {
|
|
11
|
+
statusCode: 400,
|
|
12
|
+
data: { fields: present }
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export const makeChatActionMethods = (ctx) => {
|
|
17
|
+
const methods = {
|
|
18
|
+
pinChat: async (jid, pin) => {
|
|
19
|
+
await (await ctx.getClient()).pinChat(jid, pin);
|
|
20
|
+
},
|
|
21
|
+
muteChat: async (jid, muteUntil) => {
|
|
22
|
+
await (await ctx.getClient()).muteChat(jid, muteUntil);
|
|
23
|
+
},
|
|
24
|
+
archiveChat: async (jid, archive) => {
|
|
25
|
+
await (await ctx.getClient()).archiveChat(jid, archive);
|
|
26
|
+
},
|
|
27
|
+
starMessage: async (jid, messageId, star) => {
|
|
28
|
+
await (await ctx.getClient()).starMessage(jid, messageId, star);
|
|
29
|
+
},
|
|
30
|
+
/**
|
|
31
|
+
* Compatibility wrapper for original Baileys chatModify API.
|
|
32
|
+
* Routes to the appropriate bridge method based on the modification type.
|
|
33
|
+
*
|
|
34
|
+
* Every variant either runs or throws. A variant that resolved without
|
|
35
|
+
* doing anything told the caller their chat was labelled when nothing was
|
|
36
|
+
* synced, and no signature or type catches that.
|
|
37
|
+
*/
|
|
38
|
+
chatModify: async (mod, jid) => {
|
|
39
|
+
const client = await ctx.getClient();
|
|
40
|
+
if ('archive' in mod) {
|
|
41
|
+
await client.archiveChat(jid, mod.archive);
|
|
35
42
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
await client.markChatAsRead(jid, mod.markRead);
|
|
39
|
-
}
|
|
40
|
-
else if ('delete' in mod) {
|
|
41
|
-
await client.deleteChat(jid);
|
|
42
|
-
}
|
|
43
|
-
else if ('deleteForMe' in mod) {
|
|
44
|
-
await client.deleteMessageForMe(jid, mod.deleteForMe.key.id, !!mod.deleteForMe.key.fromMe);
|
|
45
|
-
}
|
|
46
|
-
else if ('pushNameSetting' in mod) {
|
|
47
|
-
await client.setPushName(mod.pushNameSetting);
|
|
48
|
-
}
|
|
49
|
-
else if ('contact' in mod) {
|
|
50
|
-
// Save/rename a contact (syncs the name to linked devices). `jid` is the
|
|
51
|
-
// contact's bare PN jid.
|
|
52
|
-
if (mod.contact) {
|
|
53
|
-
await client.saveContact(jid, mod.contact.fullName ?? undefined, mod.contact.firstName ?? undefined, mod.contact.saveOnPrimaryAddressbook ?? true);
|
|
43
|
+
else if ('pin' in mod) {
|
|
44
|
+
await client.pinChat(jid, mod.pin);
|
|
54
45
|
}
|
|
55
|
-
else {
|
|
56
|
-
|
|
57
|
-
// bridge/core path yet — warn instead of silently dropping.
|
|
58
|
-
ctx.logger.warn({ jid }, 'chatModify: contact removal (contact: null) not yet supported by bridge');
|
|
46
|
+
else if ('mute' in mod) {
|
|
47
|
+
await client.muteChat(jid, mod.mute);
|
|
59
48
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// clears the whole chat. deleteStarred/deleteMedia aren't part of the
|
|
65
|
-
// Baileys `clear` shape, so default both to false (keep starred + media).
|
|
66
|
-
if (mod.clear) {
|
|
67
|
-
await client.clearChat(jid, false, false);
|
|
49
|
+
else if ('star' in mod) {
|
|
50
|
+
for (const msg of mod.star.messages) {
|
|
51
|
+
await client.starMessage(jid, msg.id, mod.star.star);
|
|
52
|
+
}
|
|
68
53
|
}
|
|
54
|
+
else if ('markRead' in mod) {
|
|
55
|
+
await client.markChatAsRead(jid, mod.markRead);
|
|
56
|
+
}
|
|
57
|
+
else if ('delete' in mod) {
|
|
58
|
+
await client.deleteChat(jid);
|
|
59
|
+
}
|
|
60
|
+
else if ('deleteForMe' in mod) {
|
|
61
|
+
await client.deleteMessageForMe(jid, mod.deleteForMe.key.id, !!mod.deleteForMe.key.fromMe);
|
|
62
|
+
}
|
|
63
|
+
else if ('pushNameSetting' in mod) {
|
|
64
|
+
await client.setPushName(mod.pushNameSetting);
|
|
65
|
+
}
|
|
66
|
+
else if ('contact' in mod) {
|
|
67
|
+
// Save/rename a contact (syncs the name to linked devices). `jid` is the
|
|
68
|
+
// contact's bare PN jid.
|
|
69
|
+
if (mod.contact) {
|
|
70
|
+
rejectUnsupportedFields('chatModify contact', mod.contact, ['lidJid', 'pnJid', 'username']);
|
|
71
|
+
await client.saveContact(jid, mod.contact.fullName ?? undefined, mod.contact.firstName ?? undefined, mod.contact.saveOnPrimaryAddressbook ?? true);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Its own method, not `saveContact` with empty fields: removal is
|
|
75
|
+
// the one contact mutation the wire models as a `Remove`, and a
|
|
76
|
+
// `Set` carrying empty values renames the contact to "".
|
|
77
|
+
await client.removeContact(jid);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if ('clear' in mod) {
|
|
81
|
+
// Clear a chat's messages while keeping the chat. `lastMessages` (the
|
|
82
|
+
// message range) is ignored, same as the `delete` branch — the bridge
|
|
83
|
+
// clears the whole chat. deleteStarred/deleteMedia aren't part of the
|
|
84
|
+
// Baileys `clear` shape, so default both to false (keep starred + media).
|
|
85
|
+
if (mod.clear) {
|
|
86
|
+
await client.clearChat(jid, false, false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if ('disableLinkPreviews' in mod) {
|
|
90
|
+
await client.setLinkPreviewsDisabled(mod.disableLinkPreviews.isPreviewsDisabled ?? false);
|
|
91
|
+
}
|
|
92
|
+
else if ('addLabel' in mod) {
|
|
93
|
+
// One upstream variant, two wire actions: an edit carrying
|
|
94
|
+
// `deleted` is the delete, not a separate modification.
|
|
95
|
+
if (mod.addLabel.deleted) {
|
|
96
|
+
await client.deleteLabel(mod.addLabel.id);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
rejectUnsupportedFields('chatModify addLabel', mod.addLabel, ['predefinedId']);
|
|
100
|
+
// The mutation replaces the whole label, so a missing field is
|
|
101
|
+
// not "leave it alone", it is "set it to nothing". Upstream can
|
|
102
|
+
// omit one because it builds the proto directly; this call
|
|
103
|
+
// cannot, so both are required rather than defaulted.
|
|
104
|
+
if (mod.addLabel.name === undefined || mod.addLabel.color === undefined) {
|
|
105
|
+
throw new Boom('chatModify addLabel: name and color are both required, because the label mutation is a full replace and an omitted field would reset it', { statusCode: 400 });
|
|
106
|
+
}
|
|
107
|
+
await client.createLabel(mod.addLabel.id, mod.addLabel.name, mod.addLabel.color);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if ('addChatLabel' in mod) {
|
|
111
|
+
await client.addChatLabel(mod.addChatLabel.labelId, jid);
|
|
112
|
+
}
|
|
113
|
+
else if ('removeChatLabel' in mod) {
|
|
114
|
+
await client.removeChatLabel(mod.removeChatLabel.labelId, jid);
|
|
115
|
+
}
|
|
116
|
+
else if ('addMessageLabel' in mod) {
|
|
117
|
+
await client.addMessageLabel(mod.addMessageLabel.labelId, jid, mod.addMessageLabel.messageId);
|
|
118
|
+
}
|
|
119
|
+
else if ('removeMessageLabel' in mod) {
|
|
120
|
+
await client.removeMessageLabel(mod.removeMessageLabel.labelId, jid, mod.removeMessageLabel.messageId);
|
|
121
|
+
}
|
|
122
|
+
else if ('quickReply' in mod) {
|
|
123
|
+
// Upstream's `timestamp` is the app-state index key, the same slot
|
|
124
|
+
// the core calls `id`. Deleting is the same upsert with `deleted`.
|
|
125
|
+
// `||`, not `??`: upstream treats an empty timestamp as absent and
|
|
126
|
+
// mints a key, and the core rejects an empty index outright.
|
|
127
|
+
const id = mod.quickReply.timestamp || String(Math.floor(Date.now() / 1000));
|
|
128
|
+
if (mod.quickReply.deleted) {
|
|
129
|
+
await client.deleteQuickReply(id);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
await client.setQuickReply(id, mod.quickReply.shortcut ?? '', mod.quickReply.message ?? '', mod.quickReply.keywords ?? [], mod.quickReply.count ?? 0);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const variant = Object.keys(mod)[0] ?? '(empty)';
|
|
137
|
+
throw new Boom(`chatModify: unsupported modification '${variant}'`, { statusCode: 400, data: { variant, jid } });
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
// Upstream models all of these as sugar over `chatModify`, and so do we:
|
|
141
|
+
// one place decides which bridge call a modification becomes, so the
|
|
142
|
+
// method and the modification can never disagree.
|
|
143
|
+
addOrEditContact: async (jid, contact) => {
|
|
144
|
+
await methods.chatModify({ contact }, jid);
|
|
145
|
+
},
|
|
146
|
+
removeContact: async (jid) => {
|
|
147
|
+
await methods.chatModify({ contact: null }, jid);
|
|
148
|
+
},
|
|
149
|
+
addLabel: async (jid, labels) => {
|
|
150
|
+
await methods.chatModify({ addLabel: { ...labels } }, jid);
|
|
151
|
+
},
|
|
152
|
+
addChatLabel: async (jid, labelId) => {
|
|
153
|
+
await methods.chatModify({ addChatLabel: { labelId } }, jid);
|
|
154
|
+
},
|
|
155
|
+
removeChatLabel: async (jid, labelId) => {
|
|
156
|
+
await methods.chatModify({ removeChatLabel: { labelId } }, jid);
|
|
157
|
+
},
|
|
158
|
+
addMessageLabel: async (jid, messageId, labelId) => {
|
|
159
|
+
await methods.chatModify({ addMessageLabel: { labelId, messageId } }, jid);
|
|
160
|
+
},
|
|
161
|
+
removeMessageLabel: async (jid, messageId, labelId) => {
|
|
162
|
+
await methods.chatModify({ removeMessageLabel: { labelId, messageId } }, jid);
|
|
163
|
+
},
|
|
164
|
+
star: async (jid, messages, star) => {
|
|
165
|
+
await methods.chatModify({ star: { messages, star } }, jid);
|
|
166
|
+
},
|
|
167
|
+
// No jid: a quick reply is account-wide, keyed only by its index.
|
|
168
|
+
addOrEditQuickReply: async (quickReply) => {
|
|
169
|
+
await methods.chatModify({ quickReply }, '');
|
|
170
|
+
},
|
|
171
|
+
removeQuickReply: async (timestamp) => {
|
|
172
|
+
await methods.chatModify({ quickReply: { timestamp, deleted: true } }, '');
|
|
69
173
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// removeChatLabel, addMessageLabel, removeMessageLabel, quickReply
|
|
74
|
-
const variant = Object.keys(mod)[0];
|
|
75
|
-
ctx.logger.warn({ variant, jid }, 'chatModify: variant requires app-state patch support not yet available in bridge');
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
/**
|
|
79
|
-
* Force re-sync of app state collections.
|
|
80
|
-
*
|
|
81
|
-
* In the Rust bridge architecture, app state is managed internally by the engine
|
|
82
|
-
* and synced automatically on connect. This method is a no-op provided for API
|
|
83
|
-
* compatibility with upstream Baileys.
|
|
84
|
-
*/
|
|
85
|
-
resyncAppState: async (_collections, _isInitialSync) => {
|
|
86
|
-
ctx.logger.info('resyncAppState: app state is synced automatically by the Rust bridge');
|
|
87
|
-
}
|
|
88
|
-
});
|
|
174
|
+
};
|
|
175
|
+
return methods;
|
|
176
|
+
};
|
|
89
177
|
//# sourceMappingURL=chat-actions.js.map
|