qwen_2.5_model / src /engine /interfaces /whatsapp-engine.interface.ts
Muhammad Noman
Deploy OpenWA to Hugging Face Spaces
46252cd
Raw
History Blame Contribute Delete
25.4 kB
// 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:
// - `<phone>@c.us` a user known by phone (the raw `@s.whatsapp.net` form folds into this)
// - `<id>@g.us` a group
// - `<lid>@lid` a user known ONLY by privacy id - phone genuinely unknown (a first-class state)
// - `status@broadcast` / `<id>@newsletter` / `<id>@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 (`<phone>@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 `<id>@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 `<id>@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<void>;
disconnect(): Promise<void>; // Closes browser but keeps session (can reconnect without QR)
logout(): Promise<void>; // Logs out and clears session data (requires QR scan again)
destroy(): Promise<void>;
// 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<void>;
// 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<boolean>;
getQRCode(): string | null;
/** Request an 8-char pairing code to link via phone number instead of scanning the QR. */
requestPairingCode(phoneNumber: string): Promise<string>;
getPhoneNumber(): string | null;
getPushName(): string | null;
// Messaging - Basic
sendTextMessage(chatId: string, text: string, mentions?: string[]): Promise<MessageResult>;
sendImageMessage(chatId: string, media: MediaInput): Promise<MessageResult>;
sendVideoMessage(chatId: string, media: MediaInput): Promise<MessageResult>;
sendAudioMessage(chatId: string, media: MediaInput): Promise<MessageResult>;
sendDocumentMessage(chatId: string, media: MediaInput): Promise<MessageResult>;
// Messaging - Extended (Phase 3)
sendLocationMessage(chatId: string, location: LocationInput): Promise<MessageResult>;
sendContactMessage(chatId: string, contact: ContactCard): Promise<MessageResult>;
sendStickerMessage(chatId: string, media: MediaInput): Promise<MessageResult>;
sendPollMessage(chatId: string, poll: PollInput): Promise<MessageResult>;
// Reply & Forward
replyToMessage(chatId: string, quotedMsgId: string, text: string): Promise<MessageResult>;
forwardMessage(fromChatId: string, toChatId: string, messageId: string): Promise<MessageResult>;
// Reactions (Phase 3)
reactToMessage(chatId: string, messageId: string, emoji: string): Promise<void>;
getMessageReactions(chatId: string, messageId: string): Promise<MessageReaction[]>;
// Contacts
getContacts(): Promise<Contact[]>;
getContactById(contactId: string): Promise<Contact | null>;
checkNumberExists(number: string): Promise<boolean>;
/**
* Resolve a phone number to its canonical chat id in the neutral dialect (`<phone>@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<string | null>;
/**
* 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<string | null>;
// Groups - Basic
getGroups(): Promise<Group[]>;
// Groups - Extended (Phase 3)
getGroupInfo(groupId: string): Promise<GroupInfo | null>;
createGroup(name: string, participants: string[]): Promise<Group>;
addParticipants(groupId: string, participants: string[]): Promise<void>;
removeParticipants(groupId: string, participants: string[]): Promise<void>;
promoteParticipants(groupId: string, participants: string[]): Promise<void>;
demoteParticipants(groupId: string, participants: string[]): Promise<void>;
leaveGroup(groupId: string): Promise<void>;
setGroupSubject(groupId: string, subject: string): Promise<void>;
setGroupDescription(groupId: string, description: string): Promise<void>;
getGroupInviteCode(groupId: string): Promise<string>;
revokeGroupInviteCode(groupId: string): Promise<string>;
/**
* Join a group via an invite code (the token from a `https://chat.whatsapp.com/<code>` link).
* Resolves with the joined group's neutral id (`<id>@g.us`).
*/
joinGroupViaInviteCode(inviteCode: string): Promise<string>;
/** Set the "only admins can send messages" group setting (WhatsApp announce). */
setGroupMessagesAdminsOnly(groupId: string, adminsOnly: boolean): Promise<void>;
/** Set the "only admins can edit group info" group setting (WhatsApp locked/restrict). */
setGroupInfoAdminsOnly(groupId: string, adminsOnly: boolean): Promise<void>;
/**
* 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<void>;
// Message Operations
deleteMessage(chatId: string, messageId: string, forEveryone?: boolean): Promise<void>;
/**
* 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<MessageResult>;
getChatHistory(chatId: string, limit?: number, includeMedia?: boolean): Promise<IncomingMessage[]>;
// 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<void>;
// Contact Extended Operations
getProfilePicture(contactId: string): Promise<string | null>;
blockContact(contactId: string): Promise<void>;
unblockContact(contactId: string): Promise<void>;
// Profile (own account)
/** Set the account's display name. */
setProfileName(name: string): Promise<void>;
/** Set the account's "about" / status text. */
setProfileStatus(status: string): Promise<void>;
/** Set the account's profile picture (a URL payload is fetched server-side). */
setProfilePicture(media: MediaInput): Promise<void>;
// Labels (Phase 3) - WhatsApp Business only
getLabels(): Promise<Label[]>;
getLabelById(labelId: string): Promise<Label | null>;
getChatLabels(chatId: string): Promise<Label[]>;
addLabelToChat(chatId: string, labelId: string): Promise<void>;
removeLabelFromChat(chatId: string, labelId: string): Promise<void>;
// Channels/Newsletter (Phase 3)
getSubscribedChannels(): Promise<Channel[]>;
getChannelById(channelId: string): Promise<Channel | null>;
subscribeToChannel(inviteCode: string): Promise<Channel>;
unsubscribeFromChannel(channelId: string): Promise<void>;
getChannelMessages(channelId: string, limit?: number): Promise<ChannelMessage[]>;
// Status/Stories (Phase 3)
getContactStatuses(): Promise<Status[]>;
getContactStatus(contactId: string): Promise<Status[]>;
postTextStatus(text: string, options: StatusPostOptions): Promise<StatusResult>;
postImageStatus(media: MediaInput, options: StatusPostOptions): Promise<StatusResult>;
postVideoStatus(media: MediaInput, options: StatusPostOptions): Promise<StatusResult>;
deleteStatus(statusId: string): Promise<void>;
// Catalog (Phase 3) - WhatsApp Business only
getCatalog(): Promise<Catalog | null>;
getProducts(options?: ProductQueryOptions): Promise<PaginatedProducts>;
getProduct(productId: string): Promise<Product | null>;
sendProduct(chatId: string, productId: string, body?: string): Promise<MessageResult>;
sendCatalog(chatId: string, body?: string): Promise<MessageResult>;
// Chats
getChats(): Promise<ChatSummary[]>;
sendSeen(chatId: string): Promise<boolean>;
markUnread(chatId: string): Promise<boolean>;
deleteChat(chatId: string): Promise<boolean>;
/**
* Send a typing/recording presence indicator to a chat, or clear it (`paused`).
* Engine-agnostic and best-effort: engines without a presence concept should no-op.
*/
sendChatState(chatId: string, state: ChatState): Promise<void>;
}