whisp-js 0.1.0 → 0.3.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.
@@ -1,7 +1,7 @@
1
1
  import { type EventHandlers } from './listener.js';
2
2
  import { type MediaSource } from './media.js';
3
3
  import { type Timers } from './runtime.js';
4
- import type { Chat, ChatListQuery, ChatMessagesQuery, Message, PhoneCheck, ProjectInfo, PublicEventType, SendMessageBody, SendStrategy, WebhookEvent, WhatsAppNumber } from './types.js';
4
+ import type { Chat, ChatListQuery, ChatMessagesQuery, MarkReadResult, Message, PhoneCheck, ProjectInfo, PublicEventType, SendMessageBody, SendStrategy, TypingOptions, TypingResult, WebhookEvent, WhatsAppNumber } from './types.js';
5
5
  import { type VerifyWebhookInput } from './webhook.js';
6
6
  export declare const DEFAULT_TIMEOUT_MS = 30000;
7
7
  export declare const DEFAULT_MAX_RETRIES = 2;
@@ -59,10 +59,24 @@ export interface SendOptions {
59
59
  dailyCap?: number;
60
60
  /** Makes retries safe across your own restarts too. Default: a random UUID per call, kept across its retries. */
61
61
  idempotencyKey?: string;
62
+ /**
63
+ * Phones or `<digits>@lid` JIDs to mention (0.3.0), each written `@<digits>` in the text or caption. Groups
64
+ * only; at most 50.
65
+ */
66
+ mentions?: string[];
62
67
  }
63
68
  export interface SendFileOptions extends SendOptions {
64
69
  caption?: string;
65
70
  }
71
+ /** A voice note has no text to carry `@<digits>`, so it takes no `mentions`. */
72
+ export type SendVoiceOptions = Omit<SendOptions, 'mentions'>;
73
+ export interface EditOptions {
74
+ /**
75
+ * Replaces the message's mentions (phones or `<digits>@lid` JIDs, each written `@<digits>` in `text`). Left
76
+ * out, the mentions still named in the new text are kept.
77
+ */
78
+ mentions?: string[];
79
+ }
66
80
  export interface MessagesQuery {
67
81
  reference: string;
68
82
  }
