@gakr-gakr/whatsapp 0.1.0

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.
Files changed (159) hide show
  1. package/action-runtime-api.ts +1 -0
  2. package/action-runtime.runtime.ts +1 -0
  3. package/api.ts +67 -0
  4. package/auth-presence.ts +80 -0
  5. package/autobot.plugin.json +23 -0
  6. package/channel-config-api.ts +1 -0
  7. package/channel-plugin-api.ts +3 -0
  8. package/config-api.ts +4 -0
  9. package/constants.ts +1 -0
  10. package/contract-api.ts +29 -0
  11. package/directory-contract-api.ts +4 -0
  12. package/doctor-contract-api.ts +8 -0
  13. package/index.ts +16 -0
  14. package/legacy-session-surface-api.ts +6 -0
  15. package/legacy-state-migrations-api.ts +1 -0
  16. package/light-runtime-api.ts +12 -0
  17. package/login-qr-api.ts +1 -0
  18. package/login-qr-runtime.ts +23 -0
  19. package/outbound-payload-test-api.ts +1 -0
  20. package/package.json +76 -0
  21. package/runtime-api.ts +84 -0
  22. package/secret-contract-api.ts +4 -0
  23. package/security-contract-api.ts +4 -0
  24. package/setup-entry.ts +21 -0
  25. package/setup-plugin-api.ts +3 -0
  26. package/src/account-config.ts +77 -0
  27. package/src/account-ids.ts +17 -0
  28. package/src/account-types.ts +5 -0
  29. package/src/accounts.ts +176 -0
  30. package/src/action-runtime-target-auth.ts +27 -0
  31. package/src/action-runtime.ts +76 -0
  32. package/src/active-listener.ts +17 -0
  33. package/src/agent-tools-login.ts +113 -0
  34. package/src/approval-auth.ts +27 -0
  35. package/src/auth-store.runtime.ts +1 -0
  36. package/src/auth-store.ts +494 -0
  37. package/src/auto-reply/config.runtime.ts +16 -0
  38. package/src/auto-reply/constants.ts +1 -0
  39. package/src/auto-reply/deliver-reply.ts +332 -0
  40. package/src/auto-reply/loggers.ts +6 -0
  41. package/src/auto-reply/mentions.ts +131 -0
  42. package/src/auto-reply/monitor/ack-reaction.ts +99 -0
  43. package/src/auto-reply/monitor/audio-preflight.runtime.ts +9 -0
  44. package/src/auto-reply/monitor/broadcast.ts +153 -0
  45. package/src/auto-reply/monitor/commands.ts +19 -0
  46. package/src/auto-reply/monitor/echo.ts +64 -0
  47. package/src/auto-reply/monitor/group-activation.runtime.ts +1 -0
  48. package/src/auto-reply/monitor/group-activation.ts +73 -0
  49. package/src/auto-reply/monitor/group-gating.runtime.ts +8 -0
  50. package/src/auto-reply/monitor/group-gating.ts +218 -0
  51. package/src/auto-reply/monitor/group-members.ts +65 -0
  52. package/src/auto-reply/monitor/inbound-context.ts +92 -0
  53. package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +22 -0
  54. package/src/auto-reply/monitor/inbound-dispatch.ts +749 -0
  55. package/src/auto-reply/monitor/last-route.ts +61 -0
  56. package/src/auto-reply/monitor/listener-log.ts +28 -0
  57. package/src/auto-reply/monitor/message-line.runtime.ts +38 -0
  58. package/src/auto-reply/monitor/message-line.ts +54 -0
  59. package/src/auto-reply/monitor/on-message.ts +333 -0
  60. package/src/auto-reply/monitor/peer.ts +17 -0
  61. package/src/auto-reply/monitor/process-message.ts +584 -0
  62. package/src/auto-reply/monitor/runtime-api.ts +36 -0
  63. package/src/auto-reply/monitor/status-reaction.ts +108 -0
  64. package/src/auto-reply/monitor-state.ts +114 -0
  65. package/src/auto-reply/monitor.ts +720 -0
  66. package/src/auto-reply/reply-resolver.runtime.ts +1 -0
  67. package/src/auto-reply/types.ts +48 -0
  68. package/src/auto-reply/util.ts +62 -0
  69. package/src/auto-reply.impl.ts +6 -0
  70. package/src/auto-reply.ts +1 -0
  71. package/src/channel-actions.runtime.ts +7 -0
  72. package/src/channel-actions.ts +85 -0
  73. package/src/channel-outbound.ts +87 -0
  74. package/src/channel-react-action.runtime.ts +10 -0
  75. package/src/channel-react-action.ts +247 -0
  76. package/src/channel.runtime.ts +117 -0
  77. package/src/channel.setup.ts +32 -0
  78. package/src/channel.ts +356 -0
  79. package/src/command-policy.ts +7 -0
  80. package/src/config-accessors.ts +22 -0
  81. package/src/config-schema.ts +6 -0
  82. package/src/config-ui-hints.ts +24 -0
  83. package/src/connection-controller-registry.ts +49 -0
  84. package/src/connection-controller.ts +680 -0
  85. package/src/creds-files.ts +19 -0
  86. package/src/creds-persistence.ts +71 -0
  87. package/src/directory-config.ts +40 -0
  88. package/src/doctor-contract.ts +11 -0
  89. package/src/doctor.ts +56 -0
  90. package/src/document-filename.ts +17 -0
  91. package/src/group-intro.ts +15 -0
  92. package/src/group-policy.ts +40 -0
  93. package/src/group-session-contract.ts +20 -0
  94. package/src/group-session-key.ts +42 -0
  95. package/src/heartbeat.ts +34 -0
  96. package/src/identity.ts +164 -0
  97. package/src/inbound/access-control.ts +187 -0
  98. package/src/inbound/dedupe.ts +132 -0
  99. package/src/inbound/extract.ts +484 -0
  100. package/src/inbound/lifecycle.ts +39 -0
  101. package/src/inbound/media.ts +128 -0
  102. package/src/inbound/monitor.ts +1042 -0
  103. package/src/inbound/outbound-mentions.ts +260 -0
  104. package/src/inbound/runtime-api.ts +7 -0
  105. package/src/inbound/save-media.runtime.ts +1 -0
  106. package/src/inbound/send-api.ts +203 -0
  107. package/src/inbound/send-result.ts +109 -0
  108. package/src/inbound/types.ts +107 -0
  109. package/src/inbound-policy.ts +215 -0
  110. package/src/inbound.ts +9 -0
  111. package/src/login-qr.ts +542 -0
  112. package/src/login.ts +83 -0
  113. package/src/media.ts +10 -0
  114. package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +417 -0
  115. package/src/monitor-inbox.append-upsert.test-support.ts +133 -0
  116. package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +418 -0
  117. package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +308 -0
  118. package/src/monitor-inbox.streams-inbound-messages.test-support.ts +824 -0
  119. package/src/normalize-target.ts +148 -0
  120. package/src/normalize.ts +8 -0
  121. package/src/outbound-adapter.ts +36 -0
  122. package/src/outbound-base.ts +256 -0
  123. package/src/outbound-media-contract.ts +307 -0
  124. package/src/outbound-media.runtime.ts +41 -0
  125. package/src/outbound-send-deps.ts +1 -0
  126. package/src/outbound-test-support.ts +16 -0
  127. package/src/qa-driver.runtime.ts +189 -0
  128. package/src/qr-image.ts +1 -0
  129. package/src/qr-terminal.ts +1 -0
  130. package/src/quoted-message.ts +184 -0
  131. package/src/reaction-level.ts +24 -0
  132. package/src/reconnect.ts +55 -0
  133. package/src/resolve-outbound-target.ts +58 -0
  134. package/src/runtime-api.ts +59 -0
  135. package/src/runtime-group-policy.ts +16 -0
  136. package/src/runtime.ts +9 -0
  137. package/src/security-contract.ts +47 -0
  138. package/src/security-fix.ts +71 -0
  139. package/src/send.ts +342 -0
  140. package/src/session-contract.ts +43 -0
  141. package/src/session-errors.ts +125 -0
  142. package/src/session-route.ts +32 -0
  143. package/src/session.runtime.ts +8 -0
  144. package/src/session.ts +327 -0
  145. package/src/setup-core.ts +52 -0
  146. package/src/setup-finalize.ts +450 -0
  147. package/src/setup-surface.ts +71 -0
  148. package/src/setup-test-helpers.ts +217 -0
  149. package/src/shared.ts +291 -0
  150. package/src/socket-timing.ts +38 -0
  151. package/src/state-migrations.ts +55 -0
  152. package/src/status-issues.ts +185 -0
  153. package/src/system-prompt.ts +31 -0
  154. package/src/targets-runtime.ts +221 -0
  155. package/src/text-runtime.ts +18 -0
  156. package/src/vcard.ts +84 -0
  157. package/targets.ts +5 -0
  158. package/test-api.ts +2 -0
  159. package/tsconfig.json +16 -0
