@oxidezap/baileyrs 0.0.35 → 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 +147 -24
- 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/Compatibility/proto-runtime.js +30 -20
- package/lib/Compatibility/websocket-client.d.ts +23 -2
- package/lib/Compatibility/websocket-client.js +47 -18
- package/lib/Socket/bridge-client-owner.d.ts +89 -0
- package/lib/Socket/bridge-client-owner.js +135 -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.d.ts +31 -0
- package/lib/Socket/events.js +144 -41
- package/lib/Socket/index.d.ts +113 -16
- package/lib/Socket/index.js +468 -157
- package/lib/Socket/internals.d.ts +88 -0
- package/lib/Socket/internals.js +145 -0
- package/lib/Socket/messages.d.ts +1 -12
- package/lib/Socket/messages.js +3 -20
- 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/terminal-close-reporter.d.ts +79 -0
- package/lib/Socket/terminal-close-reporter.js +108 -0
- package/lib/Socket/terminal-close.d.ts +39 -0
- package/lib/Socket/terminal-close.js +51 -0
- package/lib/Socket/types.d.ts +6 -0
- package/lib/Types/Product.d.ts +9 -0
- package/lib/Utils/event-buffer.js +31 -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 +31 -7
- package/lib/Utils/messages.js +49 -17
- package/lib/Utils/wrap-legacy-store.d.ts +1 -0
- package/lib/Utils/wrap-legacy-store.js +1 -0
- package/package.json +4 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns the bridge client's lifetime.
|
|
3
|
+
*
|
|
4
|
+
* The socket's startup is async and its teardown can start at any point during
|
|
5
|
+
* it — a `sock.end()` right after `makeWASocket()`, an `await using` scope
|
|
6
|
+
* exiting, or a terminal disconnect the dispatcher reports while `init()` is
|
|
7
|
+
* still building the client. That window used to be managed by hand across six
|
|
8
|
+
* closure variables and a scattering of `if (ended) return` checks, which is
|
|
9
|
+
* where every teardown bug in this file came from: startup dereferencing a
|
|
10
|
+
* handle teardown had already nulled, a client built after teardown with
|
|
11
|
+
* nobody left to free it, an `end()` that resolved for its second caller while
|
|
12
|
+
* the first was still flushing, and a re-entrant close releasing the same wasm
|
|
13
|
+
* handle twice.
|
|
14
|
+
*
|
|
15
|
+
* All of those are one question — *what phase is this client in?* — so it is
|
|
16
|
+
* one discriminated union rather than a set of booleans that can disagree:
|
|
17
|
+
*
|
|
18
|
+
* ```
|
|
19
|
+
* starting ──adopt()──► running ──close()──► closing ──► closed
|
|
20
|
+
* │ │ ▲
|
|
21
|
+
* └──────close()────────┴──discard()─────────┘
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Two rules make the whole thing safe, and both are properties of the union
|
|
25
|
+
* rather than of any individual method:
|
|
26
|
+
*
|
|
27
|
+
* - **The transition is synchronous and happens first.** `close()` publishes
|
|
28
|
+
* `closing` — carrying the promise callers await — before any teardown work
|
|
29
|
+
* starts, so re-entering it finds an in-flight close instead of starting a
|
|
30
|
+
* second one. That is why the work is deferred by a microtask rather than
|
|
31
|
+
* started inline: an async body would otherwise run eagerly to its first
|
|
32
|
+
* `await`, i.e. before the state was stored.
|
|
33
|
+
* - **`closing` still carries the client.** The socket's transport close
|
|
34
|
+
* reads it back through `peek()` (`ws.close()` is `getClient()?.disconnect()`),
|
|
35
|
+
* so dropping the handle at the start of teardown silently turns that into
|
|
36
|
+
* a no-op and moves the disconnect after the auth-store flush. `isClosing()`
|
|
37
|
+
* — not `peek()` — is the "should I still be doing work" signal.
|
|
38
|
+
*/
|
|
39
|
+
export const makeBridgeClientOwner = (opts) => {
|
|
40
|
+
const { logger, teardown, release } = opts;
|
|
41
|
+
let state = { phase: 'starting' };
|
|
42
|
+
/** Releases started outside `close()`, so `settled()` can join them. */
|
|
43
|
+
const pendingReleases = new Set();
|
|
44
|
+
const releaseQuietly = async (target) => {
|
|
45
|
+
try {
|
|
46
|
+
await release(target);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
logger.error({ err }, 'failed to release the bridge client');
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/** `releaseQuietly`, but joinable through `settled()`. */
|
|
53
|
+
const trackRelease = (target) => {
|
|
54
|
+
const running = releaseQuietly(target).finally(() => pendingReleases.delete(running));
|
|
55
|
+
pendingReleases.add(running);
|
|
56
|
+
return running;
|
|
57
|
+
};
|
|
58
|
+
/** Drain releases started outside this close — see `runClose`. */
|
|
59
|
+
const drainReleases = async () => {
|
|
60
|
+
while (pendingReleases.size)
|
|
61
|
+
await Promise.all(pendingReleases);
|
|
62
|
+
};
|
|
63
|
+
const runClose = async (client, error, done) => {
|
|
64
|
+
try {
|
|
65
|
+
await teardown(client, error);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
state = { phase: 'closed', done };
|
|
69
|
+
if (client)
|
|
70
|
+
await releaseQuietly(client);
|
|
71
|
+
// A `discard()` in flight when this close started put the client
|
|
72
|
+
// back in `starting`, so the capture above found none and teardown
|
|
73
|
+
// ran without it. Its release is still going: joining here keeps
|
|
74
|
+
// `close()` from settling while a client is being disconnected and
|
|
75
|
+
// freed, which is what the caller is told it can rely on.
|
|
76
|
+
await drainReleases();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
peek: () => (state.phase === 'running' || state.phase === 'closing' ? state.client : undefined),
|
|
81
|
+
adopt: candidate => {
|
|
82
|
+
if (state.phase !== 'starting') {
|
|
83
|
+
// Teardown has already been through here and found nothing, so
|
|
84
|
+
// this client would have no owner: nothing would free it and its
|
|
85
|
+
// read loop would reconnect forever against a disposed socket.
|
|
86
|
+
void trackRelease(candidate);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
state = { phase: 'running', client: candidate };
|
|
90
|
+
return true;
|
|
91
|
+
},
|
|
92
|
+
isClosing: () => state.phase === 'closing' || state.phase === 'closed',
|
|
93
|
+
close: error => {
|
|
94
|
+
if (state.phase === 'closing' || state.phase === 'closed') {
|
|
95
|
+
logger.trace({ trace: error?.stack }, 'already closing; awaiting the in-flight teardown');
|
|
96
|
+
return state.done;
|
|
97
|
+
}
|
|
98
|
+
const client = state.phase === 'running' ? state.client : undefined;
|
|
99
|
+
// Deferred by a microtask so the `closing` state below is stored
|
|
100
|
+
// before any teardown work runs. Calling `runClose` inline would
|
|
101
|
+
// execute its body eagerly up to the first `await` — teardown starts
|
|
102
|
+
// by closing the transport — and anything reached synchronously that
|
|
103
|
+
// calls back into `close()` would find no in-flight close and start
|
|
104
|
+
// a second teardown, releasing the same wasm handle twice.
|
|
105
|
+
const done = Promise.resolve().then(() => runClose(client, error, done));
|
|
106
|
+
state = { phase: 'closing', client, done };
|
|
107
|
+
return done;
|
|
108
|
+
},
|
|
109
|
+
discard: async () => {
|
|
110
|
+
// `close()` owns the client from the moment it starts, and keeps it
|
|
111
|
+
// published for the whole of teardown — releasing it here as well
|
|
112
|
+
// would be two disconnect/free sequences on one handle. Join instead.
|
|
113
|
+
//
|
|
114
|
+
// Joining without adopting the failure: `close()` rejects when
|
|
115
|
+
// teardown rethrows the first auth-store flush error, and this is
|
|
116
|
+
// best-effort cleanup called from `init()`'s catch. Propagating it
|
|
117
|
+
// would make `initPromise` reject — which the socket documents as
|
|
118
|
+
// impossible, relies on for `getClient()`'s error message, and does
|
|
119
|
+
// not always await, so it could surface as an unhandled rejection.
|
|
120
|
+
if (state.phase === 'closing' || state.phase === 'closed') {
|
|
121
|
+
await state.done.catch(() => { });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (state.phase !== 'running')
|
|
125
|
+
return;
|
|
126
|
+
const { client } = state;
|
|
127
|
+
// Back to `starting`, not `closed`: a later `close()` must still run
|
|
128
|
+
// teardown for everything the socket owns beyond the client.
|
|
129
|
+
state = { phase: 'starting' };
|
|
130
|
+
await trackRelease(client);
|
|
131
|
+
},
|
|
132
|
+
settled: drainReleases
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
//# sourceMappingURL=bridge-client-owner.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
|
package/lib/Socket/events.d.ts
CHANGED
|
@@ -20,6 +20,37 @@ interface EventCallbacks {
|
|
|
20
20
|
onDirtyState?: (event: Extract<CanonicalEvent, {
|
|
21
21
|
type: 'dirtyState';
|
|
22
22
|
}>) => void;
|
|
23
|
+
/**
|
|
24
|
+
* The engine has stopped reconnecting: this client is dead weight only
|
|
25
|
+
* `free()` can reclaim.
|
|
26
|
+
*
|
|
27
|
+
* Owns publishing the `close` too — `publish()` must be called, and the
|
|
28
|
+
* point of handing it over is that the socket can finish tearing down
|
|
29
|
+
* first. Upstream does the same, emitting its close only after `ws.close()`
|
|
30
|
+
* and the end handlers (`Socket/socket.ts`). A consumer answering `close`
|
|
31
|
+
* with a replacement socket on the same auth folder would otherwise race
|
|
32
|
+
* the old one's store flush and `free()`.
|
|
33
|
+
*/
|
|
34
|
+
onTerminalClose?: (error: Error, publish: () => void) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Hand back a cleanup for anything the dispatcher armed that outlives a
|
|
37
|
+
* single event — today the history-sync pause timer. The socket registers
|
|
38
|
+
* it as an end handler, so a plain `sock.end()` or an `await using` scope
|
|
39
|
+
* exiting cancels it too: only the terminal-close path goes through
|
|
40
|
+
* `emitClose`, and a timer surviving disposal fires
|
|
41
|
+
* `messaging-history.status: paused` from a socket that is already gone.
|
|
42
|
+
*
|
|
43
|
+
* Called once, during `makeEventHandlers`.
|
|
44
|
+
*/
|
|
45
|
+
onCleanup?: (cleanup: () => void) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Whether `sock.setAutoReconnect(true)` is in effect. A plain drop is only
|
|
48
|
+
* transient while the engine still intends to retry: with auto-reconnect
|
|
49
|
+
* off, the run loop dispatches `Disconnected` and then breaks for good
|
|
50
|
+
* (`client/lifecycle.rs` tests the flag *after* the dispatch), so the same
|
|
51
|
+
* event becomes terminal. Absent callback means the default, enabled.
|
|
52
|
+
*/
|
|
53
|
+
isAutoReconnectEnabled?: () => boolean;
|
|
23
54
|
}
|
|
24
55
|
/**
|
|
25
56
|
* Create typed single and batch handlers for the bridge. The bridge only uses
|