@flink-app/whatsapp-plugin 2.0.0-alpha.74

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.
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __rest = (this && this.__rest) || function (s, e) {
26
+ var t = {};
27
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
28
+ t[p] = s[p];
29
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
30
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
31
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
32
+ t[p[i]] = s[p[i]];
33
+ }
34
+ return t;
35
+ };
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.createWhatsappContext = createWhatsappContext;
38
+ exports.createScopedWhatsappContext = createScopedWhatsappContext;
39
+ const transport = __importStar(require("./WhatsappTransport"));
40
+ const DEFAULT_VERSION = "v21.0";
41
+ function getVersion(conn) {
42
+ var _a;
43
+ return (_a = conn.graphApiVersion) !== null && _a !== void 0 ? _a : DEFAULT_VERSION;
44
+ }
45
+ function createScopedContext(getConn) {
46
+ return {
47
+ async send(opts) {
48
+ const conn = getConn();
49
+ const { to } = opts, rest = __rest(opts, ["to"]);
50
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, Object.assign({ to }, rest), getVersion(conn));
51
+ },
52
+ async sendText(to, text) {
53
+ const conn = getConn();
54
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "text", text: { body: text } }, getVersion(conn));
55
+ },
56
+ async sendTemplate(to, template) {
57
+ const conn = getConn();
58
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "template", template }, getVersion(conn));
59
+ },
60
+ async sendImage(to, image, caption) {
61
+ const conn = getConn();
62
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "image", image: Object.assign(Object.assign({}, image), (caption ? { caption } : {})) }, getVersion(conn));
63
+ },
64
+ async sendDocument(to, doc, caption) {
65
+ const conn = getConn();
66
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "document", document: Object.assign(Object.assign({}, doc), (caption ? { caption } : {})) }, getVersion(conn));
67
+ },
68
+ async sendInteractiveButtons(to, body, buttons) {
69
+ const conn = getConn();
70
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, {
71
+ to,
72
+ type: "interactive",
73
+ interactive: {
74
+ type: "button",
75
+ body: { text: body },
76
+ action: {
77
+ buttons: buttons.map((b) => ({
78
+ type: "reply",
79
+ reply: { id: b.id, title: b.title },
80
+ })),
81
+ },
82
+ },
83
+ }, getVersion(conn));
84
+ },
85
+ async sendInteractiveList(to, body, buttonText, sections) {
86
+ const conn = getConn();
87
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, {
88
+ to,
89
+ type: "interactive",
90
+ interactive: {
91
+ type: "list",
92
+ body: { text: body },
93
+ action: {
94
+ button: buttonText,
95
+ sections,
96
+ },
97
+ },
98
+ }, getVersion(conn));
99
+ },
100
+ async sendLocation(to, location) {
101
+ const conn = getConn();
102
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "location", location }, getVersion(conn));
103
+ },
104
+ async sendReaction(to, messageId, emoji) {
105
+ const conn = getConn();
106
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "reaction", reaction: { message_id: messageId, emoji } }, getVersion(conn));
107
+ },
108
+ async reply(message, text) {
109
+ const conn = getConn();
110
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, {
111
+ to: message.from,
112
+ type: "text",
113
+ text: { body: text },
114
+ context: { message_id: message.messageId },
115
+ }, getVersion(conn));
116
+ },
117
+ async markAsRead(messageId) {
118
+ const conn = getConn();
119
+ await transport.markAsRead(conn.phoneNumberId, conn.accessToken, messageId, getVersion(conn));
120
+ },
121
+ async uploadMedia(file, mimeType, filename) {
122
+ const conn = getConn();
123
+ return transport.uploadMedia(conn.phoneNumberId, conn.accessToken, file, mimeType, filename, getVersion(conn));
124
+ },
125
+ async downloadMedia(mediaId) {
126
+ const conn = getConn();
127
+ return transport.downloadMedia(mediaId, conn.accessToken, getVersion(conn));
128
+ },
129
+ };
130
+ }
131
+ function createWhatsappContext(manager, addConnectionFn) {
132
+ const getConn = (connectionId) => {
133
+ return manager.get(connectionId !== null && connectionId !== void 0 ? connectionId : "default");
134
+ };
135
+ // Build the full object directly — do NOT spread a scoped context,
136
+ // because spreading eagerly evaluates getters before any connections have been added.
137
+ return {
138
+ async send(opts) {
139
+ const conn = getConn();
140
+ const { to } = opts, rest = __rest(opts, ["to"]);
141
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, Object.assign({ to }, rest), getVersion(conn));
142
+ },
143
+ async sendText(to, text) {
144
+ const conn = getConn();
145
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "text", text: { body: text } }, getVersion(conn));
146
+ },
147
+ async sendTemplate(to, template) {
148
+ const conn = getConn();
149
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "template", template }, getVersion(conn));
150
+ },
151
+ async sendImage(to, image, caption) {
152
+ const conn = getConn();
153
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "image", image: Object.assign(Object.assign({}, image), (caption ? { caption } : {})) }, getVersion(conn));
154
+ },
155
+ async sendDocument(to, doc, caption) {
156
+ const conn = getConn();
157
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "document", document: Object.assign(Object.assign({}, doc), (caption ? { caption } : {})) }, getVersion(conn));
158
+ },
159
+ async sendInteractiveButtons(to, body, buttons) {
160
+ const conn = getConn();
161
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, {
162
+ to,
163
+ type: "interactive",
164
+ interactive: {
165
+ type: "button",
166
+ body: { text: body },
167
+ action: {
168
+ buttons: buttons.map((b) => ({
169
+ type: "reply",
170
+ reply: { id: b.id, title: b.title },
171
+ })),
172
+ },
173
+ },
174
+ }, getVersion(conn));
175
+ },
176
+ async sendInteractiveList(to, body, buttonText, sections) {
177
+ const conn = getConn();
178
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, {
179
+ to,
180
+ type: "interactive",
181
+ interactive: {
182
+ type: "list",
183
+ body: { text: body },
184
+ action: {
185
+ button: buttonText,
186
+ sections,
187
+ },
188
+ },
189
+ }, getVersion(conn));
190
+ },
191
+ async sendLocation(to, location) {
192
+ const conn = getConn();
193
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "location", location }, getVersion(conn));
194
+ },
195
+ async sendReaction(to, messageId, emoji) {
196
+ const conn = getConn();
197
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, { to, type: "reaction", reaction: { message_id: messageId, emoji } }, getVersion(conn));
198
+ },
199
+ async reply(message, text) {
200
+ const conn = getConn();
201
+ return transport.sendMessage(conn.phoneNumberId, conn.accessToken, {
202
+ to: message.from,
203
+ type: "text",
204
+ text: { body: text },
205
+ context: { message_id: message.messageId },
206
+ }, getVersion(conn));
207
+ },
208
+ async markAsRead(messageId) {
209
+ const conn = getConn();
210
+ await transport.markAsRead(conn.phoneNumberId, conn.accessToken, messageId, getVersion(conn));
211
+ },
212
+ async uploadMedia(file, mimeType, filename) {
213
+ const conn = getConn();
214
+ return transport.uploadMedia(conn.phoneNumberId, conn.accessToken, file, mimeType, filename, getVersion(conn));
215
+ },
216
+ async downloadMedia(mediaId) {
217
+ const conn = getConn();
218
+ return transport.downloadMedia(mediaId, conn.accessToken, getVersion(conn));
219
+ },
220
+ addConnection(id, connection) {
221
+ addConnectionFn(id, connection);
222
+ },
223
+ removeConnection(id) {
224
+ manager.remove(id);
225
+ },
226
+ withConnection(id) {
227
+ return createScopedContext(() => manager.get(id));
228
+ },
229
+ listConnections() {
230
+ return manager.list();
231
+ },
232
+ };
233
+ }
234
+ function createScopedWhatsappContext(manager, connectionId) {
235
+ return createScopedContext(() => manager.get(connectionId));
236
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Convert standard Markdown to WhatsApp formatting.
3
+ *
4
+ * WhatsApp uses:
5
+ * *bold*
6
+ * _italic_
7
+ * ~strikethrough~
8
+ * ```code```
9
+ * `inline code` (not officially documented but works in most clients)
10
+ *
11
+ * Fenced code blocks and inline code are preserved as-is.
12
+ */
13
+ export declare function toWhatsappFormat(md: string): string;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toWhatsappFormat = toWhatsappFormat;
4
+ /**
5
+ * Convert standard Markdown to WhatsApp formatting.
6
+ *
7
+ * WhatsApp uses:
8
+ * *bold*
9
+ * _italic_
10
+ * ~strikethrough~
11
+ * ```code```
12
+ * `inline code` (not officially documented but works in most clients)
13
+ *
14
+ * Fenced code blocks and inline code are preserved as-is.
15
+ */
16
+ function toWhatsappFormat(md) {
17
+ // Split into code-fenced blocks vs everything else so we never
18
+ // mangle content inside ``` … ```
19
+ const parts = md.split(/(```[\s\S]*?```)/g);
20
+ for (let i = 0; i < parts.length; i++) {
21
+ // Odd indices are fenced code blocks — leave them alone
22
+ if (i % 2 === 1)
23
+ continue;
24
+ let text = parts[i];
25
+ // Protect inline code spans from further transforms
26
+ const inlineCode = [];
27
+ text = text.replace(/`[^`]+`/g, (match) => {
28
+ inlineCode.push(match);
29
+ return `\x00IC${inlineCode.length - 1}\x00`;
30
+ });
31
+ // Headings: # … → *…* (bold as best approximation)
32
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, "*$1*");
33
+ // Bold: **text** or __text__ → *text*
34
+ // Must run before italic so we don't eat the double markers
35
+ text = text.replace(/\*\*(.+?)\*\*/g, "*$1*");
36
+ text = text.replace(/__(.+?)__/g, "*$1*");
37
+ // Italic: *text* or _text_ → _text_
38
+ // Only match single * that aren't part of ** (already converted above)
39
+ text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "_$1_");
40
+ // Strikethrough: ~~text~~ → ~text~
41
+ text = text.replace(/~~(.+?)~~/g, "~$1~");
42
+ // Links: [text](url) → text (url)
43
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
44
+ // Images: ![alt](url) → url
45
+ text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$2");
46
+ // Restore inline code spans
47
+ text = text.replace(/\x00IC(\d+)\x00/g, (_, idx) => inlineCode[Number(idx)]);
48
+ parts[i] = text;
49
+ }
50
+ return parts.join("");
51
+ }
@@ -0,0 +1,6 @@
1
+ import { WhatsappMessage, WhatsappRouteProps } from "./types";
2
+ /**
3
+ * Returns true if the message matches the given route criteria.
4
+ * An empty/undefined Route always matches (catch-all).
5
+ */
6
+ export declare function matchesRoute(message: WhatsappMessage, route: WhatsappRouteProps): boolean;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchesRoute = matchesRoute;
4
+ function matchesStringOrRegExp(value, pattern) {
5
+ if (pattern instanceof RegExp) {
6
+ return pattern.test(value);
7
+ }
8
+ return value === pattern;
9
+ }
10
+ /**
11
+ * Returns true if the message matches the given route criteria.
12
+ * An empty/undefined Route always matches (catch-all).
13
+ */
14
+ function matchesRoute(message, route) {
15
+ if (route.type !== undefined) {
16
+ if (Array.isArray(route.type)) {
17
+ if (!route.type.includes(message.type))
18
+ return false;
19
+ }
20
+ else {
21
+ if (message.type !== route.type)
22
+ return false;
23
+ }
24
+ }
25
+ if (route.from !== undefined) {
26
+ if (typeof route.from === "function") {
27
+ if (!route.from(message))
28
+ return false;
29
+ }
30
+ else if (!matchesStringOrRegExp(message.from, route.from)) {
31
+ return false;
32
+ }
33
+ }
34
+ if (route.pattern !== undefined) {
35
+ if (typeof route.pattern === "function") {
36
+ if (!route.pattern(message))
37
+ return false;
38
+ }
39
+ else if (!message.text || !matchesStringOrRegExp(message.text, route.pattern)) {
40
+ return false;
41
+ }
42
+ }
43
+ if (route.connectionId !== undefined) {
44
+ if (Array.isArray(route.connectionId)) {
45
+ if (!route.connectionId.includes(message.connectionId))
46
+ return false;
47
+ }
48
+ else {
49
+ if (message.connectionId !== route.connectionId)
50
+ return false;
51
+ }
52
+ }
53
+ return true;
54
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@flink-app/whatsapp-plugin",
3
+ "version": "2.0.0-alpha.74",
4
+ "description": "Flink plugin for bi-directional WhatsApp messaging via Cloud API with auto-discovered WhatsappHandler files",
5
+ "author": "joel@frost.se",
6
+ "license": "MIT",
7
+ "types": "dist/index.d.ts",
8
+ "main": "dist/index.js",
9
+ "bin": {
10
+ "flink-whatsapp": "bin/flink-whatsapp.js"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "default": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ },
17
+ "./compiler": {
18
+ "default": "./dist/compiler.js",
19
+ "types": "./dist/compiler.d.ts"
20
+ }
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "22.13.10",
27
+ "tsc-watch": "^4.2.9",
28
+ "rimraf": "^5.0.5",
29
+ "@flink-app/flink": "2.0.0-alpha.74"
30
+ },
31
+ "peerDependencies": {
32
+ "@flink-app/flink": ">=2.0.0-alpha.74"
33
+ },
34
+ "scripts": {
35
+ "watch": "tsc-watch --project tsconfig.dist.json",
36
+ "build": "tsc --project tsconfig.dist.json",
37
+ "clean": "rimraf dist"
38
+ }
39
+ }
@@ -0,0 +1,67 @@
1
+ import { FlinkLogFactory } from "@flink-app/flink";
2
+ import { WhatsappConnectionOptions } from "./types";
3
+
4
+ const log = FlinkLogFactory.createLogger("flink.whatsapp-plugin.connections");
5
+
6
+ export class WhatsappConnectionManager {
7
+ private connections = new Map<string, WhatsappConnectionOptions>();
8
+ /** Reverse map: phoneNumberId → connection ID for webhook routing */
9
+ private phoneNumberIdMap = new Map<string, string>();
10
+
11
+ add(id: string, options: WhatsappConnectionOptions): void {
12
+ if (this.connections.has(id)) {
13
+ throw new Error(`WhatsApp connection "${id}" already exists`);
14
+ }
15
+
16
+ this.connections.set(id, options);
17
+ this.phoneNumberIdMap.set(options.phoneNumberId, id);
18
+ log.info(`Connection "${id}" added (phone number ID: ${options.phoneNumberId})`);
19
+ }
20
+
21
+ remove(id: string): void {
22
+ const conn = this.connections.get(id);
23
+ if (!conn) {
24
+ throw new Error(`WhatsApp connection "${id}" not found`);
25
+ }
26
+
27
+ this.phoneNumberIdMap.delete(conn.phoneNumberId);
28
+ this.connections.delete(id);
29
+ log.info(`Connection "${id}" removed`);
30
+ }
31
+
32
+ get(id: string): WhatsappConnectionOptions {
33
+ const conn = this.connections.get(id);
34
+ if (!conn) {
35
+ throw new Error(`WhatsApp connection "${id}" not found`);
36
+ }
37
+ return conn;
38
+ }
39
+
40
+ has(id: string): boolean {
41
+ return this.connections.has(id);
42
+ }
43
+
44
+ list(): string[] {
45
+ return Array.from(this.connections.keys());
46
+ }
47
+
48
+ /** Resolve a phone number ID from a webhook to a connection ID */
49
+ resolveConnectionId(phoneNumberId: string): string | undefined {
50
+ return this.phoneNumberIdMap.get(phoneNumberId);
51
+ }
52
+
53
+ /** Get any connection's appSecret for signature verification */
54
+ getAnyAppSecret(): string | undefined {
55
+ const first = this.connections.values().next();
56
+ return first.done ? undefined : first.value.appSecret;
57
+ }
58
+
59
+ /** Get all unique app secrets (for trying signature verification with multiple secrets) */
60
+ getAppSecrets(): string[] {
61
+ const secrets = new Set<string>();
62
+ for (const conn of this.connections.values()) {
63
+ secrets.add(conn.appSecret);
64
+ }
65
+ return Array.from(secrets);
66
+ }
67
+ }