Skip to content

Commit 952ad5f

Browse files
committed
refactor(events): group new payload fields after the is-flags and rename isOffline to isOld
1 parent 44bc873 commit 952ad5f

7 files changed

Lines changed: 34 additions & 46 deletions

File tree

docs/content/events.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ client.on('message', async (msg) => {
298298
| `senderLid` | `string \| null` | The sender's LID (linked-device identifier), if known. Keeps the raw LID even after `senderId` is resolved to PN. |
299299
| `senderName` | `string \| null` | The sender's WhatsApp push name. |
300300
| `senderUsername` | `string \| null` | The sender's WhatsApp username (`@handle`), without the `@`. `null` unless WhatsApp sends one — mostly group messages from LID-only senders. |
301-
| `isOffline` | `boolean` | `true` for a backlog message replayed after a reconnect. Guard on this so the bot does not answer old messages twice. |
301+
| `isOld` | `boolean` | `true` for a backlog message replayed after a reconnect. Guard on this so the bot does not answer old messages twice. |
302302
| `forwardCount` | `number` | Forward hops. `>= 5` is WhatsApp's "forwarded many times". |
303303
| `ephemeralDuration` | `number \| null` | The chat's disappearing timer in seconds. |
304304
| `addressingMode` | `'pn' \| 'lid'` | How WhatsApp addressed the message. |

docs/content/message-payload.mdx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,8 @@ So plain fields are cheap to read; the function fields do real work (network/asy
5858
| `senderLid` | `string \| null` | `key.participantAlt` / `remoteJidAlt` ending in `@lid` | The sender's LID, when WhatsApp exposes one. `null` if not available. |
5959
| `senderName` | `string \| null` | `message.pushName` | Display/push name. Can be `null` or spoofed — don't use for authz. |
6060
| `senderUsername` | `string \| null` | `key.participantUsername` / `remoteJidUsername` | The sender's WhatsApp username (the `@handle`), without the `@`. `null` unless WhatsApp sends one — see the note below. |
61-
| `isOffline` | `boolean` | `messages.upsert` type `append` | **`true` for a backlog message replayed after a reconnect.** Skip these so your bot doesn't answer hours-old messages twice. |
62-
| `forwardCount` | `number` | `contextInfo.forwardingScore` | How many hops the message has been forwarded. `0` = not forwarded, `>= 5` = WhatsApp's "forwarded many times". |
63-
| `ephemeralDuration` | `number \| null` | `contextInfo.expiration` | The chat's disappearing timer **in seconds**, or `null` when messages are kept. |
64-
| `addressingMode` | `'pn' \| 'lid'` | `key.addressingMode` | Whether WhatsApp addressed this message by phone number or by LID. Useful when debugging identity resolution. |
65-
| `mentionedGroups` | `string[]` | `contextInfo.groupMentions` | JIDs of groups tagged in the message (community `@group` mentions). |
66-
| `ad` | `AdAttribution \| undefined` | `contextInfo.externalAdReply` | Present **only** on the first message of a chat opened from a Meta ad. |
67-
| `business` | `BusinessInfo \| undefined` | `verifiedBizName` | Present **only** when the sender is a verified WhatsApp Business account. |
6861
| `senderDevice` | `SenderDevice` | Parsed from the sender's device jid | Which client app sent it: `android` · `ios` · `web` · `desktop` · `unknown`. |
62+
| `addressingMode` | `'pn' \| 'lid'` | `key.addressingMode` | Whether WhatsApp addressed this message by phone number or by LID. Useful when debugging identity resolution. |
6963
| `isFromMe` | `boolean` | `key.fromMe === true` | `true` if your own account sent it (echo of your sends, other-device activity). |
7064
| `isGroup` | `boolean` | `remoteJid` is a group jid | Branch group vs DM logic. |
7165
| `isNewsletter` | `boolean` | `remoteJid` ends `@newsletter` | Message came from a channel/newsletter. |
@@ -82,7 +76,12 @@ So plain fields are cheap to read; the function fields do real work (network/asy
8276
| `text` | `string` | body / caption / poll name / location label, after unwrapping | The textual content. Empty string if none. Caption text for media is mirrored here too. |
8377
| `timestamp` | `number` | `messageTimestamp`, converted **seconds → milliseconds** | Unix **ms** epoch. Feed straight into `new Date(ctx.timestamp)`. `0` if invalid. |
8478
| `mentions` | `string[]` | `contextInfo.mentionedJid`, mapped to PN | Jids tagged in the message. |
79+
| `mentionedGroups` | `string[]` | `contextInfo.groupMentions` | Groups tagged in the message (community `@group` mentions). |
8580
| `links` | `string[]` | Regex over `text` | URLs found in the text (trailing punctuation trimmed). |
81+
| `forwardCount` | `number` | `contextInfo.forwardingScore` | How many hops the message has been forwarded. `0` = not forwarded, `>= 5` = WhatsApp's "forwarded many times". |
82+
| `ephemeralDuration` | `number \| null` | `contextInfo.expiration` | The chat's disappearing timer **in seconds**, or `null` when messages are kept. |
83+
| `ad` | `AdAttribution \| undefined` | `contextInfo.externalAdReply` | Present **only** on the first message of a chat opened from a Meta ad. See [`ad`](#ad--where-a-lead-came-from). |
84+
| `business` | `BusinessInfo \| undefined` | `verifiedBizName` | Present **only** when the sender is a verified WhatsApp Business account. |
8685

8786
---
8887

@@ -95,6 +94,7 @@ All computed from the message **except `isSpam`** (reserved, always `false` for
9594
| `isViewOnce` | view-once wrapper present, or media marked `viewOnce`. |
9695
| `isEphemeral` | ephemeral wrapper present, or `contextInfo.expiration > 0` (disappearing messages). |
9796
| `isForwarded` | marked forwarded, or `forwardingScore > 0`. |
97+
| `isOld` | the message is **backlog** — it arrived while your bot was disconnected and WhatsApp replayed it on reconnect. Guard on this so you don't answer hours-old messages. |
9898
| `isQuestion` | trimmed `text` ends with `?`. |
9999
| `isPrefix` | `text` starts with one of your configured command **prefixes**. |
100100
| `isTagMe` | your jid is in `mentions` (you were @-tagged). |
@@ -299,7 +299,7 @@ A few values come from your `Client` config rather than the raw message:
299299
- **`roomId` follows the conversation, not the direction** — safe as a reply target for both inbound and your own (`isFromMe`) messages.
300300
- **`isSpam` is reserved** and always `false` today. Use `forwardCount >= 5` for chain-message detection.
301301
- **Answer only live messages.** After a reconnect WhatsApp replays everything you missed. Guard with
302-
`if (ctx.isOffline) return` unless you deliberately want to process the backlog.
302+
`if (ctx.isOld) return` unless you deliberately want to process the backlog.
303303
- **Mirror the disappearing timer on replies.** `ctx.reply()` does not inherit it — pass
304304
`ctx.ephemeralDuration` to `.disappearing()` yourself, or your answer outlives the thread it belongs to.
305305
- **`senderUsername` is usually `null` — WhatsApp sends the phone number _or_ the username, never

src/events/context.ts

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,11 @@ export interface MediaAttachment {
4646
fileName: string | null
4747
fileSize: number | null
4848
ptt: boolean
49-
/** Playback length in seconds, for audio and video. */
49+
isAnimated: boolean
5050
duration: number | null
5151
width: number | null
5252
height: number | null
53-
/** Page count, for documents. */
5453
pages: number | null
55-
/** `true` for an animated sticker or a GIF-playback video. */
56-
isAnimated: boolean
57-
/** Inline JPEG preview, when WhatsApp sent one. Lets you show a thumbnail without downloading. */
5854
thumbnail: Buffer | null
5955
buffer(): Promise<Buffer>
6056
stream(): Promise<Readable>
@@ -248,7 +244,6 @@ export interface MessageContext {
248244
senderId: string
249245
senderLid: string | null
250246
senderName: string | null
251-
/** WhatsApp username (the `@handle`), without the `@`. Only sent for accounts that set one. */
252247
senderUsername: string | null
253248
senderDevice: SenderDevice
254249
timestamp: number
@@ -262,20 +257,7 @@ export interface MessageContext {
262257
isViewOnce: boolean
263258
isEphemeral: boolean
264259
isForwarded: boolean
265-
/** `true` for a backlog message replayed after a reconnect. Skip these to avoid answering twice. */
266-
isOffline: boolean
267-
/** How many hops this message has been forwarded. `>= 5` is WhatsApp's "forwarded many times". */
268-
forwardCount: number
269-
/** The chat's disappearing timer in seconds, or `null` when messages are kept. */
270-
ephemeralDuration: number | null
271-
/** Whether WhatsApp addressed this message by phone number or by LID. */
272-
addressingMode: 'pn' | 'lid'
273-
/** JIDs of groups tagged in the message (community `@group` mentions). */
274-
mentionedGroups: string[]
275-
/** Present only on the first message of a chat opened from a Meta ad. */
276-
ad?: AdAttribution
277-
/** Present only when the sender is a verified WhatsApp Business account. */
278-
business?: BusinessInfo
260+
isOld: boolean
279261
isQuestion: boolean
280262
isPrefix: boolean
281263
isTagMe: boolean
@@ -290,6 +272,12 @@ export interface MessageContext {
290272
isGroupStatusMention: boolean
291273
isGroupStatus: boolean
292274
isStory: boolean
275+
forwardCount: number
276+
ephemeralDuration: number | null
277+
addressingMode: 'pn' | 'lid'
278+
mentionedGroups: string[]
279+
ad?: AdAttribution
280+
business?: BusinessInfo
293281
roomName(): Promise<string | null>
294282
receiverName(): Promise<string | null>
295283
media?: ContextMedia
@@ -326,7 +314,7 @@ export interface BuildContextInput {
326314
isForwarded: boolean
327315
isBroadcast: boolean
328316
isNewsletter: boolean
329-
isOffline?: boolean
317+
isOld?: boolean
330318
forwardCount?: number
331319
ephemeralDuration?: number | null
332320
addressingMode?: 'pn' | 'lid'
@@ -510,13 +498,7 @@ export const buildMessageContext = (input: BuildContextInput): MessageContext =>
510498
isViewOnce: input.isViewOnce,
511499
isEphemeral: input.isEphemeral,
512500
isForwarded: input.isForwarded,
513-
isOffline: input.isOffline === true,
514-
forwardCount: input.forwardCount ?? 0,
515-
ephemeralDuration: input.ephemeralDuration ?? null,
516-
addressingMode: input.addressingMode ?? 'pn',
517-
mentionedGroups: input.mentionedGroups ?? [],
518-
...(input.ad !== undefined ? { ad: input.ad } : {}),
519-
...(input.business !== undefined ? { business: input.business } : {}),
501+
isOld: input.isOld === true,
520502
isQuestion: isQuestionOf(input.text),
521503
isPrefix: isPrefixOf(input.text, input.prefixes),
522504
isTagMe: isTagMeOf(input.selfJid, input.mentions),
@@ -531,6 +513,12 @@ export const buildMessageContext = (input: BuildContextInput): MessageContext =>
531513
isGroupStatusMention: flags.isGroupStatusMention,
532514
isGroupStatus: flags.isGroupStatus,
533515
isStory: remoteJid === 'status@broadcast',
516+
forwardCount: input.forwardCount ?? 0,
517+
ephemeralDuration: input.ephemeralDuration ?? null,
518+
addressingMode: input.addressingMode ?? 'pn',
519+
mentionedGroups: input.mentionedGroups ?? [],
520+
...(input.ad !== undefined ? { ad: input.ad } : {}),
521+
...(input.business !== undefined ? { business: input.business } : {}),
534522
roomName: input.resolveRoomName,
535523
receiverName: input.resolveReceiverName,
536524
replied: input.resolveReplied,

src/events/decoders/messages.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export interface DecodeContext {
3030
selfName?: string
3131
lidMap?: Map<string, string>
3232
/** WhatsApp replays a reconnect's backlog as `append`; live traffic arrives as `notify`. */
33-
isOffline?: boolean
33+
isOld?: boolean
3434
logger?: DownloadLogger
3535
channelId?: string
3636
receiverId?: string
@@ -774,14 +774,14 @@ const buildContext = (
774774
isForwarded,
775775
isBroadcast,
776776
isNewsletter,
777-
isOffline: ctx.isOffline === true,
777+
isOld: ctx.isOld === true,
778+
prefixes,
778779
forwardCount: numOr(contextInfo?.forwardingScore, 0),
779780
ephemeralDuration: ephemeralDurationOf(msg, contextInfo),
780781
addressingMode: key.addressingMode === 'lid' ? ('lid' as const) : ('pn' as const),
781782
mentionedGroups: groupMentionsOf(contextInfo),
782783
...(ad !== undefined ? { ad } : {}),
783784
...(business !== undefined ? { business } : {}),
784-
prefixes,
785785
...(ctx.lidMap != null ? { lidMap: ctx.lidMap } : {}),
786786
resolveRoomName,
787787
resolveReceiverName,
@@ -816,11 +816,11 @@ const buildMediaAttachment = (
816816
fileName: typeof node.fileName === 'string' ? node.fileName : null,
817817
fileSize: toNum(node.fileLength),
818818
ptt: node.ptt === true,
819+
isAnimated: node.isAnimated === true || node.gifPlayback === true,
819820
duration: toNum(node.seconds),
820821
width: toNum(node.width),
821822
height: toNum(node.height),
822823
pages: toNum(node.pageCount),
823-
isAnimated: node.isAnimated === true || node.gifPlayback === true,
824824
thumbnail: node.jpegThumbnail != null ? Buffer.from(node.jpegThumbnail) : null,
825825
buffer: async () => (await bufferFn()).buffer,
826826
stream: streamFn,

src/events/pipeline.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export function attachInboundPipeline(
226226

227227
subscribe('messages.upsert', (raw) => {
228228
const upsert = dropSpoofedSelfOnly(raw as UpsertPayload)
229-
const baseCtx = upsert.type === 'append' ? { ...decodeCtx, isOffline: true } : decodeCtx
229+
const baseCtx = upsert.type === 'append' ? { ...decodeCtx, isOld: true } : decodeCtx
230230
for (const msg of upsert.messages) {
231231
if (ctx.ignoreMe === true && msg.key?.fromMe === true) continue
232232
const lids = ctx.resolveLidToPn != null ? lidTargetsOf(msg) : []

tests/command/dispatcher.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const msg = (text: string): MessageContext => ({
1919
senderLid: null,
2020
senderName: null,
2121
senderUsername: null,
22-
isOffline: false,
22+
isOld: false,
2323
forwardCount: 0,
2424
ephemeralDuration: null,
2525
addressingMode: 'pn',

tests/events/payload-additions.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ const emit = (messages: Record<string, unknown>[], type = 'notify'): MessageCont
3838
return seen.mock.calls[0]?.[0] as MessageContext
3939
}
4040

41-
describe('isOffline', () => {
41+
describe('isOld', () => {
4242
it('is false for live traffic', () => {
43-
expect(emit([msg()]).isOffline).toBe(false)
43+
expect(emit([msg()]).isOld).toBe(false)
4444
})
4545

4646
it('is true for the backlog WhatsApp replays after a reconnect', () => {
47-
expect(emit([msg()], 'append').isOffline).toBe(true)
47+
expect(emit([msg()], 'append').isOld).toBe(true)
4848
})
4949
})
5050

0 commit comments

Comments
 (0)