// WhatsApp Engine Interface - Abstract layer for WA engines // // Identity contract (the engine boundary is an anti-corruption layer for WhatsApp's id dialects): // every JID an engine EMITS in a neutral field (`from` / `to` / `chatId` / `author` / contact + chat // `id`, etc.) is in the NEUTRAL dialect, so application code never has to know which engine produced // it. The neutral dialect is small: // - `@c.us` a user known by phone (the raw `@s.whatsapp.net` form folds into this) // - `@g.us` a group // - `@lid` a user known ONLY by privacy id - phone genuinely unknown (a first-class state) // - `status@broadcast` / `@newsletter` / `@broadcast` special channels // - never `@s.whatsapp.net`, never a `:device` suffix // Resolution rule: prefer `@c.us` (resolve a lid to its phone when the mapping is known), fall back to // `@lid` only when it can't be resolved. See `engine/identity/wa-id.ts` for the shared implementation. // (Ids the engine ACCEPTS - e.g. `sendTextMessage(chatId)` - may be neutral; the adapter de-normalizes // to its own dialect. Full inbound + outbound conformance is being rolled out per-engine.) export enum EngineStatus { DISCONNECTED = 'disconnected', INITIALIZING = 'initializing', QR_READY = 'qr_ready', AUTHENTICATING = 'authenticating', READY = 'ready', FAILED = 'failed', } export interface MessageResult { id: string; timestamp: number; } export interface MediaInput { mimetype: string; data: Buffer | string; // Buffer or base64 or URL filename?: string; caption?: string; /** Neutral WIDs (`@c.us`) to @mention in the caption. The adapter de-normalizes per engine. */ mentions?: string[]; /** When true, send as a WhatsApp voice note (PTT). audio-only; ignored by other media types. */ ptt?: boolean; } /** * Engine-neutral message type. Each adapter maps its library's native message-type tokens * (e.g. whatsapp-web.js `chat`/`ptt`/`vcard`) to this vocabulary at the adapter boundary, * so no consumer outside the adapter sees engine-specific type strings. `unknown` covers any * type the active engine reports that doesn't map to a first-class kind. */ export type MessageType = | 'text' | 'image' | 'video' | 'audio' | 'voice' | 'document' | 'sticker' | 'location' | 'contact' | 'poll' | 'call' | 'revoked' // A message WhatsApp deliberately withheld from linked/companion devices (e.g. high-security // business OTPs): the payload is absent by design, not unparseable. See `mapBaileysMessageType`. | 'masked' | 'unknown'; export interface IncomingMessage { id: string; from: string; to: string; chatId: string; body: string; type: MessageType; timestamp: number; fromMe: boolean; isGroup: boolean; /** * True for a status/story broadcast (not a real conversation). Set by the adapter so engine-neutral * code can skip these without matching an engine-specific pseudo-JID (e.g. `status@broadcast`). */ isStatusBroadcast?: boolean; /** WhatsApp ephemeral/disappearing-messages timer in seconds. Set per-chat on each message * in the raw payload. 0 or undefined = no disappearing timer. * Known values: 86400 (24h), 604800 (7d), 7776000 (90d). */ ephemeralDuration?: number; /** For group messages, the WID of the participant who actually sent it (`from` is the group JID there). */ author?: string; /** WIDs @mentioned in the message (empty/absent when none). Surfaced for command targeting. */ mentionedIds?: string[]; /** Set for `call` (call_log) messages: video vs voice, and whether an incoming call went unanswered. */ call?: { video: boolean; missed: boolean }; /** * Set by the adapter when the sender is identified by a privacy id (e.g. a WhatsApp `@lid`) rather * than a phone number, so engine-neutral code can decide whether to attempt phone resolution without * matching an engine-specific JID scheme. */ isLidSender?: boolean; /** * Best-effort phone number (MSISDN digits) of the sender, resolved from a privacy id when inline * resolution is enabled (`RESOLVE_LID_TO_PHONE`). `null` when the engine cannot map it. Only * populated for `isLidSender` messages. */ senderPhone?: string | null; /** Sender contact info, best-effort from the WhatsApp Web cache. Sync fields only (no network). */ contact?: MessageContact; media?: { mimetype: string; filename?: string; data?: string; // base64; absent when the payload was omitted (see `omitted`) /** True when the media blob was dropped due to a size cap, timeout, or concurrency saturation. */ omitted?: boolean; /** Decoded byte size of the media; always set when `omitted` is true. */ sizeBytes?: number; }; quotedMessage?: { id: string; body: string; }; location?: { latitude: number; longitude: number; description?: string; address?: string; url?: string; }; } /** * Synchronous (already-resolved, no network call) fields of a sender contact, surfaced on * {@link IncomingMessage}. Async getters (profile pic / about / formatted number) are intentionally * NOT included — they hit WhatsApp servers per message and risk rate-limit/ban. All optional; a key * is present only when the engine populated it. */ export interface MessageContact { /** Sender JID (`…@c.us` or a `…@lid` privacy id). */ id?: string; /** Phone digits, best-effort. For `@lid` senders the authoritative number is `IncomingMessage.senderPhone`. */ number?: string; name?: string; pushName?: string; shortName?: string; /** whatsapp-web.js contact type token. */ type?: string; /** Saved in the account's address book. */ isMyContact?: boolean; /** Is a WhatsApp user. */ isWAContact?: boolean; isBusiness?: boolean; isEnterprise?: boolean; /** Business verified name. */ verifiedName?: string; /** Business verification level. */ verifiedLevel?: number; isBlocked?: boolean; /** Label IDs (CRM). Names are not resolved — that would need a network call. */ labels?: string[]; } export interface Contact { id: string; name?: string; pushName?: string; number: string; isMyContact: boolean; isBlocked: boolean; profilePicUrl?: string; } export interface Group { id: string; name: string; participantsCount?: number; isAdmin?: boolean; /** JID of the parent community this group is linked to, or null if standalone. */ linkedParentJID?: string | null; } export interface GroupParticipant { id: string; number: string; name?: string; isAdmin: boolean; isSuperAdmin: boolean; } export interface GroupInfo { id: string; name: string; description?: string; owner?: string; createdAt?: number; participants: GroupParticipant[]; isReadOnly?: boolean; isAnnounce?: boolean; /** Only admins can send messages (the WhatsApp "announce" group setting). */ announce?: boolean; /** Only admins can edit group info — subject, description, picture (the WhatsApp "locked"/restrict setting). */ locked?: boolean; /** Disappearing-messages timer in seconds; 0 or undefined = off. */ ephemeralSeconds?: number; /** JID of the parent community this group is linked to, or null if standalone. */ linkedParentJID?: string | null; } export interface ContactCard { name: string; number: string; } export interface LocationInput { latitude: number; longitude: number; description?: string; address?: string; } export interface PollInput { /** Poll question / title. */ name: string; /** Options to vote on (WhatsApp accepts between 2 and 12). */ options: string[]; /** When true a voter can pick several options; default is single choice. */ allowMultipleAnswers?: boolean; } export interface ReactionSender { senderId: string; emoji: string; timestamp: number; } export interface MessageReaction { emoji: string; senders: ReactionSender[]; } // Phase 3: Labels (WhatsApp Business) export interface Label { id: string; name: string; hexColor: string; } // Phase 3: Status/Stories export interface Status { id: string; contact: { id: string; name?: string; pushName?: string; }; type: 'text' | 'image' | 'video'; caption?: string; mediaUrl?: string; backgroundColor?: string; font?: number; timestamp: Date; expiresAt: Date; } export interface StatusPostOptions { /** REQUIRED. Neutral JIDs (@c.us / @lid) permitted to see the status. Maps to Baileys statusJidList. */ recipients: string[]; /** Hex background colour (#RRGGBB). Text status only. */ backgroundColor?: string; /** Font index. Text status only. */ font?: number; /** Caption. Image/video status only. */ caption?: string; } export interface StatusResult { statusId: string; timestamp: Date; expiresAt: Date; } // Phase 3: Channels/Newsletter export interface Channel { id: string; name: string; description?: string; inviteCode?: string; subscriberCount?: number; picture?: string; verified?: boolean; createdAt?: number; } export interface ChannelMessage { id: string; body: string; timestamp: number; hasMedia: boolean; mediaUrl?: string; } // Phase 3: Catalog (WhatsApp Business) export interface Catalog { id: string; name: string; description?: string; productCount: number; url: string; } export interface Product { id: string; name: string; description?: string; price: number; currency: string; priceFormatted: string; imageUrl?: string; url: string; isAvailable: boolean; retailerId?: string; } export interface ProductQueryOptions { page?: number; limit?: number; } export interface PaginatedProducts { products: Product[]; pagination: { page: number; limit: number; total: number; totalPages: number; }; } /** * Lightweight summary of a chat, exposed to the dashboard's real-time chats view. * Only library-agnostic primitives are leaked here; raw whatsapp-web.js objects are * mapped to this shape inside the adapter. */ export interface ChatSummary { id: string; name: string; isGroup: boolean; unreadCount: number; timestamp: number; lastMessage?: string; } /** * Engine-neutral chat presence state. `typing`/`recording` show the indicator to the chat; * `paused` clears it. Best-effort: engines without a presence concept may no-op. */ export type ChatState = 'typing' | 'recording' | 'paused'; /** * Engine-neutral message delivery status. Each adapter maps its native delivery signal * (e.g. whatsapp-web.js MessageAck integers, Baileys WAMessageStatus) to this vocabulary, * so no consumer outside the adapter sees engine-specific ack codes. */ export type DeliveryStatus = 'pending' | 'sent' | 'delivered' | 'read' | 'failed'; /** * Structured payload for a remotely-revoked ("deleted for everyone") message. * The engine layer never emits a localized display string; `body` is intentionally * empty and the dashboard renders the localized "message deleted" text. */ export interface RevokedMessage { id: string; /** * Serialized id of the ORIGINAL message that was deleted (when available). * * This is the reliable cross-engine field for reconciling the deleted message in * your own storage — both adapters populate it with the original message id: * - whatsapp-web.js: `id` is the revocation NOTIFICATION (a distinct message), so * `id !== revokedId`. `revokedId` may be undefined when the original is not in * the local store. * - Baileys: the revoke arrives as a protocolMessage whose key already points at * the original, so `id === revokedId`. * * Consumers should match on `revokedId` (falling back to `id`) rather than `id`. */ revokedId?: string; chatId: string; from: string; to: string; type: 'revoked'; body: ''; timestamp: number; } export interface EditedMessage { messageId: string; chatId: string; body: string; senderId: string; from: string; to: string; fromMe: boolean; isGroup: boolean; type: MessageType; hasMedia: boolean; /** For group messages, the participant that authored the edited message. */ author?: string; /** WIDs mentioned by the edited message's latest content. */ mentionedIds?: string[]; /** Unix seconds when the edit occurred (not the original message creation time). */ timestamp: number; } export interface ReactionEvent { messageId: string; chatId: string; reaction: string; senderId: string; } /** * A group membership or metadata change, mapped at the adapter boundary to this neutral * shape so consumers never see engine-specific payloads: * - whatsapp-web.js: `group_join` / `group_leave` / `group_update` (GroupNotification). * - Baileys: `group-participants.update` (add/remove only — promote/demote are not * surfaced) and `groups.update` (subject/desc/announce/restrict). * All ids are in the neutral dialect (`@g.us` / `@c.us`; a lid stays `@lid` when the * lid->phone mapping is unknown). */ export interface GroupEvent { kind: 'join' | 'leave' | 'update'; /** Neutral group id (`@g.us`). */ groupId: string; /** Who performed the action, neutral user id when the engine reports one. */ actorId?: string; /** Affected users (join/leave), neutral ids. Empty for metadata updates. */ participantIds: string[]; /** Metadata delta for kind 'update'; absent or partially populated for join/leave. */ changes?: { subject?: string; description?: string; announce?: boolean; locked?: boolean }; /** Unix seconds when the change occurred (engine timestamp when available, else receipt time). */ timestamp: number; } /** * An incoming (ringing) call, mapped at the adapter boundary to this neutral shape: * - whatsapp-web.js: the client `call` event (a `Call` object the adapter caches so a later * `rejectCall` can act on it — the object is only usable while the call is live). * - Baileys: the `call` event's `offer` status entries (other statuses are lifecycle updates and * are not surfaced). * All ids are in the neutral dialect (`@c.us`; a lid caller stays `@lid` when the lid->phone * mapping is unknown, resolved via the inline phone twin when the engine provides one). */ export interface IncomingCallEvent { /** Engine call id; the handle `rejectCall` accepts while the call is still ringing. */ callId: string; /** Neutral caller id. */ from: string; isVideo: boolean; isGroup: boolean; /** Unix seconds when the call was created (engine timestamp). */ timestamp: number; } export interface EngineEventCallbacks { onQRCode?: (qr: string) => void; onReady?: (phone: string, pushName: string) => void; onMessage?: (message: IncomingMessage) => void; /** * Fired for messages the account itself created (outgoing) — including sends composed on a * linked phone, which the `message`/`onMessage` event never delivers. Used to emit `message.sent`. */ onMessageCreate?: (message: IncomingMessage) => void; /** * Fired when the delivery status of an outgoing message advances. The adapter maps its native * delivery signal to the neutral `DeliveryStatus`, so consumers never see engine-specific codes. */ onMessageAck?: (messageId: string, status: DeliveryStatus) => void; onMessageRevoked?: (message: RevokedMessage) => void; onMessageReaction?: (event: ReactionEvent) => void; onMessageEdited?: (message: EditedMessage) => void; /** * Fired on group membership changes (join/leave) and group metadata updates * (subject/description/announce/locked). The `kind` selects the consumer event name * (`group.join` / `group.leave` / `group.update`). */ onGroupEvent?: (event: GroupEvent) => void; /** * Fired when an incoming call starts ringing (consumers emit `call.received`). The call can be * rejected via `rejectCall(callId)` only while it is still ringing — the adapter keeps the * engine's live call handle cached for that window. */ onCall?: (event: IncomingCallEvent) => void; /** * Bulk historical messages from an engine's initial sync (e.g. Baileys `messaging-history.set`). * They predate the live session, so consumers persist them for the chat view but must not dispatch. */ onHistoryMessages?: (messages: IncomingMessage[]) => void; onDisconnected?: (reason: string) => void; onStateChanged?: (state: EngineStatus) => void; /** * Fired on a terminal initialization/authentication failure (e.g. Chromium * could not launch, or WhatsApp rejected the stored credentials). The engine * has already moved to FAILED; `reason` carries a human-readable cause that * callers may surface to operators. Distinct from `onDisconnected`, which is * recoverable and triggers reconnection. */ onError?: (reason: string) => void; } export interface IWhatsAppEngine { // Lifecycle initialize(callbacks: EngineEventCallbacks): Promise; disconnect(): Promise; // Closes browser but keeps session (can reconnect without QR) logout(): Promise; // Logs out and clears session data (requires QR scan again) destroy(): Promise; // Force-kill THIS engine's own resources immediately (e.g. SIGKILL a wedged Chromium for a stuck // session), then best-effort graceful teardown — used to recover a session that destroy() can't. // Each adapter kills only its own resources (never a process-wide pkill). forceDestroy(): Promise; // Status getStatus(): EngineStatus; /** * Active liveness probe: performs a real round-trip against the engine's connection and * resolves true only when the session is genuinely alive. Implementations must treat probe * failure/timeout as "dead" — a wedged connection can keep reporting READY (cached status), * so getStatus() alone is not proof of life. Optional: engines whose transport already * self-detects death (e.g. Baileys keepalive emits a close event within ~35s) may return a * cheap local check. Polled periodically by the session watchdog. */ probeLiveness?(): Promise; getQRCode(): string | null; /** Request an 8-char pairing code to link via phone number instead of scanning the QR. */ requestPairingCode(phoneNumber: string): Promise; getPhoneNumber(): string | null; getPushName(): string | null; // Messaging - Basic sendTextMessage(chatId: string, text: string, mentions?: string[]): Promise; sendImageMessage(chatId: string, media: MediaInput): Promise; sendVideoMessage(chatId: string, media: MediaInput): Promise; sendAudioMessage(chatId: string, media: MediaInput): Promise; sendDocumentMessage(chatId: string, media: MediaInput): Promise; // Messaging - Extended (Phase 3) sendLocationMessage(chatId: string, location: LocationInput): Promise; sendContactMessage(chatId: string, contact: ContactCard): Promise; sendStickerMessage(chatId: string, media: MediaInput): Promise; sendPollMessage(chatId: string, poll: PollInput): Promise; // Reply & Forward replyToMessage(chatId: string, quotedMsgId: string, text: string): Promise; forwardMessage(fromChatId: string, toChatId: string, messageId: string): Promise; // Reactions (Phase 3) reactToMessage(chatId: string, messageId: string, emoji: string): Promise; getMessageReactions(chatId: string, messageId: string): Promise; // Contacts getContacts(): Promise; getContactById(contactId: string): Promise; checkNumberExists(number: string): Promise; /** * Resolve a phone number to its canonical chat id in the neutral dialect (`@c.us`), or null * if the number is not registered. The engine owns the JID scheme and returns it already neutralized, * so the value is engine-agnostic and round-trips back to a send on any engine. */ getNumberId(number: string): Promise; /** * Best-effort resolution of a contact id to a phone number (MSISDN digits), or `null` when the * engine cannot map it (e.g. a privacy `@lid` the account has never seen). The contact id is the * engine's native scheme; the adapter decides how to resolve it. */ resolveContactPhone(contactId: string): Promise; // Groups - Basic getGroups(): Promise; // Groups - Extended (Phase 3) getGroupInfo(groupId: string): Promise; createGroup(name: string, participants: string[]): Promise; addParticipants(groupId: string, participants: string[]): Promise; removeParticipants(groupId: string, participants: string[]): Promise; promoteParticipants(groupId: string, participants: string[]): Promise; demoteParticipants(groupId: string, participants: string[]): Promise; leaveGroup(groupId: string): Promise; setGroupSubject(groupId: string, subject: string): Promise; setGroupDescription(groupId: string, description: string): Promise; getGroupInviteCode(groupId: string): Promise; revokeGroupInviteCode(groupId: string): Promise; /** * Join a group via an invite code (the token from a `https://chat.whatsapp.com/` link). * Resolves with the joined group's neutral id (`@g.us`). */ joinGroupViaInviteCode(inviteCode: string): Promise; /** Set the "only admins can send messages" group setting (WhatsApp announce). */ setGroupMessagesAdminsOnly(groupId: string, adminsOnly: boolean): Promise; /** Set the "only admins can edit group info" group setting (WhatsApp locked/restrict). */ setGroupInfoAdminsOnly(groupId: string, adminsOnly: boolean): Promise; /** * Set the group's disappearing-messages timer in seconds; 0 disables it. * Known values: 86400 (24h), 604800 (7d), 7776000 (90d). */ setGroupEphemeral(groupId: string, durationSec: number): Promise; // Message Operations deleteMessage(chatId: string, messageId: string, forEveryone?: boolean): Promise; /** * Edit the body of a text message. Only the account's OWN messages can be edited; engines reject * non-text or foreign messages at their own layer — the engine's error is surfaced as-is. */ editMessage(chatId: string, messageId: string, body: string): Promise; getChatHistory(chatId: string, limit?: number, includeMedia?: boolean): Promise; // Calls /** * Reject an incoming call. Only a currently-ringing call can be rejected: the adapter keeps the * engine's live call handle (keyed by the `callId` from {@link IncomingCallEvent}) for the * ringing window, and an unknown or expired callId fails with a not-found error (HTTP 404). */ rejectCall(callId: string): Promise; // Contact Extended Operations getProfilePicture(contactId: string): Promise; blockContact(contactId: string): Promise; unblockContact(contactId: string): Promise; // Profile (own account) /** Set the account's display name. */ setProfileName(name: string): Promise; /** Set the account's "about" / status text. */ setProfileStatus(status: string): Promise; /** Set the account's profile picture (a URL payload is fetched server-side). */ setProfilePicture(media: MediaInput): Promise; // Labels (Phase 3) - WhatsApp Business only getLabels(): Promise; getLabelById(labelId: string): Promise