@vanzxy/baileys 2.0.0 → 2.0.2

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.
@@ -0,0 +1,144 @@
1
+ import { BaseBuilder, Toolkit, RowBuilder, generateWAMessageFromContent, prepareWAMessageMedia, generateMessageIDV2, crypto } from './shared.js';
2
+ class ButtonV2 extends BaseBuilder {
3
+ #client;
4
+
5
+ /** @param {import('../../WAProto/index.js').WASocket} client Active Baileys socket. */
6
+ constructor(client) {
7
+ super();
8
+ if (!client) {
9
+ throw new Error('Socket is required');
10
+ }
11
+
12
+ this.#client = client;
13
+ this._image;
14
+ this._data;
15
+ this._buttons = [];
16
+ }
17
+
18
+ /** Add a simple quick-reply button. @param {string} displayText Label. @param {string} [buttonId] Defaults to a random uuid. */
19
+ addButton(displayText = '', buttonId = crypto.randomUUID()) {
20
+ if (!displayText) throw new TypeError('addButton(displayText) requires a non-empty label');
21
+ this._buttons.push({
22
+ buttonId,
23
+ buttonText: { displayText },
24
+ type: 1,
25
+ });
26
+ return this;
27
+ }
28
+
29
+ /** Push a raw pre-built button object, bypassing the `addButton()` shorthand. */
30
+ addRawButton(obj) {
31
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
32
+ throw new TypeError('Buttons must be a plain object');
33
+ }
34
+
35
+ this._buttons.push(obj);
36
+ return this;
37
+ }
38
+
39
+ /** Set the header thumbnail (used as a fallback location-header image when no `setMedia()` header is given). */
40
+ setThumbnail(path) {
41
+ if (!path) throw new Error('Url or buffer needed');
42
+ this._image = path;
43
+ return this;
44
+ }
45
+
46
+ /** Set a raw pre-built header media object for the buttons message. */
47
+ setMedia(obj) {
48
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
49
+ throw new TypeError('Media must be a plain object');
50
+ }
51
+
52
+ this._data = obj;
53
+ return this;
54
+ }
55
+
56
+ /** Alias for addButton() — shorthand parity with RowBuilder#button(). */
57
+ button(displayText, buttonId) {
58
+ return this.addButton(displayText, buttonId);
59
+ }
60
+
61
+ /**
62
+ * Vanz@Add 29-08-26 --- Fluent row helper ported from the RowBuilder class (already
63
+ * present but previously unwired into ButtonV2). Lets callers group buttons via a
64
+ * callback instead of chaining addButton() calls one at a time.
65
+ * @param {(row: RowBuilder) => void} cb
66
+ */
67
+ row(cb) {
68
+ const r = new RowBuilder();
69
+ cb(r);
70
+ r.buttons.forEach((b) => this._buttons.push(b));
71
+ return this;
72
+ }
73
+
74
+ // Vanz@Fix 22-08-26 (v4.7) --- _thumbnail was computed unconditionally (fetch + resize) even
75
+ // when setMedia() is used, in which case the location-fallback header (the only place
76
+ // _thumbnail is used) never runs at all — wasted network/CPU work on every build() call.
77
+ // Now only computed when it'll actually be used. Also: `viewOnce` was hardcoded true with no
78
+ // way to opt out (kept as the default — some clients need it to render legacy buttonsMessage
79
+ // at all — but it's now a `{ viewOnce = true }` option instead of a hardcoded literal).
80
+ /** @returns {Promise<Record<string, any>>} The generated WAMessage (without sending). @param {boolean} [viewOnce] Default true — some clients require this for legacy buttonsMessage to render; pass false to send it as a normal (non-disappearing) message. */
81
+ async build(jid, { viewOnce = true, ...options } = {}) {
82
+ const _thumbnail = !this._data && this._image ? await Toolkit.resize(Buffer.isBuffer(this._image) ? this._image : await Toolkit.fetchBuffer(this._image, {}, { silent: true }), 300, 300) : null;
83
+ const msg = generateWAMessageFromContent(
84
+ jid,
85
+ {
86
+ ...this._extraPayload,
87
+ buttonsMessage: {
88
+ contentText: this._body,
89
+ footerText: this._footer,
90
+ ...(this._data
91
+ ? this._data
92
+ : {
93
+ headerType: 6,
94
+ locationMessage: {
95
+ degreesLatitude: 0,
96
+ degreesLongitude: 0,
97
+ name: this._title,
98
+ address: this._subtitle,
99
+ jpegThumbnail: _thumbnail,
100
+ },
101
+ }),
102
+ viewOnce,
103
+ contextInfo: this._contextInfo,
104
+ buttons: [...this._buttons],
105
+ },
106
+ },
107
+ { ...options }
108
+ );
109
+ return msg;
110
+ }
111
+
112
+ /** Build and send this buttons message. @param {string} jid Destination chat/group jid. */
113
+ async send(jid, { ...options } = {}) {
114
+ if (this._buttons.length < 1) throw new Error('ButtonV2 requires at least one button');
115
+ const msg = await this.build(jid, options);
116
+
117
+ await this.#client.relayMessage(msg.key.remoteJid, msg.message, {
118
+ messageId: msg.key.id,
119
+ additionalNodes: [
120
+ {
121
+ tag: 'biz',
122
+ attrs: {},
123
+ content: [
124
+ {
125
+ tag: 'interactive',
126
+ attrs: { type: 'native_flow', v: '1' },
127
+ content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }],
128
+ },
129
+ ],
130
+ },
131
+ ],
132
+ ...options,
133
+ });
134
+ return msg;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Legacy `templateMessage` / `hydratedFourRowTemplate` builder — WA's Generation-1
140
+ * button protocol (predates the nativeFlow format that Button/ButtonV2 use).
141
+ * Capped at 3 buttons (quickReply/url/call only), no interactive list/flow support.
142
+ * Ported from MessageBuilderV4.7.
143
+ */
144
+ export { ButtonV2 };
@@ -0,0 +1,184 @@
1
+ import { BaseBuilder, generateWAMessageFromContent, prepareWAMessageMedia, generateMessageIDV2, crypto } from './shared.js';
2
+ class ButtonV3 extends BaseBuilder {
3
+ #client;
4
+
5
+ /** @param {import('../../WAProto/index.js').WASocket} client Active Baileys socket. */
6
+ constructor(client) {
7
+ super();
8
+ if (!client) {
9
+ throw new Error('Socket is required');
10
+ }
11
+
12
+ this.#client = client;
13
+ this._data;
14
+ this._mediaHeaderType = null;
15
+ this._buttons = [];
16
+ }
17
+
18
+ /** Load an existing templateMessage (e.g. from a fetched/quoted message) for editing. */
19
+ loadFrom(msg) {
20
+ if (!msg) throw new Error('templateMessage needed');
21
+ if (!msg.templateMessage) throw new Error('templateMessage not found');
22
+
23
+ const { templateMessage, ...extraPayload } = msg;
24
+ const hft = templateMessage.hydratedFourRowTemplate || {};
25
+
26
+ this._title = hft.hydratedTitleText || '';
27
+ this._body = hft.hydratedContentText || '';
28
+ this._footer = hft.hydratedFooterText || '';
29
+ this._contextInfo = templateMessage.contextInfo || {};
30
+ this._extraPayload = extraPayload;
31
+
32
+ this._buttons = Array.isArray(hft.hydratedButtons)
33
+ ? hft.hydratedButtons.map((button) => ({ ...button }))
34
+ : [];
35
+
36
+ if (hft.imageMessage) {
37
+ this._data = { imageMessage: hft.imageMessage };
38
+ this._mediaHeaderType = 'imageMessage';
39
+ } else if (hft.videoMessage) {
40
+ this._data = { videoMessage: hft.videoMessage };
41
+ this._mediaHeaderType = 'videoMessage';
42
+ } else if (hft.documentMessage) {
43
+ this._data = { documentMessage: hft.documentMessage };
44
+ this._mediaHeaderType = 'documentMessage';
45
+ } else if (hft.locationMessage) {
46
+ this._data = { locationMessage: hft.locationMessage };
47
+ this._mediaHeaderType = 'locationMessage';
48
+ } else {
49
+ this._data = undefined;
50
+ this._mediaHeaderType = null;
51
+ }
52
+
53
+ return this;
54
+ }
55
+
56
+ setImage(path, options = {}) {
57
+ if (!path) throw new Error('Url or buffer needed');
58
+ this._data = Buffer.isBuffer(path)
59
+ ? { image: path, ...options }
60
+ : { image: { url: path }, ...options };
61
+ this._mediaHeaderType = 'imageMessage';
62
+ return this;
63
+ }
64
+
65
+ setVideo(path, options = {}) {
66
+ if (!path) throw new Error('Url or buffer needed');
67
+ this._data = Buffer.isBuffer(path)
68
+ ? { video: path, ...options }
69
+ : { video: { url: path }, ...options };
70
+ this._mediaHeaderType = 'videoMessage';
71
+ return this;
72
+ }
73
+
74
+ setDocument(path, options = {}) {
75
+ if (!path) throw new Error('Url or buffer needed');
76
+ this._data = Buffer.isBuffer(path)
77
+ ? { document: path, ...options }
78
+ : { document: { url: path }, ...options };
79
+ this._mediaHeaderType = 'documentMessage';
80
+ return this;
81
+ }
82
+
83
+ setMedia(obj) {
84
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
85
+ throw new TypeError('Media must be a plain object');
86
+ }
87
+ this._data = obj;
88
+ this._mediaHeaderType = null; // caller is expected to pass an already-resolved shape
89
+ return this;
90
+ }
91
+
92
+ clearButtons() {
93
+ this._buttons = [];
94
+ return this;
95
+ }
96
+
97
+ addButton(hydratedButton) {
98
+ if (this._buttons.length >= 3) {
99
+ throw new Error('ButtonV3 (TemplateMessage) supports a maximum of 3 buttons');
100
+ }
101
+ this._buttons.push({ index: this._buttons.length + 1, ...hydratedButton });
102
+ return this;
103
+ }
104
+
105
+ addReply(display_text = '', id = '') {
106
+ return this.addButton({
107
+ quickReplyButton: { displayText: display_text, id },
108
+ });
109
+ }
110
+
111
+ addUrl(display_text = '', url = '', options = {}) {
112
+ return this.addButton({
113
+ urlButton: { displayText: display_text, url, ...options },
114
+ });
115
+ }
116
+
117
+ addCall(display_text = '', phone_number = '') {
118
+ return this.addButton({
119
+ callButton: { displayText: display_text, phoneNumber: phone_number },
120
+ });
121
+ }
122
+
123
+ async toTemplate() {
124
+ let mediaFields = {};
125
+
126
+ if (this._data) {
127
+ const alreadyResolved =
128
+ this._data.imageMessage || this._data.videoMessage ||
129
+ this._data.documentMessage || this._data.locationMessage;
130
+
131
+ mediaFields = alreadyResolved
132
+ ? this._data
133
+ : await prepareWAMessageMedia(this._data, {
134
+ upload: this.#client.waUploadToServer,
135
+ }).catch((e) => {
136
+ if (String(e).includes('Invalid media type')) return this._data;
137
+ throw e;
138
+ });
139
+ } else if (this._title) {
140
+ mediaFields = { hydratedTitleText: this._title };
141
+ }
142
+
143
+ return {
144
+ hydratedContentText: this._body,
145
+ hydratedFooterText: this._footer,
146
+ hydratedButtons: this._buttons,
147
+ ...mediaFields,
148
+ };
149
+ }
150
+
151
+ async build(jid, { messageId, ...options } = {}) {
152
+ const hydratedFourRowTemplate = await this.toTemplate();
153
+
154
+ return generateWAMessageFromContent(
155
+ jid,
156
+ {
157
+ ...this._extraPayload,
158
+ templateMessage: {
159
+ hydratedFourRowTemplate,
160
+ contextInfo: this._contextInfo,
161
+ },
162
+ },
163
+ { messageId: messageId || generateMessageIDV2(), ...options },
164
+ );
165
+ }
166
+
167
+ async send(jid, { messageId, additionalNodes = [], ...options } = {}) {
168
+ if (this._buttons.length < 1)
169
+ throw new Error('ButtonV3 requires at least one button');
170
+ const msg = await this.build(jid, { messageId, ...options });
171
+
172
+ // TemplateMessage predates the nativeFlow protocol and does not need the
173
+ // "biz"/"native_flow" additionalNodes hack that Button/ButtonV2 use.
174
+ await this.#client.relayMessage(msg.key.remoteJid, msg.message, {
175
+ messageId: msg.key.id,
176
+ additionalNodes,
177
+ ...options,
178
+ });
179
+ return msg;
180
+ }
181
+ }
182
+
183
+ /** Carousel of interactive cards (each with its own header media + optional buttons), scrollable horizontally in-chat. */
184
+ export { ButtonV3 };
@@ -0,0 +1,111 @@
1
+ import { BaseBuilder, generateWAMessageFromContent, prepareWAMessageMedia, generateMessageIDV2, crypto } from './shared.js';
2
+ class Carousel extends BaseBuilder {
3
+ #client;
4
+
5
+ // Vanz@Add 22-08-26 (v4.7) --- WhatsApp caps carousels at 10 cards; anything beyond
6
+ // that is silently truncated client-side, so failing fast here is more useful than
7
+ // shipping a carousel that quietly loses cards.
8
+ static MAX_CARDS = 10;
9
+
10
+ /** @param {import('../../WAProto/index.js').WASocket} client Active Baileys socket. */
11
+ constructor(client) {
12
+ super();
13
+ if (!client) {
14
+ throw new Error('Socket is required');
15
+ }
16
+
17
+ this.#client = client;
18
+ this._cards = [];
19
+ }
20
+
21
+ /**
22
+ * Add one card, or an array of cards, to the carousel.
23
+ * @param {Record<string, any>|Record<string, any>[]} card A card (or array of cards) with `header.hasMediaAttachment: true`
24
+ * — typically built via `new Button(client).setImage(...).addUrl(...).toCard()`.
25
+ */
26
+ addCard(card) {
27
+ const cards = Array.isArray(card) ? card : [card];
28
+ const baseIndex = this._cards.length;
29
+
30
+ for (const [index, c] of cards.entries()) {
31
+ if (!c?.header?.hasMediaAttachment) {
32
+ throw new Error(`Card [${baseIndex + index}] must include an image or video in header`);
33
+ }
34
+ }
35
+
36
+ if (this._cards.length + cards.length > Carousel.MAX_CARDS) {
37
+ throw new Error(`Carousel supports at most ${Carousel.MAX_CARDS} cards (got ${this._cards.length + cards.length})`);
38
+ }
39
+
40
+ this._cards.push(...cards);
41
+ return this;
42
+ }
43
+
44
+ /** @returns {Record<string, any>} The generated WAMessage (without sending). */
45
+ build(jid, { ...options } = {}) {
46
+ return generateWAMessageFromContent(
47
+ jid,
48
+ {
49
+ ...this._extraPayload,
50
+ interactiveMessage: {
51
+ header: {
52
+ hasMediaAttachment: false,
53
+ },
54
+ body: { text: this._body },
55
+ footer: { text: this._footer },
56
+ contextInfo: this._contextInfo,
57
+ carouselMessage: {
58
+ cards: this._cards,
59
+ },
60
+ },
61
+ },
62
+ { ...options }
63
+ );
64
+ }
65
+
66
+ /** Build and send this carousel. @param {string} jid Destination chat/group jid. */
67
+ async send(jid, { ...options } = {}) {
68
+ if (this._cards.length === 0) throw new Error('Carousel requires at least one card (use addCard())');
69
+
70
+ const msg = this.build(jid, options);
71
+
72
+ await this.#client.relayMessage(msg.key.remoteJid, msg.message, {
73
+ messageId: msg.key.id,
74
+ additionalNodes: [
75
+ {
76
+ tag: 'biz',
77
+ attrs: {},
78
+ content: [
79
+ {
80
+ tag: 'interactive',
81
+ attrs: { type: 'native_flow', v: '1' },
82
+ content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }],
83
+ },
84
+ ],
85
+ },
86
+ ],
87
+ ...options,
88
+ });
89
+ return msg;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Vanz@Add (v4.8) --- Chainable poll builder, wrapping the socket's own well-tested
95
+ * `sendMessage({ poll })` path (see messages.js) instead of hand-building
96
+ * pollCreationMessageV3/V5 over relayMessage. Two things from the traffic you sent were
97
+ * deliberately NOT implemented here because they can't be built with confidence:
98
+ * 1. Per-option poll images (`values: [{ name, image }]`) — the proto this fork ships
99
+ * only has a plain `optionName` string per option; an image-poll option isn't a named
100
+ * field anywhere in it. The one place an image-poll concept even appears
101
+ * (`pollCreationOptionImageMessage`) is typed as an opaque `FutureProofMessage` (a
102
+ * forward-compat envelope with no documented inner layout) — there's no field list to
103
+ * target, so adding "support" for it would just be silently dropping the image and
104
+ * guessing at a shape. Flagging instead of faking it.
105
+ * 2. Quiz-mode `correctAnswer.optionHash` built by hand — the one working example you
106
+ * captured had a 65-character hex string where a sha256 digest should be 64, and this
107
+ * builder's target `sendMessage({poll})` path (pollCreationMessageV5) already computes
108
+ * quiz mode correctly from a plain `correctAnswer` string, so `setQuiz()` below defers to
109
+ * that existing, already-tested logic rather than reimplementing the hash.
110
+ */
111
+ export { Carousel };
@@ -0,0 +1,83 @@
1
+ import { BaseBuilder, generateWAMessageFromContent, prepareWAMessageMedia, generateMessageIDV2, crypto } from './shared.js';
2
+
3
+ /**
4
+ * Contact-card message builder — one contact sends as `contactMessage`, two or
5
+ * more as `contactsArrayMessage` (both handled by the existing `message.contacts`
6
+ * shorthand in Utils/messages.js). Accepts either a raw vCard string via
7
+ * `addRawVcard()` or plain fields via `addContact()`, which builds a minimal
8
+ * VCARD 3.0 block itself.
9
+ */
10
+ class Contact extends BaseBuilder {
11
+ #client;
12
+
13
+ /** @param {import('../../WAProto/index.js').WASocket} client Active Baileys socket (must expose `sendMessage`). */
14
+ constructor(client) {
15
+ super();
16
+ if (!client) throw new Error('Socket is required');
17
+ this.#client = client;
18
+
19
+ this._contacts = [];
20
+ this._displayName;
21
+ }
22
+
23
+ /** Name shown on the card when sending more than one contact (`contactsArrayMessage.displayName`). Ignored for a single contact. */
24
+ setDisplayName(name) {
25
+ this._displayName = name;
26
+ return this;
27
+ }
28
+
29
+ /**
30
+ * Add a contact from plain fields — builds a minimal VCARD 3.0 block.
31
+ * @param {{name: string, phone?: string|string[], org?: string, email?: string, waid?: string}} fields
32
+ * `waid` (digits-only, no `+`) makes the number tappable to chat/call on WhatsApp; without it the number is still shown but may not be tappable on every client.
33
+ */
34
+ addContact({ name, phone, org, email, waid } = {}) {
35
+ if (typeof name !== 'string' || !name) throw new TypeError('addContact({name, ...}) requires a non-empty name');
36
+
37
+ const phones = phone === undefined ? [] : Array.isArray(phone) ? phone : [phone];
38
+ const lines = ['BEGIN:VCARD', 'VERSION:3.0', `FN:${name}`, `N:${name};;;;`];
39
+ phones.forEach((number, i) => {
40
+ const type = waid && i === 0 ? `TYPE=CELL,WAID=${waid}` : 'TYPE=CELL';
41
+ lines.push(`TEL;${type}:${number}`);
42
+ });
43
+ if (org) lines.push(`ORG:${org}`);
44
+ if (email) lines.push(`EMAIL:${email}`);
45
+ lines.push('END:VCARD');
46
+
47
+ this._contacts.push({ displayName: name, vcard: lines.join('\n') });
48
+ return this;
49
+ }
50
+
51
+ /** Add a contact from a pre-built vCard string (bypasses `addContact()`'s auto-generation). */
52
+ addRawVcard(displayName, vcard) {
53
+ if (typeof displayName !== 'string' || !displayName) throw new TypeError('addRawVcard(displayName, vcard) requires a non-empty displayName');
54
+ if (typeof vcard !== 'string' || !vcard) throw new TypeError('addRawVcard(displayName, vcard) requires a non-empty vcard string');
55
+ this._contacts.push({ displayName, vcard });
56
+ return this;
57
+ }
58
+
59
+ /** Remove every contact added so far. */
60
+ clearContacts() {
61
+ this._contacts = [];
62
+ return this;
63
+ }
64
+
65
+ /** @returns {{contacts: Record<string, any>}} The `sendMessage()`-shaped payload, without sending it. */
66
+ build() {
67
+ if (!this._contacts.length) throw new Error('Contact requires at least one contact (use addContact()/addRawVcard())');
68
+
69
+ return {
70
+ contacts: {
71
+ ...(this._displayName !== undefined && { displayName: this._displayName }),
72
+ contacts: this._contacts,
73
+ },
74
+ };
75
+ }
76
+
77
+ /** Build and send via the socket's `sendMessage()`. */
78
+ async send(jid, options = {}) {
79
+ return this.#client.sendMessage(jid, this.build(), options);
80
+ }
81
+ }
82
+
83
+ export { Contact };
@@ -0,0 +1,151 @@
1
+ import { BaseBuilder, generateWAMessageFromContent, prepareWAMessageMedia, generateMessageIDV2, crypto } from './shared.js';
2
+ import { proto } from '../../WAProto/index.js';
3
+
4
+ /**
5
+ * Location message builder — static pin via the existing `message.location`
6
+ * shorthand (see Utils/messages.js, `hasNonNullishProperty(message, 'location')`),
7
+ * or a live/moving location. There is no `liveLocation` shorthand in
8
+ * `generateWAMessageContent` yet, so the live path goes through the `raw: true`
9
+ * escape hatch (same file, top of `generateWAMessageContent`) with a hand-built
10
+ * `liveLocationMessage`.
11
+ */
12
+ class Location extends BaseBuilder {
13
+ #client;
14
+
15
+ /** @param {import('../../WAProto/index.js').WASocket} client Active Baileys socket (must expose `sendMessage`). */
16
+ constructor(client) {
17
+ super();
18
+ if (!client) throw new Error('Socket is required');
19
+ this.#client = client;
20
+
21
+ this._lat;
22
+ this._lng;
23
+ this._name;
24
+ this._address;
25
+ this._url;
26
+ this._thumbnail;
27
+ this._live = false;
28
+ this._accuracy;
29
+ this._speed;
30
+ this._degrees;
31
+ this._caption;
32
+ this._sequence;
33
+ this._timeOffset;
34
+ }
35
+
36
+ /** Set the pin's coordinates. Required. */
37
+ setCoordinates(lat, lng) {
38
+ if (typeof lat !== 'number' || typeof lng !== 'number') throw new TypeError('setCoordinates(lat, lng) requires two numbers');
39
+ this._lat = lat;
40
+ this._lng = lng;
41
+ return this;
42
+ }
43
+
44
+ /** Place name shown above the address (static location only). */
45
+ setName(name) {
46
+ this._name = name;
47
+ return this;
48
+ }
49
+
50
+ /** Address line shown under the name (static location only). */
51
+ setAddress(address) {
52
+ this._address = address;
53
+ return this;
54
+ }
55
+
56
+ /** External map URL attached to the pin (static location only). */
57
+ setUrl(url) {
58
+ this._url = url;
59
+ return this;
60
+ }
61
+
62
+ /** Raw jpeg thumbnail bytes for the map preview. */
63
+ setThumbnail(buffer) {
64
+ this._thumbnail = buffer;
65
+ return this;
66
+ }
67
+
68
+ /** Switch between a static pin (default) and a live/moving location. */
69
+ setLive(isLive = true) {
70
+ this._live = isLive;
71
+ return this;
72
+ }
73
+
74
+ /** GPS accuracy radius in meters (live location only). */
75
+ setAccuracy(meters) {
76
+ this._accuracy = meters;
77
+ return this;
78
+ }
79
+
80
+ /** Current speed in meters/second (live location only). */
81
+ setSpeed(mps) {
82
+ this._speed = mps;
83
+ return this;
84
+ }
85
+
86
+ /** Heading in degrees clockwise from magnetic north (live location only). */
87
+ setDegrees(degreesClockwiseFromMagneticNorth) {
88
+ this._degrees = degreesClockwiseFromMagneticNorth;
89
+ return this;
90
+ }
91
+
92
+ /** Caption text shown with a live location update. */
93
+ setCaption(text) {
94
+ this._caption = text;
95
+ return this;
96
+ }
97
+
98
+ /** Ordinal of this update within a live-location session — bump it on each subsequent send so clients render the latest pin instead of stacking old ones. */
99
+ setSequence(n) {
100
+ this._sequence = n;
101
+ return this;
102
+ }
103
+
104
+ /** Seconds since the live-location session started. */
105
+ setTimeOffset(n) {
106
+ this._timeOffset = n;
107
+ return this;
108
+ }
109
+
110
+ /** @returns {Record<string, any>} The `sendMessage()`-shaped payload, without sending it. */
111
+ build() {
112
+ if (typeof this._lat !== 'number' || typeof this._lng !== 'number') {
113
+ throw new Error('Location requires setCoordinates(lat, lng)');
114
+ }
115
+
116
+ if (!this._live) {
117
+ return {
118
+ location: {
119
+ degreesLatitude: this._lat,
120
+ degreesLongitude: this._lng,
121
+ ...(this._name !== undefined && { name: this._name }),
122
+ ...(this._address !== undefined && { address: this._address }),
123
+ ...(this._url !== undefined && { url: this._url }),
124
+ ...(this._thumbnail !== undefined && { jpegThumbnail: this._thumbnail }),
125
+ },
126
+ };
127
+ }
128
+
129
+ return {
130
+ raw: true,
131
+ liveLocationMessage: proto.Message.LiveLocationMessage.create({
132
+ degreesLatitude: this._lat,
133
+ degreesLongitude: this._lng,
134
+ ...(this._accuracy !== undefined && { accuracyInMeters: this._accuracy }),
135
+ ...(this._speed !== undefined && { speedInMps: this._speed }),
136
+ ...(this._degrees !== undefined && { degreesClockwiseFromMagneticNorth: this._degrees }),
137
+ ...(this._caption !== undefined && { caption: this._caption }),
138
+ ...(this._sequence !== undefined && { sequenceNumber: this._sequence }),
139
+ ...(this._timeOffset !== undefined && { timeOffset: this._timeOffset }),
140
+ ...(this._thumbnail !== undefined && { jpegThumbnail: this._thumbnail }),
141
+ }),
142
+ };
143
+ }
144
+
145
+ /** Build and send via the socket's `sendMessage()`. Call again with a higher `setSequence()` to push live-location updates. */
146
+ async send(jid, options = {}) {
147
+ return this.#client.sendMessage(jid, this.build(), options);
148
+ }
149
+ }
150
+
151
+ export { Location };