@@ -0,0 +1,260 @@
1
+ import type { AnyMessageContent } from "baileys";
2
+
3
+ export type WhatsAppOutboundMentionParticipant =
4
+ | string
5
+ | {
6
+ id?: string | null;
7
+ lid?: string | null;
8
+ phoneNumber?: string | null;
9
+ e164?: string | null;
10
+ };
11
+
12
+ export type WhatsAppOutboundMentionResolution = {
13
+ text: string;
14
+ mentionedJids: string[];
15
+ };
16
+
17
+ const CODE_FENCE_RE = /```[\s\S]*?```/g;
18
+ const INLINE_CODE_RE = /`[^`\n]+`/g;
19
+ const OUTBOUND_MENTION_RE = /@(\+?\d+)/g;
20
+ const KNOWN_USER_JID_RE = /^(\d+)(?::\d+)?@(s\.whatsapp\.net|hosted|lid|hosted\.lid|c\.us)$/i;
21
+ const PHONE_JID_DOMAIN_RE = /^(s\.whatsapp\.net|hosted|c\.us)$/i;
22
+ const LID_JID_DOMAIN_RE = /^(lid|hosted\.lid)$/i;
23
+
24
+ type TextRange = {
25
+ start: number;
26
+ end: number;
27
+ };
28
+
29
+ type MentionTarget = {
30
+ mentionJid: string;
31
+ replacementText?: string;
32
+ };
33
+
34
+ function isWhatsAppGroupJid(jid: string): boolean {
35
+ return jid.endsWith("@g.us");
36
+ }
37
+
38
+ export function mayContainWhatsAppOutboundMention(text: string): boolean {
39
+ return /@\+?\d/.test(text);
40
+ }
41
+
42
+ function collectCodeRanges(text: string): TextRange[] {
43
+ const ranges: TextRange[] = [];
44
+ for (const match of text.matchAll(CODE_FENCE_RE)) {
45
+ ranges.push({ start: match.index, end: match.index + match[0].length });
46
+ }
47
+ for (const match of text.matchAll(INLINE_CODE_RE)) {
48
+ const start = match.index;
49
+ if (ranges.some((range) => start >= range.start && start < range.end)) {
50
+ continue;
51
+ }
52
+ ranges.push({ start, end: start + match[0].length });
53
+ }
54
+ return ranges.toSorted((a, b) => a.start - b.start);
55
+ }
56
+
57
+ function isInRange(index: number, ranges: readonly TextRange[]): boolean {
58
+ return ranges.some((range) => index >= range.start && index < range.end);
59
+ }
60
+
61
+ function normalizeKnownUserJid(value: string): string | null {
62
+ const trimmed = value.replace(/^whatsapp:/i, "").trim();
63
+ const jidMatch = trimmed.match(KNOWN_USER_JID_RE);
64
+ if (jidMatch) {
65
+ const domain =
66
+ jidMatch[2].toLowerCase() === "c.us" ? "s.whatsapp.net" : jidMatch[2].toLowerCase();
67
+ return `${jidMatch[1]}@${domain}`;
68
+ }
69
+ const digits = trimmed.startsWith("+")
70
+ ? trimmed.replace(/\D/g, "")
71
+ : /^\d+$/.test(trimmed)
72
+ ? trimmed
73
+ : "";
74
+ return digits ? `${digits}@s.whatsapp.net` : null;
75
+ }
76
+
77
+ function extractKnownJidParts(value: string): { user: string; domain: string } | null {
78
+ const normalized = normalizeKnownUserJid(value);
79
+ if (!normalized) {
80
+ return null;
81
+ }
82
+ const match = normalized.match(/^(\d+)@(.+)$/);
83
+ return match ? { user: match[1], domain: match[2] } : null;
84
+ }
85
+
86
+ function extractPhoneDigits(value: string | null | undefined): string | null {
87
+ if (!value) {
88
+ return null;
89
+ }
90
+ const trimmed = value.replace(/^whatsapp:/i, "").trim();
91
+ if (trimmed.startsWith("+") || /^\d+$/.test(trimmed)) {
92
+ const digits = trimmed.replace(/\D/g, "");
93
+ return digits || null;
94
+ }
95
+ const parts = extractKnownJidParts(trimmed);
96
+ return parts && PHONE_JID_DOMAIN_RE.test(parts.domain) ? parts.user : null;
97
+ }
98
+
99
+ function extractLidDigits(value: string | null | undefined): string | null {
100
+ if (!value) {
101
+ return null;
102
+ }
103
+ const parts = extractKnownJidParts(value);
104
+ return parts && LID_JID_DOMAIN_RE.test(parts.domain) ? parts.user : null;
105
+ }
106
+
107
+ function isLidJid(jid: string): boolean {
108
+ const parts = extractKnownJidParts(jid);
109
+ return Boolean(parts && LID_JID_DOMAIN_RE.test(parts.domain));
110
+ }
111
+
112
+ function lidReplacementText(jid: string): string | undefined {
113
+ const parts = extractKnownJidParts(jid);
114
+ if (!parts || !LID_JID_DOMAIN_RE.test(parts.domain)) {
115
+ return undefined;
116
+ }
117
+ return `@${parts.user}`;
118
+ }
119
+
120
+ function participantValues(participant: WhatsAppOutboundMentionParticipant): {
121
+ id?: string | null;
122
+ lid?: string | null;
123
+ phoneNumber?: string | null;
124
+ e164?: string | null;
125
+ } {
126
+ return typeof participant === "string" ? { id: participant } : participant;
127
+ }
128
+
129
+ function chooseMentionJid(participant: WhatsAppOutboundMentionParticipant): string | null {
130
+ const values = participantValues(participant);
131
+ const idJid = normalizeKnownUserJid(values.id ?? "");
132
+ const lidJid = normalizeKnownUserJid(values.lid ?? "");
133
+ return (
134
+ (idJid && isLidJid(idJid) ? idJid : null) ??
135
+ (lidJid && isLidJid(lidJid) ? lidJid : null) ??
136
+ idJid ??
137
+ lidJid ??
138
+ normalizeKnownUserJid(values.phoneNumber ?? "") ??
139
+ normalizeKnownUserJid(values.e164 ?? "")
140
+ );
141
+ }
142
+
143
+ function buildMentionTargetMaps(participants: readonly WhatsAppOutboundMentionParticipant[]): {
144
+ byPhone: Map<string, MentionTarget>;
145
+ byLid: Map<string, MentionTarget>;
146
+ } {
147
+ const byPhone = new Map<string, MentionTarget>();
148
+ const byLid = new Map<string, MentionTarget>();
149
+ for (const participant of participants) {
150
+ const mentionJid = chooseMentionJid(participant);
151
+ if (!mentionJid) {
152
+ continue;
153
+ }
154
+ const target = {
155
+ mentionJid,
156
+ ...(isLidJid(mentionJid) ? { replacementText: lidReplacementText(mentionJid) } : {}),
157
+ };
158
+ const values = participantValues(participant);
159
+ for (const value of [values.id, values.phoneNumber, values.e164]) {
160
+ const digits = extractPhoneDigits(value);
161
+ if (digits && !byPhone.has(digits)) {
162
+ byPhone.set(digits, target);
163
+ }
164
+ }
165
+ for (const value of [values.id, values.lid]) {
166
+ const digits = extractLidDigits(value);
167
+ if (digits && !byLid.has(digits)) {
168
+ byLid.set(digits, target);
169
+ }
170
+ }
171
+ }
172
+ return { byPhone, byLid };
173
+ }
174
+
175
+ function shouldSkipMentionAt(
176
+ text: string,
177
+ index: number,
178
+ end: number,
179
+ codeRanges: readonly TextRange[],
180
+ ): boolean {
181
+ if (isInRange(index, codeRanges)) {
182
+ return true;
183
+ }
184
+ const previous = index > 0 ? text[index - 1] : "";
185
+ const next = text[end] ?? "";
186
+ return Boolean((previous && /[\w@]/.test(previous)) || (next && /[\w@]/.test(next)));
187
+ }
188
+
189
+ export function resolveWhatsAppOutboundMentions(params: {
190
+ chatJid: string;
191
+ text: string;
192
+ participants?: readonly WhatsAppOutboundMentionParticipant[];
193
+ }): WhatsAppOutboundMentionResolution {
194
+ if (
195
+ !isWhatsAppGroupJid(params.chatJid) ||
196
+ !mayContainWhatsAppOutboundMention(params.text) ||
197
+ !params.participants?.length
198
+ ) {
199
+ return { text: params.text, mentionedJids: [] };
200
+ }
201
+
202
+ const { byPhone, byLid } = buildMentionTargetMaps(params.participants);
203
+ if (byPhone.size === 0 && byLid.size === 0) {
204
+ return { text: params.text, mentionedJids: [] };
205
+ }
206
+
207
+ const codeRanges = collectCodeRanges(params.text);
208
+ const replacements: Array<{ start: number; end: number; text: string }> = [];
209
+ const mentionedJids: string[] = [];
210
+ const seenMentionJids = new Set<string>();
211
+
212
+ for (const match of params.text.matchAll(OUTBOUND_MENTION_RE)) {
213
+ const start = match.index;
214
+ const token = match[0];
215
+ if (shouldSkipMentionAt(params.text, start, start + token.length, codeRanges)) {
216
+ continue;
217
+ }
218
+ const digits = match[1].replace(/\D/g, "");
219
+ const target = token.startsWith("@+")
220
+ ? (byPhone.get(digits) ?? byLid.get(digits))
221
+ : (byLid.get(digits) ?? byPhone.get(digits));
222
+ if (!target) {
223
+ continue;
224
+ }
225
+ if (!seenMentionJids.has(target.mentionJid)) {
226
+ seenMentionJids.add(target.mentionJid);
227
+ mentionedJids.push(target.mentionJid);
228
+ }
229
+ if (target.replacementText && target.replacementText !== token) {
230
+ replacements.push({
231
+ start,
232
+ end: start + token.length,
233
+ text: target.replacementText,
234
+ });
235
+ }
236
+ }
237
+
238
+ if (replacements.length === 0) {
239
+ return { text: params.text, mentionedJids };
240
+ }
241
+
242
+ let text = "";
243
+ let cursor = 0;
244
+ for (const replacement of replacements) {
245
+ text += params.text.slice(cursor, replacement.start);
246
+ text += replacement.text;
247
+ cursor = replacement.end;
248
+ }
249
+ text += params.text.slice(cursor);
250
+ return { text, mentionedJids };
251
+ }
252
+
253
+ export function addWhatsAppOutboundMentionsToContent(
254
+ content: AnyMessageContent,
255
+ mentionedJids: readonly string[],
256
+ ): AnyMessageContent {
257
+ return mentionedJids.length > 0
258
+ ? ({ ...content, mentions: [...mentionedJids] } as AnyMessageContent)
259
+ : content;
260
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ DisconnectReason,
3
+ downloadMediaMessage,
4
+ isJidGroup,
5
+ normalizeMessageContent,
6
+ } from "baileys";
7
+ export { saveMediaBuffer } from "./save-media.runtime.js";
@@ -0,0 +1 @@
1
+ export { saveMediaBuffer } from "autobot/plugin-sdk/media-store";
@@ -0,0 +1,203 @@
1
+ import type {
2
+ AnyMessageContent,
3
+ MiscMessageGenerationOptions,
4
+ WAMessage,
5
+ WAPresence,
6
+ } from "baileys";
7
+ import { recordChannelActivity } from "autobot/plugin-sdk/channel-activity-runtime";
8
+ import { resolveWhatsAppDocumentFileName } from "../document-filename.js";
9
+ import { isWhatsAppNewsletterJid } from "../normalize.js";
10
+ import { buildQuotedMessageOptions } from "../quoted-message.js";
11
+ import { toWhatsappJid, toWhatsappJidWithLid } from "../text-runtime.js";
12
+ import {
13
+ addWhatsAppOutboundMentionsToContent,
14
+ type WhatsAppOutboundMentionResolution,
15
+ } from "./outbound-mentions.js";
16
+ import {
17
+ combineWhatsAppSendResults,
18
+ normalizeWhatsAppSendResult,
19
+ type WhatsAppSendResult,
20
+ } from "./send-result.js";
21
+ import type { ActiveWebSendOptions } from "./types.js";
22
+
23
+ function recordWhatsAppOutbound(accountId: string) {
24
+ recordChannelActivity({
25
+ channel: "whatsapp",
26
+ accountId,
27
+ direction: "outbound",
28
+ });
29
+ }
30
+
31
+ function supportsForcedDocumentMediaType(mediaType: string): boolean {
32
+ return mediaType.startsWith("image/") || mediaType.startsWith("video/");
33
+ }
34
+
35
+ export function createWebSendApi(params: {
36
+ sock: {
37
+ sendMessage: (
38
+ jid: string,
39
+ content: AnyMessageContent,
40
+ options?: MiscMessageGenerationOptions,
41
+ ) => Promise<WAMessage | undefined>;
42
+ sendPresenceUpdate: (presence: WAPresence, jid?: string) => Promise<unknown>;
43
+ };
44
+ defaultAccountId: string;
45
+ resolveOutboundMentions?: (params: {
46
+ jid: string;
47
+ text: string;
48
+ }) => Promise<WhatsAppOutboundMentionResolution> | WhatsAppOutboundMentionResolution;
49
+ // When provided, lets outbound resolve `{phone}@s.whatsapp.net` to `{lid}@lid`
50
+ // via Baileys' lid-mapping-{phone-digits}.json files in the auth dir, so
51
+ // proactive sends to LID-addressed contacts reach the recipient instead of
52
+ // ending up in a sender-only ghost chat (#67378). Defaults to PN-only.
53
+ authDir?: string;
54
+ }) {
55
+ const resolveOutboundJid = (recipient: string): string =>
56
+ params.authDir
57
+ ? toWhatsappJidWithLid(recipient, { authDir: params.authDir })
58
+ : toWhatsappJid(recipient);
59
+ const resolveMentions = async (
60
+ jid: string,
61
+ text: string,
62
+ ): Promise<WhatsAppOutboundMentionResolution> =>
63
+ params.resolveOutboundMentions
64
+ ? await params.resolveOutboundMentions({ jid, text })
65
+ : { text, mentionedJids: [] };
66
+
67
+ return {
68
+ sendMessage: async (
69
+ to: string,
70
+ text: string,
71
+ mediaBuffer?: Buffer,
72
+ mediaType?: string,
73
+ sendOptions?: ActiveWebSendOptions,
74
+ ): Promise<WhatsAppSendResult> => {
75
+ const jid = resolveOutboundJid(to);
76
+ let payload: AnyMessageContent;
77
+ if (mediaBuffer) {
78
+ mediaType ??= "application/octet-stream";
79
+ }
80
+ const shouldSendAudioText = Boolean(
81
+ mediaBuffer && mediaType?.startsWith("audio/") && text.trim(),
82
+ );
83
+ const resolvedPayloadText = shouldSendAudioText
84
+ ? { text, mentionedJids: [] }
85
+ : await resolveMentions(jid, text);
86
+ if (mediaBuffer && mediaType) {
87
+ if (sendOptions?.asDocument === true && supportsForcedDocumentMediaType(mediaType)) {
88
+ const fileName = resolveWhatsAppDocumentFileName({
89
+ fileName: sendOptions?.fileName,
90
+ mimetype: mediaType,
91
+ });
92
+ payload = {
93
+ document: mediaBuffer,
94
+ fileName,
95
+ caption: resolvedPayloadText.text || undefined,
96
+ mimetype: mediaType,
97
+ };
98
+ } else if (mediaType.startsWith("image/")) {
99
+ payload = {
100
+ image: mediaBuffer,
101
+ caption: resolvedPayloadText.text || undefined,
102
+ mimetype: mediaType,
103
+ };
104
+ } else if (mediaType.startsWith("audio/")) {
105
+ payload = { audio: mediaBuffer, ptt: true, mimetype: mediaType };
106
+ } else if (mediaType.startsWith("video/")) {
107
+ const gifPlayback = sendOptions?.gifPlayback;
108
+ payload = {
109
+ video: mediaBuffer,
110
+ caption: resolvedPayloadText.text || undefined,
111
+ mimetype: mediaType,
112
+ ...(gifPlayback ? { gifPlayback: true } : {}),
113
+ };
114
+ } else {
115
+ const fileName = resolveWhatsAppDocumentFileName({
116
+ fileName: sendOptions?.fileName,
117
+ mimetype: mediaType,
118
+ });
119
+ payload = {
120
+ document: mediaBuffer,
121
+ fileName,
122
+ caption: resolvedPayloadText.text || undefined,
123
+ mimetype: mediaType,
124
+ };
125
+ }
126
+ } else {
127
+ payload = { text: resolvedPayloadText.text };
128
+ }
129
+ payload = addWhatsAppOutboundMentionsToContent(payload, resolvedPayloadText.mentionedJids);
130
+ const quotedOpts = buildQuotedMessageOptions({
131
+ messageId: sendOptions?.quotedMessageKey?.id,
132
+ remoteJid: sendOptions?.quotedMessageKey?.remoteJid,
133
+ fromMe: sendOptions?.quotedMessageKey?.fromMe,
134
+ participant: sendOptions?.quotedMessageKey?.participant,
135
+ messageText: sendOptions?.quotedMessageKey?.messageText,
136
+ });
137
+ const result = quotedOpts
138
+ ? await params.sock.sendMessage(jid, payload, quotedOpts)
139
+ : await params.sock.sendMessage(jid, payload);
140
+ const results = [normalizeWhatsAppSendResult(result, mediaBuffer ? "media" : "text")];
141
+ if (shouldSendAudioText) {
142
+ const resolvedAudioText = await resolveMentions(jid, text);
143
+ const textPayload = addWhatsAppOutboundMentionsToContent(
144
+ { text: resolvedAudioText.text },
145
+ resolvedAudioText.mentionedJids,
146
+ );
147
+ const textResult = quotedOpts
148
+ ? await params.sock.sendMessage(jid, textPayload, quotedOpts)
149
+ : await params.sock.sendMessage(jid, textPayload);
150
+ results.push(normalizeWhatsAppSendResult(textResult, "text"));
151
+ }
152
+ const accountId = sendOptions?.accountId ?? params.defaultAccountId;
153
+ recordWhatsAppOutbound(accountId);
154
+ return combineWhatsAppSendResults(mediaBuffer ? "media" : "text", results);
155
+ },
156
+ sendPoll: async (
157
+ to: string,
158
+ poll: { question: string; options: string[]; maxSelections?: number },
159
+ ): Promise<WhatsAppSendResult> => {
160
+ const jid = resolveOutboundJid(to);
161
+ const result = await params.sock.sendMessage(jid, {
162
+ poll: {
163
+ name: poll.question,
164
+ values: poll.options,
165
+ selectableCount: poll.maxSelections ?? 1,
166
+ },
167
+ } as AnyMessageContent);
168
+ recordWhatsAppOutbound(params.defaultAccountId);
169
+ return normalizeWhatsAppSendResult(result, "poll");
170
+ },
171
+ sendReaction: async (
172
+ chatJid: string,
173
+ messageId: string,
174
+ emoji: string,
175
+ fromMe: boolean,
176
+ participant?: string,
177
+ ): Promise<WhatsAppSendResult> => {
178
+ // chatJid is typically already a JID (group or DM); pass through
179
+ // unchanged. The participant is a sender id and stays PN-shaped to match
180
+ // how the existing inbound flow stores it.
181
+ const jid = toWhatsappJid(chatJid);
182
+ const result = await params.sock.sendMessage(jid, {
183
+ react: {
184
+ text: emoji,
185
+ key: {
186
+ remoteJid: jid,
187
+ id: messageId,
188
+ fromMe,
189
+ participant: participant ? toWhatsappJid(participant) : undefined,
190
+ },
191
+ },
192
+ } as AnyMessageContent);
193
+ return normalizeWhatsAppSendResult(result, "reaction");
194
+ },
195
+ sendComposingTo: async (to: string): Promise<void> => {
196
+ const jid = resolveOutboundJid(to);
197
+ if (isWhatsAppNewsletterJid(jid)) {
198
+ return;
199
+ }
200
+ await params.sock.sendPresenceUpdate("composing", jid);
201
+ },
202
+ } as const;
203
+ }
@@ -0,0 +1,109 @@
1
+ import type { WAMessage, WAMessageKey } from "baileys";
2
+ import {
3
+ createMessageReceiptFromOutboundResults,
4
+ listMessageReceiptPlatformIds,
5
+ type MessageReceipt,
6
+ type MessageReceiptPartKind,
7
+ type MessageReceiptSourceResult,
8
+ } from "autobot/plugin-sdk/channel-message";
9
+
10
+ export type WhatsAppSendKind = "media" | "poll" | "reaction" | "text";
11
+
12
+ type WhatsAppSendKey = Omit<
13
+ Pick<WAMessageKey, "fromMe" | "id" | "participant" | "remoteJid">,
14
+ "id"
15
+ > & {
16
+ id: string;
17
+ };
18
+
19
+ export type WhatsAppSendResult = {
20
+ kind: WhatsAppSendKind;
21
+ messageId: string;
22
+ receipt?: MessageReceipt;
23
+ keys: WhatsAppSendKey[];
24
+ providerAccepted: boolean;
25
+ };
26
+
27
+ function resolveWhatsAppReceiptKind(kind: WhatsAppSendKind): MessageReceiptPartKind {
28
+ if (kind === "media" || kind === "text") {
29
+ return kind;
30
+ }
31
+ return "unknown";
32
+ }
33
+
34
+ function toReceiptSourceResult(key: WhatsAppSendKey): MessageReceiptSourceResult {
35
+ return {
36
+ channel: "whatsapp",
37
+ messageId: key.id,
38
+ ...(key.remoteJid ? { toJid: key.remoteJid } : {}),
39
+ meta: {
40
+ fromMe: key.fromMe,
41
+ participant: key.participant,
42
+ },
43
+ };
44
+ }
45
+
46
+ function createWhatsAppSendReceipt(
47
+ kind: WhatsAppSendKind,
48
+ keys: readonly WhatsAppSendKey[],
49
+ ): MessageReceipt {
50
+ return createMessageReceiptFromOutboundResults({
51
+ kind: resolveWhatsAppReceiptKind(kind),
52
+ results: keys.map(toReceiptSourceResult),
53
+ });
54
+ }
55
+
56
+ function normalizeKey(key: WAMessageKey | undefined): WhatsAppSendKey | undefined {
57
+ const id = typeof key?.id === "string" ? key.id.trim() : "";
58
+ if (!id) {
59
+ return undefined;
60
+ }
61
+ return {
62
+ id,
63
+ remoteJid: key?.remoteJid,
64
+ fromMe: key?.fromMe,
65
+ participant: key?.participant,
66
+ };
67
+ }
68
+
69
+ export function normalizeWhatsAppSendResult(
70
+ result: WAMessage | undefined,
71
+ kind: WhatsAppSendKind,
72
+ ): WhatsAppSendResult {
73
+ const key = normalizeKey(result?.key);
74
+ const messageId = key?.id ?? "unknown";
75
+ return {
76
+ kind,
77
+ messageId,
78
+ receipt: createWhatsAppSendReceipt(kind, key ? [key] : []),
79
+ keys: key ? [key] : [],
80
+ providerAccepted: Boolean(key),
81
+ };
82
+ }
83
+
84
+ export function combineWhatsAppSendResults(
85
+ kind: WhatsAppSendKind,
86
+ results: readonly WhatsAppSendResult[],
87
+ ): WhatsAppSendResult {
88
+ const messageIds = [...new Set(results.flatMap(listWhatsAppSendResultMessageIds))];
89
+ const keys = results.flatMap((result) => result.keys);
90
+ return {
91
+ kind,
92
+ messageId: messageIds[0] ?? "unknown",
93
+ receipt: createWhatsAppSendReceipt(kind, keys),
94
+ keys,
95
+ providerAccepted: results.some((result) => result.providerAccepted),
96
+ };
97
+ }
98
+
99
+ export function listWhatsAppSendResultMessageIds(result: WhatsAppSendResult): string[] {
100
+ const receiptIds = result.receipt ? listMessageReceiptPlatformIds(result.receipt) : [];
101
+ if (receiptIds.length > 0) {
102
+ return receiptIds;
103
+ }
104
+ const keyIds = result.keys.map((key) => key.id.trim()).filter(Boolean);
105
+ if (keyIds.length > 0) {
106
+ return [...new Set(keyIds)];
107
+ }
108
+ return [];
109
+ }
@@ -0,0 +1,107 @@
1
+ import type { AnyMessageContent, MiscMessageGenerationOptions } from "baileys";
2
+ import type { NormalizedLocation } from "autobot/plugin-sdk/channel-inbound";
3
+ import type { PollInput } from "autobot/plugin-sdk/poll-runtime";
4
+ import type { WhatsAppIdentity, WhatsAppReplyContext, WhatsAppSelfIdentity } from "../identity.js";
5
+ import type { WhatsAppSendResult } from "./send-result.js";
6
+
7
+ export type WebListenerCloseReason = {
8
+ status?: number;
9
+ isLoggedOut: boolean;
10
+ error?: unknown;
11
+ };
12
+
13
+ export type ActiveWebSendOptions = {
14
+ quotedMessageKey?: {
15
+ id: string;
16
+ remoteJid: string;
17
+ fromMe: boolean;
18
+ participant?: string;
19
+ messageText?: string;
20
+ };
21
+ gifPlayback?: boolean;
22
+ accountId?: string;
23
+ fileName?: string;
24
+ asDocument?: boolean;
25
+ };
26
+
27
+ export type ActiveWebListener = {
28
+ sendMessage: (
29
+ to: string,
30
+ text: string,
31
+ mediaBuffer?: Buffer,
32
+ mediaType?: string,
33
+ options?: ActiveWebSendOptions,
34
+ ) => Promise<WhatsAppSendResult>;
35
+ sendPoll: (to: string, poll: PollInput) => Promise<WhatsAppSendResult>;
36
+ sendReaction: (
37
+ chatJid: string,
38
+ messageId: string,
39
+ emoji: string,
40
+ fromMe: boolean,
41
+ participant?: string,
42
+ ) => Promise<WhatsAppSendResult>;
43
+ sendComposingTo: (to: string) => Promise<void>;
44
+ close?: () => Promise<void>;
45
+ };
46
+
47
+ export type WhatsAppStructuredContactContext = {
48
+ kind: "contact" | "contacts";
49
+ total: number;
50
+ contacts: Array<{
51
+ name?: string;
52
+ phones?: string[];
53
+ }>;
54
+ };
55
+
56
+ export type WebInboundMessage = {
57
+ id?: string;
58
+ from: string; // conversation id: E.164 for direct chats, group JID for groups
59
+ conversationId: string; // alias for clarity (same as from)
60
+ to: string;
61
+ accountId: string;
62
+ /** Set by the real inbound monitor after access-control / pairing checks pass. */
63
+ accessControlPassed?: boolean;
64
+ body: string;
65
+ pushName?: string;
66
+ timestamp?: number;
67
+ chatType: "direct" | "group";
68
+ chatId: string;
69
+ sender?: WhatsAppIdentity;
70
+ senderJid?: string;
71
+ senderE164?: string;
72
+ senderName?: string;
73
+ replyTo?: WhatsAppReplyContext;
74
+ replyToId?: string;
75
+ replyToBody?: string;
76
+ replyToSender?: string;
77
+ replyToSenderJid?: string;
78
+ replyToSenderE164?: string;
79
+ groupSubject?: string;
80
+ groupParticipants?: string[];
81
+ mentions?: string[];
82
+ mentionedJids?: string[];
83
+ self?: WhatsAppSelfIdentity;
84
+ selfJid?: string | null;
85
+ selfLid?: string | null;
86
+ selfE164?: string | null;
87
+ fromMe?: boolean;
88
+ location?: NormalizedLocation;
89
+ sendComposing: () => Promise<void>;
90
+ reply: (text: string, options?: MiscMessageGenerationOptions) => Promise<WhatsAppSendResult>;
91
+ sendMedia: (
92
+ payload: AnyMessageContent,
93
+ options?: MiscMessageGenerationOptions,
94
+ ) => Promise<WhatsAppSendResult>;
95
+ mediaPath?: string;
96
+ mediaType?: string;
97
+ mediaFileName?: string;
98
+ mediaUrl?: string;
99
+ untrustedStructuredContext?: Array<{
100
+ label: string;
101
+ source?: string;
102
+ type?: string;
103
+ payload: unknown;
104
+ }>;
105
+ wasMentioned?: boolean;
106
+ isBatched?: boolean;
107
+ };