@@ -105,12 +119,47 @@ export declare class Whisp {
105
119
  sendImage(to: string, media: MediaSource, options?: SendFileOptions): Promise<Message>;
106
120
  sendVideo(to: string, media: MediaSource, options?: SendFileOptions): Promise<Message>;
107
121
  sendDocument(to: string, media: MediaSource, options?: SendFileOptions): Promise<Message>;
108
- sendVoice(to: string, media: MediaSource, options?: SendOptions): Promise<Message>;
122
+ sendVoice(to: string, media: MediaSource, options?: SendVoiceOptions): Promise<Message>;
109
123
  /**
110
124
  * `POST /messages` with a body you build (5.6). The instance's `from`,
111
125
  * `strategy`, `sticky` and `dailyCap` fill in what the body leaves out.
112
126
  */
113
127
  sendMessage(body: SendMessageBody, options?: Pick<SendOptions, 'idempotencyKey'>): Promise<Message>;
128
+ /**
129
+ * Sets this number's reaction to a message (one emoji), replacing its earlier one. Resolves once the server
130
+ * queued it for the number's connection; a `message.reaction` event with `fromMe: true` follows.
131
+ */
132
+ react(message: Message | string, emoji: string): Promise<void>;
133
+ /** Removes this number's reaction to a message. */
134
+ unreact(message: Message | string): Promise<void>;
135
+ /**
136
+ * Edits the text or caption of a message this number sent, within WhatsApp's 15 minutes
137
+ * (EDIT_WINDOW_PASSED after). Refused on a paused number (NUMBER_PAUSED). Resolves the edited message; the
138
+ * same text without `mentions` resolves it unchanged.
139
+ */
140
+ edit(message: Message | string, text: string, options?: EditOptions): Promise<Message>;
141
+ /**
142
+ * Deletes a message this number sent for everyone, within 2 days (REVOKE_WINDOW_PASSED after). Resolves the
143
+ * message with `deletedAt` set, also when it was deleted already.
144
+ */
145
+ revoke(message: Message | string): Promise<Message>;
146
+ /**
147
+ * Marks a chat of this instance's number read (a phone, a `cht_` id or a group JID). Unread is one count per
148
+ * chat, so this clears it for the panel's members too. `readReceipts` says what happened to the blue ticks:
149
+ * `queued` (handed to the server, not proof WhatsApp got them), `off` (the number's setting) or `none`
150
+ * (nothing was unread).
151
+ */
152
+ markRead(chat: string): Promise<MarkReadResult>;
153
+ /**
154
+ * Shows "typing..." (or "recording audio..." with `state: 'recording'`) in a chat of this instance's number (a
155
+ * phone, a `cht_` id or a group JID). The server keeps it up until a message of the number goes out in that chat
156
+ * (no need to stop before sending), stopTyping(), or 2 minutes from the latest start (call it again to keep
157
+ * going). Refused on a paused number (NUMBER_PAUSED), in a group the number left (NOT_IN_GROUP) and on a number
158
+ * that isn't connected or connecting (NUMBER_NOT_CONNECTED).
159
+ */
160
+ startTyping(chat: string, options?: TypingOptions): Promise<TypingResult>;
161
+ /** Stops this number's typing in a chat at once ("paused"). Safe to call when nothing is typing. */
162
+ stopTyping(chat: string): Promise<void>;
114
163
  /** Every number of the project, any status. */
115
164
  numbers(): Promise<WhatsAppNumber[]>;
116
165
  /** One message of the project's numbers; 404 MESSAGE_NOT_FOUND otherwise. */
@@ -137,7 +186,7 @@ export declare class Whisp {
137
186
  */
138
187
  downloadMedia(message: Message | string, options?: DownloadOptions): Promise<Response>;
139
188
  /**
140
- * Calls `handler` for each event of this name (12.4): any of the twelve
189
+ * Calls `handler` for each event of this name (12.4): any of the public
141
190
  * types, `'message'` (= `message.received`), `'*'`, or the stream's own
142
191
  * `'open'`, `'close'`, `'gap'` and `'error'`. The first event handler
143
192
  * opens the stream. Returns `off()`.
@@ -17,6 +17,11 @@ exports.Whisp = exports.MEDIA_WAIT_MAX_MS = exports.DEFAULT_MAX_RETRIES = export
17
17
  * that need one number require. `dailyCap` (phase3-design 12.4) goes with
18
18
  * every send; the server counts and enforces it, never the SDK, since
19
19
  * several app instances share one number.
20
+ *
21
+ * 0.3.0 (api-gaps-design 12, T6): reactions, edits, deletes for everyone,
22
+ * marking a chat read and typing presence, and `mentions` on text and
23
+ * caption sends. None of them carries an `Idempotency-Key`: each is
24
+ * idempotent by state on the server, so they are retried like reads.
20
25
  */
21
26
  const constants_ts_1 = require("./constants.js");
22
27
  const errors_ts_1 = require("./errors.js");
@@ -83,6 +88,23 @@ const normalizeFrom = (phone) => {
83
88
  throw (0, errors_ts_1.localError)('OPTIONS_INVALID', `A pool has at most ${constants_ts_1.FROM_MAX} numbers`);
84
89
  return refs;
85
90
  };
91
+ /** `mentions` as the API takes them: a list of strings, checked here only for its shape (the server parses each). */
92
+ const checkMentions = (value, name) => {
93
+ if (value === undefined)
94
+ return undefined;
95
+ if (!Array.isArray(value) || value.length === 0 || value.some(item => typeof item !== 'string')) {
96
+ throw (0, errors_ts_1.localError)('OPTIONS_INVALID', `${name} must be a non-empty list of phones or <digits>@lid JIDs`);
97
+ }
98
+ return value;
99
+ };
100
+ /** A message's id: the message itself or its `msg_` id. */
101
+ const messageIdOf = (message, method) => {
102
+ const id = typeof message === 'string' ? message : message?.id;
103
+ if (typeof id !== 'string' || !/^msg_[0-9A-Za-z]{1,64}$/.test(id)) {
104
+ throw (0, errors_ts_1.localError)('OPTIONS_INVALID', `${method}() takes a message or its msg_ id`);
105
+ }
106
+ return id;
107
+ };
86
108
  const waitMs = (wait) => wait === true ? constants_ts_1.WAIT_DEFAULT_MS : typeof wait === 'number' && Number.isFinite(wait) ? wait : 0;
87
109
  const defaultReport = (error) => {
88
110
  console.error(`[whisp-sdk] ${error.name}: ${error.message}`);
@@ -256,7 +278,8 @@ class Whisp {
256
278
  const cap = checkDailyCap(perCall, 'dailyCap') ?? this.#options.dailyCap;
257
279
  return cap === undefined ? {} : { dailyCap: cap };
258
280
  }
259
- #common(options) {
281
+ /** The body fields every send shares; `mentions` only where the kind has text to carry them. */
282
+ #common(options, { mentions = true } = {}) {
260
283
  const out = { ...this.#pool(), ...this.#cap(options.dailyCap) };
261
284
  if (options.replyTo !== undefined)
262
285
  out.replyTo = options.replyTo;
@@ -264,6 +287,9 @@ class Whisp {
264
287
  out.reference = options.reference;
265
288
  if (options.wait !== undefined)
266
289
  out.wait = options.wait;
290
+ const listed = mentions ? checkMentions(options.mentions, 'mentions') : undefined;
291
+ if (listed !== undefined)
292
+ out.mentions = listed;
267
293
  return out;
268
294
  }
269
295
  async #send(body, options) {
@@ -280,8 +306,8 @@ class Whisp {
280
306
  return expect(answer, 'POST /messages', message => typeof message.id === 'string');
281
307
  }
282
308
  async #sendFile(kind, to, media, options, caption) {
283
- // The options first: a bad one fails before any file is uploaded.
284
- const common = this.#common(options);
309
+ // The options first: a bad one fails before any file is uploaded. A voice note never carries mentions.
310
+ const common = this.#common(options, { mentions: kind !== 'voice' });
285
311
  let input;
286
312
  if (isRecord(media) && typeof media.url === 'string') {
287
313
  input = { url: media.url };
@@ -318,6 +344,90 @@ class Whisp {
318
344
  }, upload.url, upload.headers, file);
319
345
  return upload.uploadId;
320
346
  }
347
+ // Message actions (0.3.0) ---------------------------------------------------------------------
348
+ /**
349
+ * Sets this number's reaction to a message (one emoji), replacing its earlier one. Resolves once the server
350
+ * queued it for the number's connection; a `message.reaction` event with `fromMe: true` follows.
351
+ */
352
+ async react(message, emoji) {
353
+ if (typeof emoji !== 'string' || emoji === '') {
354
+ throw (0, errors_ts_1.localError)('OPTIONS_INVALID', 'react() takes one emoji; unreact() removes the reaction');
355
+ }
356
+ await this.#reaction(messageIdOf(message, 'react'), emoji);
357
+ }
358
+ /** Removes this number's reaction to a message. */
359
+ async unreact(message) {
360
+ await this.#reaction(messageIdOf(message, 'unreact'), '');
361
+ }
362
+ async #reaction(id, emoji) {
363
+ expect(await this.#call({ method: 'POST', path: `/messages/${segment(id)}/reaction`, body: { emoji } }), 'POST /messages/:messageId/reaction', body => body.accepted === true);
364
+ }
365
+ /**
366
+ * Edits the text or caption of a message this number sent, within WhatsApp's 15 minutes
367
+ * (EDIT_WINDOW_PASSED after). Refused on a paused number (NUMBER_PAUSED). Resolves the edited message; the
368
+ * same text without `mentions` resolves it unchanged.
369
+ */
370
+ async edit(message, text, options = {}) {
371
+ const id = messageIdOf(message, 'edit');
372
+ if (typeof text !== 'string' || text === '')
373
+ throw (0, errors_ts_1.localError)('OPTIONS_INVALID', 'edit() takes the new text');
374
+ const body = { text };
375
+ const mentions = checkMentions(options.mentions, 'mentions');
376
+ if (mentions !== undefined)
377
+ body.mentions = mentions;
378
+ return expect(await this.#call({ method: 'POST', path: `/messages/${segment(id)}/edit`, body }), 'POST /messages/:messageId/edit', answer => typeof answer.id === 'string');
379
+ }
380
+ /**
381
+ * Deletes a message this number sent for everyone, within 2 days (REVOKE_WINDOW_PASSED after). Resolves the
382
+ * message with `deletedAt` set, also when it was deleted already.
383
+ */
384
+ async revoke(message) {
385
+ const id = messageIdOf(message, 'revoke');
386
+ return expect(
387
+ // An empty object: the server takes no body, and every runtime then sends a length.
388
+ await this.#call({ method: 'POST', path: `/messages/${segment(id)}/revoke`, body: {} }), 'POST /messages/:messageId/revoke', answer => typeof answer.id === 'string');
389
+ }
390
+ /**
391
+ * Marks a chat of this instance's number read (a phone, a `cht_` id or a group JID). Unread is one count per
392
+ * chat, so this clears it for the panel's members too. `readReceipts` says what happened to the blue ticks:
393
+ * `queued` (handed to the server, not proof WhatsApp got them), `off` (the number's setting) or `none`
394
+ * (nothing was unread).
395
+ */
396
+ async markRead(chat) {
397
+ const number = this.#single('markRead');
398
+ return expect(await this.#call({
399
+ method: 'POST',
400
+ path: `/numbers/${segment(number)}/chats/${segment(chat)}/read`,
401
+ body: {},
402
+ }), 'POST /numbers/:number/chats/:chat/read', body => isRecord(body.chat) && typeof body.readReceipts === 'string');
403
+ }
404
+ /**
405
+ * Shows "typing..." (or "recording audio..." with `state: 'recording'`) in a chat of this instance's number (a
406
+ * phone, a `cht_` id or a group JID). The server keeps it up until a message of the number goes out in that chat
407
+ * (no need to stop before sending), stopTyping(), or 2 minutes from the latest start (call it again to keep
408
+ * going). Refused on a paused number (NUMBER_PAUSED), in a group the number left (NOT_IN_GROUP) and on a number
409
+ * that isn't connected or connecting (NUMBER_NOT_CONNECTED).
410
+ */
411
+ async startTyping(chat, options = {}) {
412
+ const number = this.#single('startTyping');
413
+ const state = options.state ?? 'composing';
414
+ if (state !== 'composing' && state !== 'recording') {
415
+ throw (0, errors_ts_1.localError)('OPTIONS_INVALID', "startTyping()'s state must be 'composing' or 'recording'");
416
+ }
417
+ const body = { state };
418
+ return expect(await this.#call({ method: 'POST', path: `/numbers/${segment(number)}/chats/${segment(chat)}/typing`, body }), 'POST /numbers/:number/chats/:chat/typing', answer => typeof answer.state === 'string');
419
+ }
420
+ /** Stops this number's typing in a chat at once ("paused"). Safe to call when nothing is typing. */
421
+ async stopTyping(chat) {
422
+ const number = this.#single('stopTyping');
423
+ expect(
424
+ // An empty object: the server takes no body, and every runtime then sends a length.
425
+ await this.#call({
426
+ method: 'POST',
427
+ path: `/numbers/${segment(number)}/chats/${segment(chat)}/typing/stop`,
428
+ body: {},
429
+ }), 'POST /numbers/:number/chats/:chat/typing/stop', answer => answer.state === 'paused');
430
+ }
321
431
  // Reads ---------------------------------------------------------------------------------------
322
432
  /** Every number of the project, any status. */
323
433
  async numbers() {
@@ -39,5 +39,5 @@ export declare const UPLOAD_MAX_BYTES: {
39
39
  readonly voice: number;
40
40
  readonly document: number;
41
41
  };
42
- /** PLAN's twelve, in the order the API lists them. */
43
- export declare const PUBLIC_EVENT_TYPES: readonly ["message.received", "message.sent", "message.status", "message.reaction", "message.deleted", "chat.updated", "presence.updated", "group.updated", "group.participants", "number.status", "number.risk", "call.received"];
42
+ /** PLAN's twelve, then `message.edited` (0.3.0), in the order the API lists them. */
43
+ export declare const PUBLIC_EVENT_TYPES: readonly ["message.received", "message.sent", "message.status", "message.reaction", "message.deleted", "chat.updated", "presence.updated", "group.updated", "group.participants", "number.status", "number.risk", "call.received", "message.edited"];
@@ -42,7 +42,7 @@ exports.UPLOAD_MAX_BYTES = {
42
42
  voice: 16 * 1024 * 1024,
43
43
  document: 100 * 1024 * 1024,
44
44
  };
45
- /** PLAN's twelve, in the order the API lists them. */
45
+ /** PLAN's twelve, then `message.edited` (0.3.0), in the order the API lists them. */
46
46
  exports.PUBLIC_EVENT_TYPES = [
47
47
  'message.received',
48
48
  'message.sent',
@@ -56,4 +56,5 @@ exports.PUBLIC_EVENT_TYPES = [
56
56
  'number.status',
57
57
  'number.risk',
58
58
  'call.received',
59
+ 'message.edited',
59
60
  ];
@@ -9,7 +9,7 @@
9
9
  * await whisp.send('972521112233', 'Hello from Whisp')
10
10
  * whisp.on('message', msg => console.log(msg.from, msg.text))
11
11
  */
12
- export { DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, type DownloadOptions, type MessagesQuery, type Page, type PageResult, type SendFileOptions, type SendOptions, type VerifyResult, Whisp, type WhispOptions, } from './client.js';
12
+ export { DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT_MS, type DownloadOptions, type EditOptions, type MessagesQuery, type Page, type PageResult, type SendFileOptions, type SendOptions, type SendVoiceOptions, type VerifyResult, Whisp, type WhispOptions, } from './client.js';
13
13
  export { API_TOKEN_ENV, API_URL_ENV, DEFAULT_BASE_URL, EVENT_CURSOR_PREFIX, PUBLIC_EVENT_TYPES, SDK_PACKAGE_NAME, SIGNATURE_HEADER, SIGNATURE_TOLERANCE_SECONDS, TOKEN_PREFIX, USER_AGENT_PREFIX, WEBHOOK_SECRET_PREFIX, } from './constants.js';
14
14
  export { type ErrorParams, SDK_ERROR_CODES, type SdkErrorCode, type SignatureFailureReason, WhispConnectionError, WhispError, WhispSignatureError, } from './errors.js';
15
15
  export type { AnyListenerEvent, EventHandlers, EventMeta, ListenerEvent, ListenerName, StreamClose, StreamGap, } from './listener.js';
@@ -28,7 +28,7 @@ export type ListenerEvent<T extends PublicEventType> = Omit<EventData[T], 'numbe
28
28
  numberId: string;
29
29
  event: EventMeta<T>;
30
30
  };
31
- /** Any of the twelve; `event.event.type` says which. */
31
+ /** Any of the public types; `event.event.type` says which. */
32
32
  export type AnyListenerEvent = {
33
33
  [T in PublicEventType]: ListenerEvent<T>;
34
34
  }[PublicEventType];
@@ -51,7 +51,7 @@ export type EventHandlers = {
51
51
  } & {
52
52
  /** `message.received`. */
53
53
  message: Handler<ListenerEvent<'message.received'>>;
54
- /** Every one of the twelve types. */
54
+ /** Every one of the public types. */
55
55
  '*': Handler<AnyListenerEvent>;
56
56
  /** A connection reached `ready`. */
57
57
  open: Handler<StreamReady>;
@@ -27,6 +27,7 @@ exports.Listener = exports.toListenerEvent = exports.RATE_LIMITED_WAIT_MS = expo
27
27
  * `'error'` and the stream goes on. The cursor moves when an event's
28
28
  * handlers have settled, and at once for an event nobody listens to.
29
29
  */
30
+ const constants_ts_1 = require("./constants.js");
30
31
  const errors_ts_1 = require("./errors.js");
31
32
  const runtime_ts_1 = require("./runtime.js");
32
33
  const sse_ts_1 = require("./sse.js");
@@ -38,20 +39,8 @@ exports.WATCHDOG_MS = 45_000;
38
39
  /** The wait after a 429 (TOO_MANY_STREAMS or RATE_LIMITED), unless `Retry-After` asks for longer (up to a minute). */
39
40
  exports.RATE_LIMITED_WAIT_MS = 30_000;
40
41
  const RATE_LIMITED_MAX_WAIT_MS = 60_000;
41
- const KNOWN_TYPES = new Set([
42
- 'message.received',
43
- 'message.sent',
44
- 'message.status',
45
- 'message.reaction',
46
- 'message.deleted',
47
- 'chat.updated',
48
- 'presence.updated',
49
- 'group.updated',
50
- 'group.participants',
51
- 'number.status',
52
- 'number.risk',
53
- 'call.received',
54
- ]);
42
+ /** The types this version delivers; any other is skipped (its cursor still moves). */
43
+ const KNOWN_TYPES = new Set(constants_ts_1.PUBLIC_EVENT_TYPES);
55
44
  const LIFECYCLE = new Set(['open', 'close', 'gap', 'error']);
56
45
  const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
57
46
  const asError = (value) => (value instanceof Error ? value : new Error(String(value)));
@@ -34,8 +34,12 @@ export type ChatChange = 'unread' | 'done' | 'pinned' | 'assignee' | 'labels' |
34
34
  export type CursorRejection = 'cursor_unknown' | 'cursor_expired';
35
35
  /** Every message type but the inbox's own `waiting` stubs and `system` lines. */
36
36
  export type MessageType = 'text' | 'image' | 'video' | 'audio' | 'voice' | 'document' | 'sticker' | 'location' | 'contact' | 'poll' | 'unsupported';
37
- /** PLAN's twelve public event types (6.1). */
38
- export type PublicEventType = 'message.received' | 'message.sent' | 'message.status' | 'message.reaction' | 'message.deleted' | 'chat.updated' | 'presence.updated' | 'group.updated' | 'group.participants' | 'number.status' | 'number.risk' | 'call.received';
37
+ /** PLAN's twelve public event types (6.1), then `message.edited` (0.3.0). */
38
+ export type PublicEventType = 'message.received' | 'message.sent' | 'message.status' | 'message.reaction' | 'message.deleted' | 'chat.updated' | 'presence.updated' | 'group.updated' | 'group.participants' | 'number.status' | 'number.risk' | 'call.received' | 'message.edited';
39
+ /** What marking a chat read did about read receipts: handed to the server (not delivery), off, or nothing unread. */
40
+ export type ReadReceiptOutcome = 'queued' | 'off' | 'none';
41
+ /** What startTyping shows: `composing` ("typing...") or `recording` ("recording audio..."). */
42
+ export type TypingStartState = 'composing' | 'recording';
39
43
  /** Someone on WhatsApp. `phone` is null for a LID-only contact. */
40
44
  export type Actor = {
41
45
  phone: string | null;
@@ -179,6 +183,11 @@ export type Message = {
179
183
  at: string;
180
184
  editedAt: string | null;
181
185
  deletedAt: string | null;
186
+ /**
187
+ * The people the text mentions, in order (0.3.0); `@` and the digits of their `phone` or `lid` mark each in
188
+ * `text`. Empty for none and for a deleted message.
189
+ */
190
+ mentions: Actor[];
182
191
  };
183
192
  /** The number an event happened on. */
184
193
  export type EventNumber = {
@@ -216,6 +225,19 @@ export type MessageDeletedData = {
216
225
  reference: string | null;
217
226
  at: string;
218
227
  };
228
+ /** A message's text or caption changed, by its sender or by us (0.3.0). `at` is the edit's own time. */
229
+ export type MessageEditedData = {
230
+ messageId: string;
231
+ waId: string | null;
232
+ chatId: string;
233
+ fromMe: boolean;
234
+ /** The new text or caption. */
235
+ text: string | null;
236
+ /** The message's mentions after the edit. */
237
+ mentions: Actor[];
238
+ reference: string | null;
239
+ at: string;
240
+ };
219
241
  export type ChatUpdatedData = {
220
242
  chat: Chat;
221
243
  changes: ChatChange[];
@@ -308,6 +330,7 @@ export type EventData = {
308
330
  'number.status': NumberStatusData;
309
331
  'number.risk': NumberRiskData;
310
332
  'call.received': CallReceivedData;
333
+ 'message.edited': MessageEditedData;
311
334
  };
312
335
  /** The envelope: the SSE `data:`, an item of `GET /events` and a webhook body are this same object. */
313
336
  export type EventOf<T extends PublicEventType> = {
@@ -370,6 +393,7 @@ export type SendMessageBody = {
370
393
  dailyCap?: number;
371
394
  type?: 'text';
372
395
  text: string;
396
+ mentions?: string[];
373
397
  from?: string | string[];
374
398
  strategy?: SendStrategy;
375
399
  sticky?: boolean;
@@ -382,6 +406,7 @@ export type SendMessageBody = {
382
406
  type: 'image' | 'video' | 'document';
383
407
  media: MediaInput;
384
408
  caption?: string;
409
+ mentions?: string[];
385
410
  from?: string | string[];
386
411
  strategy?: SendStrategy;
387
412
  sticky?: boolean;
@@ -406,6 +431,7 @@ export type SendFromNumberBody = {
406
431
  dailyCap?: number;
407
432
  type?: 'text';
408
433
  text: string;
434
+ mentions?: string[];
409
435
  } | {
410
436
  to: string;
411
437
  replyTo?: string;
@@ -415,6 +441,7 @@ export type SendFromNumberBody = {
415
441
  type: 'image' | 'video' | 'document';
416
442
  media: MediaInput;
417
443
  caption?: string;
444
+ mentions?: string[];
418
445
  } | {
419
446
  to: string;
420
447
  replyTo?: string;
@@ -434,6 +461,7 @@ export type SendMediaFromNumberBody = {
434
461
  type: 'image' | 'video' | 'document';
435
462
  media: MediaInput;
436
463
  caption?: string;
464
+ mentions?: string[];
437
465
  } | {
438
466
  to: string;
439
467
  replyTo?: string;
@@ -508,6 +536,45 @@ export type Upload = {
508
536
  headers: Record<string, string>;
509
537
  expiresAt: string;
510
538
  };
539
+ /** `POST /messages/:messageId/reaction`: one emoji, or `''` to remove ours. */
540
+ export type ReactionBody = {
541
+ emoji: string;
542
+ };
543
+ /**
544
+ * `POST /messages/:messageId/edit`: the new text or caption. `mentions` (phones or `<digits>@lid` JIDs, each
545
+ * written `@<digits>` in the text) replaces the message's; left out, the ones still named in the text are kept.
546
+ */
547
+ export type EditMessageBody = {
548
+ text: string;
549
+ mentions?: string[];
550
+ };
551
+ /** 202 for a reaction: queued for the number's socket. */
552
+ export type ActionAccepted = {
553
+ accepted: true;
554
+ };
555
+ /** `POST /numbers/:number/chats/:chat/read`: the chat as it is now, and what happened to read receipts. */
556
+ export type MarkReadResult = {
557
+ chat: Chat;
558
+ readReceipts: ReadReceiptOutcome;
559
+ };
560
+ /** `POST /numbers/:number/chats/:chat/typing`: `composing` when left out. */
561
+ export type StartTypingBody = {
562
+ state?: TypingStartState;
563
+ };
564
+ /** startTyping's options. */
565
+ export interface TypingOptions {
566
+ /** `composing` (the default): "typing...". `recording`: "recording audio...". */
567
+ state?: TypingStartState;
568
+ }
569
+ /**
570
+ * 202 from both typing routes: the state asked for (`paused` for a stop), and when the server stops refreshing it
571
+ * at the latest (2 minutes after this start). Null for a stop, and while the server's worker is an older build that
572
+ * shows typing once, with no keep-alive.
573
+ */
574
+ export type TypingResult = {
575
+ state: TypingStartState | 'paused';
576
+ keptUntil: string | null;
577
+ };
511
578
  /** Every error the API answers. `code` is stable; `params` carries the values it names. */
512
579
  export type ApiError = {
513
580
  code: string;
@@ -1,2 +1,2 @@
1
1
  /** The package's version: sent in the User-Agent. packages/sdk/test/constants.test.ts keeps it equal to package.json's. */
2
- export declare const SDK_VERSION = "0.1.0";
2
+ export declare const SDK_VERSION = "0.3.1";
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SDK_VERSION = void 0;
4
4
  /** The package's version: sent in the User-Agent. packages/sdk/test/constants.test.ts keeps it equal to package.json's. */
5
- exports.SDK_VERSION = '0.1.0';
5
+ exports.SDK_VERSION = '0.3.1';
@@ -1,7 +1,7 @@
1
1
  import { type EventHandlers } from './listener.js';
2
2
  import { type MediaSource } from './media.js';
3
3
  import { type Timers } from './runtime.js';
4
- import type { Chat, ChatListQuery, ChatMessagesQuery, Message, PhoneCheck, ProjectInfo, PublicEventType, SendMessageBody, SendStrategy, WebhookEvent, WhatsAppNumber } from './types.js';
4
+ import type { Chat, ChatListQuery, ChatMessagesQuery, MarkReadResult, Message, PhoneCheck, ProjectInfo, PublicEventType, SendMessageBody, SendStrategy, TypingOptions, TypingResult, WebhookEvent, WhatsAppNumber } from './types.js';
5
5
  import { type VerifyWebhookInput } from './webhook.js';
6
6
  export declare const DEFAULT_TIMEOUT_MS = 30000;
7
7
  export declare const DEFAULT_MAX_RETRIES = 2;
@@ -59,10 +59,24 @@ export interface SendOptions {
59
59
  dailyCap?: number;
60
60
  /** Makes retries safe across your own restarts too. Default: a random UUID per call, kept across its retries. */
61
61
  idempotencyKey?: string;
62
+ /**
63
+ * Phones or `<digits>@lid` JIDs to mention (0.3.0), each written `@<digits>` in the text or caption. Groups
64
+ * only; at most 50.
65
+ */
66
+ mentions?: string[];
62
67
  }
63
68
  export interface SendFileOptions extends SendOptions {
64
69
  caption?: string;
65
70
  }
71
+ /** A voice note has no text to carry `@<digits>`, so it takes no `mentions`. */
72
+ export type SendVoiceOptions = Omit<SendOptions, 'mentions'>;
73
+ export interface EditOptions {
74
+ /**
75
+ * Replaces the message's mentions (phones or `<digits>@lid` JIDs, each written `@<digits>` in `text`). Left
76
+ * out, the mentions still named in the new text are kept.
77
+ */
78
+ mentions?: string[];
79
+ }
66
80
  export interface MessagesQuery {
67
81
  reference: string;
68
82
  }
@@ -105,12 +119,47 @@ export declare class Whisp {
105
119
  sendImage(to: string, media: MediaSource, options?: SendFileOptions): Promise<Message>;
106
120
  sendVideo(to: string, media: MediaSource, options?: SendFileOptions): Promise<Message>;
107
121
  sendDocument(to: string, media: MediaSource, options?: SendFileOptions): Promise<Message>;
108
- sendVoice(to: string, media: MediaSource, options?: SendOptions): Promise<Message>;
122
+ sendVoice(to: string, media: MediaSource, options?: SendVoiceOptions): Promise<Message>;
109
123
  /**
110
124
  * `POST /messages` with a body you build (5.6). The instance's `from`,
111
125
  * `strategy`, `sticky` and `dailyCap` fill in what the body leaves out.
112
126
  */
113
127
  sendMessage(body: SendMessageBody, options?: Pick<SendOptions, 'idempotencyKey'>): Promise<Message>;
128
+ /**
129
+ * Sets this number's reaction to a message (one emoji), replacing its earlier one. Resolves once the server
130
+ * queued it for the number's connection; a `message.reaction` event with `fromMe: true` follows.
131
+ */
132
+ react(message: Message | string, emoji: string): Promise<void>;
133
+ /** Removes this number's reaction to a message. */
134
+ unreact(message: Message | string): Promise<void>;
135
+ /**
136
+ * Edits the text or caption of a message this number sent, within WhatsApp's 15 minutes
137
+ * (EDIT_WINDOW_PASSED after). Refused on a paused number (NUMBER_PAUSED). Resolves the edited message; the
138
+ * same text without `mentions` resolves it unchanged.
139
+ */
140
+ edit(message: Message | string, text: string, options?: EditOptions): Promise<Message>;
141
+ /**
142
+ * Deletes a message this number sent for everyone, within 2 days (REVOKE_WINDOW_PASSED after). Resolves the
143
+ * message with `deletedAt` set, also when it was deleted already.
144
+ */
145
+ revoke(message: Message | string): Promise<Message>;
146
+ /**
147
+ * Marks a chat of this instance's number read (a phone, a `cht_` id or a group JID). Unread is one count per
148
+ * chat, so this clears it for the panel's members too. `readReceipts` says what happened to the blue ticks:
149
+ * `queued` (handed to the server, not proof WhatsApp got them), `off` (the number's setting) or `none`
150
+ * (nothing was unread).
151
+ */
152
+ markRead(chat: string): Promise<MarkReadResult>;
153
+ /**
154
+ * Shows "typing..." (or "recording audio..." with `state: 'recording'`) in a chat of this instance's number (a
155
+ * phone, a `cht_` id or a group JID). The server keeps it up until a message of the number goes out in that chat
156
+ * (no need to stop before sending), stopTyping(), or 2 minutes from the latest start (call it again to keep
157
+ * going). Refused on a paused number (NUMBER_PAUSED), in a group the number left (NOT_IN_GROUP) and on a number
158
+ * that isn't connected or connecting (NUMBER_NOT_CONNECTED).
159
+ */
160
+ startTyping(chat: string, options?: TypingOptions): Promise<TypingResult>;
161
+ /** Stops this number's typing in a chat at once ("paused"). Safe to call when nothing is typing. */
162
+ stopTyping(chat: string): Promise<void>;
114
163
  /** Every number of the project, any status. */
115
164
  numbers(): Promise<WhatsAppNumber[]>;
116
165
  /** One message of the project's numbers; 404 MESSAGE_NOT_FOUND otherwise. */
@@ -137,7 +186,7 @@ export declare class Whisp {
137
186
  */
138
187
  downloadMedia(message: Message | string, options?: DownloadOptions): Promise<Response>;
139
188
  /**
140
- * Calls `handler` for each event of this name (12.4): any of the twelve
189
+ * Calls `handler` for each event of this name (12.4): any of the public
141
190
  * types, `'message'` (= `message.received`), `'*'`, or the stream's own
142
191
  * `'open'`, `'close'`, `'gap'` and `'error'`. The first event handler
143
192
  * opens the stream. Returns `off()`.