@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.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/bin/flink-whatsapp.js +22 -0
- package/dist/WhatsappConnectionManager.d.ts +17 -0
- package/dist/WhatsappConnectionManager.js +60 -0
- package/dist/WhatsappTransport.d.ts +18 -0
- package/dist/WhatsappTransport.js +241 -0
- package/dist/autoRegisteredWhatsappHandlers.d.ts +6 -0
- package/dist/autoRegisteredWhatsappHandlers.js +8 -0
- package/dist/cli/send.d.ts +21 -0
- package/dist/cli/send.js +90 -0
- package/dist/compiler.d.ts +25 -0
- package/dist/compiler.js +27 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +192 -0
- package/dist/types.d.ts +252 -0
- package/dist/types.js +2 -0
- package/dist/whatsappContext.d.ts +4 -0
- package/dist/whatsappContext.js +236 -0
- package/dist/whatsappFormatting.d.ts +13 -0
- package/dist/whatsappFormatting.js +51 -0
- package/dist/whatsappRouter.d.ts +6 -0
- package/dist/whatsappRouter.js +54 -0
- package/package.json +39 -0
- package/src/WhatsappConnectionManager.ts +67 -0
- package/src/WhatsappTransport.ts +297 -0
- package/src/autoRegisteredWhatsappHandlers.ts +7 -0
- package/src/cli/send.ts +102 -0
- package/src/compiler.ts +32 -0
- package/src/index.ts +197 -0
- package/src/types.ts +284 -0
- package/src/whatsappContext.ts +385 -0
- package/src/whatsappFormatting.ts +59 -0
- package/src/whatsappRouter.ts +48 -0
- package/tsconfig.dist.json +4 -0
- package/tsconfig.json +24 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Frost Experience AB https://www.frost.se
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @flink-app/whatsapp-plugin
|
|
2
|
+
|
|
3
|
+
Bi-directional WhatsApp messaging plugin for [Flink](https://github.com/FrostDigital/flink-framework) via the WhatsApp Cloud API. No external SDK — uses native `fetch`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Auto-discovered `WhatsappHandler` files via Flink's compiler extension
|
|
8
|
+
- Route matching by message type, sender, text pattern, and connection
|
|
9
|
+
- Send text, templates, images, documents, interactive buttons/lists, locations, reactions
|
|
10
|
+
- Multi-connection support (multiple WhatsApp Business phone numbers)
|
|
11
|
+
- Webhook signature verification (HMAC-SHA256)
|
|
12
|
+
- Message status tracking (sent, delivered, read, failed)
|
|
13
|
+
- 24-hour conversation window awareness with template message support
|
|
14
|
+
- Markdown to WhatsApp formatting converter
|
|
15
|
+
- CLI tool for sending test messages
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @flink-app/whatsapp-plugin
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
// flink.config.js
|
|
25
|
+
const { compilerPlugin } = require("@flink-app/whatsapp-plugin/compiler");
|
|
26
|
+
module.exports = { compilerPlugins: [compilerPlugin()] };
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// src/index.ts
|
|
31
|
+
import { whatsappPlugin } from "@flink-app/whatsapp-plugin";
|
|
32
|
+
|
|
33
|
+
new FlinkApp<Ctx>({
|
|
34
|
+
plugins: [
|
|
35
|
+
whatsappPlugin<Ctx>({
|
|
36
|
+
connection: {
|
|
37
|
+
accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
|
|
38
|
+
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
|
|
39
|
+
verifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,
|
|
40
|
+
appSecret: process.env.WHATSAPP_APP_SECRET!,
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// src/whatsapp-handlers/HandleText.ts
|
|
49
|
+
import { WhatsappHandler, WhatsappRouteProps } from "@flink-app/whatsapp-plugin";
|
|
50
|
+
|
|
51
|
+
export const Route: WhatsappRouteProps = { type: "text" };
|
|
52
|
+
|
|
53
|
+
const handler: WhatsappHandler<Ctx> = async ({ message, whatsapp }) => {
|
|
54
|
+
await whatsapp.reply(message, `Echo: ${message.text}`);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default handler;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
See the [skill documentation](../../skills/flink-whatsapp-plugin/SKILL.md) for complete usage guide including routing, multi-connection setup, template messages, interactive messages, media handling, and more.
|
|
63
|
+
|
|
64
|
+
## Meta App Setup
|
|
65
|
+
|
|
66
|
+
1. Create a Meta App at [developers.facebook.com](https://developers.facebook.com)
|
|
67
|
+
2. Add the WhatsApp product
|
|
68
|
+
3. Configure webhook URL to `https://your-domain.com/webhooks/whatsapp`
|
|
69
|
+
4. Subscribe to the `messages` webhook field
|
|
70
|
+
5. Generate a permanent access token via a System User
|
|
71
|
+
|
|
72
|
+
## CLI
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pnpm exec flink-whatsapp send --token <token> --phone-id <id> --to <number> "Hello!"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const [, , command, ...rest] = process.argv;
|
|
5
|
+
|
|
6
|
+
if (!command || command === "--help" || command === "-h") {
|
|
7
|
+
console.log("Usage: flink-whatsapp <command> [options]");
|
|
8
|
+
console.log("");
|
|
9
|
+
console.log("Commands:");
|
|
10
|
+
console.log(" send --token <token> --phone-id <id> --to <number> <message>");
|
|
11
|
+
console.log("");
|
|
12
|
+
console.log("Run flink-whatsapp send --help for send options.");
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 'send' is the default (and only) command.
|
|
17
|
+
if (command === "send") {
|
|
18
|
+
process.argv = [process.argv[0], process.argv[1], ...rest];
|
|
19
|
+
} else {
|
|
20
|
+
process.argv = [process.argv[0], process.argv[1], command, ...rest];
|
|
21
|
+
}
|
|
22
|
+
require("../dist/cli/send.js");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { WhatsappConnectionOptions } from "./types";
|
|
2
|
+
export declare class WhatsappConnectionManager {
|
|
3
|
+
private connections;
|
|
4
|
+
/** Reverse map: phoneNumberId → connection ID for webhook routing */
|
|
5
|
+
private phoneNumberIdMap;
|
|
6
|
+
add(id: string, options: WhatsappConnectionOptions): void;
|
|
7
|
+
remove(id: string): void;
|
|
8
|
+
get(id: string): WhatsappConnectionOptions;
|
|
9
|
+
has(id: string): boolean;
|
|
10
|
+
list(): string[];
|
|
11
|
+
/** Resolve a phone number ID from a webhook to a connection ID */
|
|
12
|
+
resolveConnectionId(phoneNumberId: string): string | undefined;
|
|
13
|
+
/** Get any connection's appSecret for signature verification */
|
|
14
|
+
getAnyAppSecret(): string | undefined;
|
|
15
|
+
/** Get all unique app secrets (for trying signature verification with multiple secrets) */
|
|
16
|
+
getAppSecrets(): string[];
|
|
17
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WhatsappConnectionManager = void 0;
|
|
4
|
+
const flink_1 = require("@flink-app/flink");
|
|
5
|
+
const log = flink_1.FlinkLogFactory.createLogger("flink.whatsapp-plugin.connections");
|
|
6
|
+
class WhatsappConnectionManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.connections = new Map();
|
|
9
|
+
/** Reverse map: phoneNumberId → connection ID for webhook routing */
|
|
10
|
+
this.phoneNumberIdMap = new Map();
|
|
11
|
+
}
|
|
12
|
+
add(id, options) {
|
|
13
|
+
if (this.connections.has(id)) {
|
|
14
|
+
throw new Error(`WhatsApp connection "${id}" already exists`);
|
|
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
|
+
remove(id) {
|
|
21
|
+
const conn = this.connections.get(id);
|
|
22
|
+
if (!conn) {
|
|
23
|
+
throw new Error(`WhatsApp connection "${id}" not found`);
|
|
24
|
+
}
|
|
25
|
+
this.phoneNumberIdMap.delete(conn.phoneNumberId);
|
|
26
|
+
this.connections.delete(id);
|
|
27
|
+
log.info(`Connection "${id}" removed`);
|
|
28
|
+
}
|
|
29
|
+
get(id) {
|
|
30
|
+
const conn = this.connections.get(id);
|
|
31
|
+
if (!conn) {
|
|
32
|
+
throw new Error(`WhatsApp connection "${id}" not found`);
|
|
33
|
+
}
|
|
34
|
+
return conn;
|
|
35
|
+
}
|
|
36
|
+
has(id) {
|
|
37
|
+
return this.connections.has(id);
|
|
38
|
+
}
|
|
39
|
+
list() {
|
|
40
|
+
return Array.from(this.connections.keys());
|
|
41
|
+
}
|
|
42
|
+
/** Resolve a phone number ID from a webhook to a connection ID */
|
|
43
|
+
resolveConnectionId(phoneNumberId) {
|
|
44
|
+
return this.phoneNumberIdMap.get(phoneNumberId);
|
|
45
|
+
}
|
|
46
|
+
/** Get any connection's appSecret for signature verification */
|
|
47
|
+
getAnyAppSecret() {
|
|
48
|
+
const first = this.connections.values().next();
|
|
49
|
+
return first.done ? undefined : first.value.appSecret;
|
|
50
|
+
}
|
|
51
|
+
/** Get all unique app secrets (for trying signature verification with multiple secrets) */
|
|
52
|
+
getAppSecrets() {
|
|
53
|
+
const secrets = new Set();
|
|
54
|
+
for (const conn of this.connections.values()) {
|
|
55
|
+
secrets.add(conn.appSecret);
|
|
56
|
+
}
|
|
57
|
+
return Array.from(secrets);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.WhatsappConnectionManager = WhatsappConnectionManager;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { WhatsappMessage, WhatsappStatusUpdate, WhatsappSendResult } from "./types";
|
|
2
|
+
/** Verify HMAC-SHA256 webhook signature from Meta */
|
|
3
|
+
export declare function verifySignature(rawBody: string | Buffer, signature: string, appSecret: string): boolean;
|
|
4
|
+
/** Make an authenticated Graph API call */
|
|
5
|
+
export declare function callApi(path: string, method: string, body: Record<string, any> | undefined, accessToken: string, version?: string): Promise<any>;
|
|
6
|
+
/** Send a message via the WhatsApp Cloud API */
|
|
7
|
+
export declare function sendMessage(phoneNumberId: string, accessToken: string, payload: Record<string, any>, version?: string): Promise<WhatsappSendResult>;
|
|
8
|
+
/** Upload media to WhatsApp (multipart form data) */
|
|
9
|
+
export declare function uploadMedia(phoneNumberId: string, accessToken: string, file: Buffer, mimeType: string, filename: string, version?: string): Promise<string>;
|
|
10
|
+
/** Download media by ID (two-step: get URL, then fetch binary) */
|
|
11
|
+
export declare function downloadMedia(mediaId: string, accessToken: string, version?: string): Promise<Buffer>;
|
|
12
|
+
/** Mark a message as read */
|
|
13
|
+
export declare function markAsRead(phoneNumberId: string, accessToken: string, messageId: string, version?: string): Promise<void>;
|
|
14
|
+
/** Extract normalized messages and status updates from a webhook payload */
|
|
15
|
+
export declare function normalizeWebhookPayload(payload: Record<string, any>, connectionId: string, phoneNumberId: string): {
|
|
16
|
+
messages: WhatsappMessage[];
|
|
17
|
+
statuses: WhatsappStatusUpdate[];
|
|
18
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.verifySignature = verifySignature;
|
|
27
|
+
exports.callApi = callApi;
|
|
28
|
+
exports.sendMessage = sendMessage;
|
|
29
|
+
exports.uploadMedia = uploadMedia;
|
|
30
|
+
exports.downloadMedia = downloadMedia;
|
|
31
|
+
exports.markAsRead = markAsRead;
|
|
32
|
+
exports.normalizeWebhookPayload = normalizeWebhookPayload;
|
|
33
|
+
const crypto = __importStar(require("crypto"));
|
|
34
|
+
const flink_1 = require("@flink-app/flink");
|
|
35
|
+
const log = flink_1.FlinkLogFactory.createLogger("flink.whatsapp-plugin.transport");
|
|
36
|
+
const DEFAULT_GRAPH_API_VERSION = "v21.0";
|
|
37
|
+
function graphUrl(version, path) {
|
|
38
|
+
return `https://graph.facebook.com/${version}/${path}`;
|
|
39
|
+
}
|
|
40
|
+
/** Verify HMAC-SHA256 webhook signature from Meta */
|
|
41
|
+
function verifySignature(rawBody, signature, appSecret) {
|
|
42
|
+
if (!signature || !signature.startsWith("sha256=")) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const expected = crypto
|
|
46
|
+
.createHmac("sha256", appSecret)
|
|
47
|
+
.update(rawBody)
|
|
48
|
+
.digest("hex");
|
|
49
|
+
return crypto.timingSafeEqual(Buffer.from(signature.replace("sha256=", "")), Buffer.from(expected));
|
|
50
|
+
}
|
|
51
|
+
/** Make an authenticated Graph API call */
|
|
52
|
+
async function callApi(path, method, body, accessToken, version = DEFAULT_GRAPH_API_VERSION) {
|
|
53
|
+
var _a, _b;
|
|
54
|
+
const url = graphUrl(version, path);
|
|
55
|
+
const headers = {
|
|
56
|
+
Authorization: `Bearer ${accessToken}`,
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
};
|
|
59
|
+
const response = await fetch(url, {
|
|
60
|
+
method,
|
|
61
|
+
headers,
|
|
62
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
63
|
+
});
|
|
64
|
+
const data = await response.json();
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const errMsg = (_b = (_a = data === null || data === void 0 ? void 0 : data.error) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : response.statusText;
|
|
67
|
+
throw new Error(`WhatsApp API error (${response.status}): ${errMsg}`);
|
|
68
|
+
}
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
/** Send a message via the WhatsApp Cloud API */
|
|
72
|
+
async function sendMessage(phoneNumberId, accessToken, payload, version = DEFAULT_GRAPH_API_VERSION) {
|
|
73
|
+
var _a, _b, _c;
|
|
74
|
+
const data = await callApi(`${phoneNumberId}/messages`, "POST", Object.assign({ messaging_product: "whatsapp" }, payload), accessToken, version);
|
|
75
|
+
return { messageId: (_c = (_b = (_a = data.messages) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : "" };
|
|
76
|
+
}
|
|
77
|
+
/** Upload media to WhatsApp (multipart form data) */
|
|
78
|
+
async function uploadMedia(phoneNumberId, accessToken, file, mimeType, filename, version = DEFAULT_GRAPH_API_VERSION) {
|
|
79
|
+
var _a, _b;
|
|
80
|
+
const url = graphUrl(version, `${phoneNumberId}/media`);
|
|
81
|
+
const formData = new FormData();
|
|
82
|
+
formData.append("messaging_product", "whatsapp");
|
|
83
|
+
formData.append("file", new Blob([file], { type: mimeType }), filename);
|
|
84
|
+
formData.append("type", mimeType);
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
88
|
+
body: formData,
|
|
89
|
+
});
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const errMsg = (_b = (_a = data === null || data === void 0 ? void 0 : data.error) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : response.statusText;
|
|
93
|
+
throw new Error(`WhatsApp media upload error (${response.status}): ${errMsg}`);
|
|
94
|
+
}
|
|
95
|
+
return data.id;
|
|
96
|
+
}
|
|
97
|
+
/** Download media by ID (two-step: get URL, then fetch binary) */
|
|
98
|
+
async function downloadMedia(mediaId, accessToken, version = DEFAULT_GRAPH_API_VERSION) {
|
|
99
|
+
// Step 1: Get the media URL
|
|
100
|
+
const mediaInfo = await callApi(mediaId, "GET", undefined, accessToken, version);
|
|
101
|
+
const mediaUrl = mediaInfo.url;
|
|
102
|
+
if (!mediaUrl) {
|
|
103
|
+
throw new Error(`No download URL returned for media ${mediaId}`);
|
|
104
|
+
}
|
|
105
|
+
// Step 2: Download the binary content
|
|
106
|
+
const response = await fetch(mediaUrl, {
|
|
107
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
108
|
+
});
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
throw new Error(`Failed to download media ${mediaId}: ${response.status} ${response.statusText}`);
|
|
111
|
+
}
|
|
112
|
+
return Buffer.from(await response.arrayBuffer());
|
|
113
|
+
}
|
|
114
|
+
/** Mark a message as read */
|
|
115
|
+
async function markAsRead(phoneNumberId, accessToken, messageId, version = DEFAULT_GRAPH_API_VERSION) {
|
|
116
|
+
await callApi(`${phoneNumberId}/messages`, "POST", {
|
|
117
|
+
messaging_product: "whatsapp",
|
|
118
|
+
status: "read",
|
|
119
|
+
message_id: messageId,
|
|
120
|
+
}, accessToken, version);
|
|
121
|
+
}
|
|
122
|
+
function resolveMessageType(msgType) {
|
|
123
|
+
const known = [
|
|
124
|
+
"text", "image", "video", "audio", "document",
|
|
125
|
+
"sticker", "location", "contacts", "interactive",
|
|
126
|
+
"reaction", "order",
|
|
127
|
+
];
|
|
128
|
+
return known.includes(msgType)
|
|
129
|
+
? msgType
|
|
130
|
+
: "unknown";
|
|
131
|
+
}
|
|
132
|
+
function extractMedia(msg, type) {
|
|
133
|
+
const mediaTypes = ["image", "video", "audio", "document", "sticker"];
|
|
134
|
+
if (!mediaTypes.includes(type))
|
|
135
|
+
return undefined;
|
|
136
|
+
const mediaData = msg[type];
|
|
137
|
+
if (!mediaData)
|
|
138
|
+
return undefined;
|
|
139
|
+
return {
|
|
140
|
+
id: mediaData.id,
|
|
141
|
+
mimeType: mediaData.mime_type,
|
|
142
|
+
sha256: mediaData.sha256,
|
|
143
|
+
caption: mediaData.caption,
|
|
144
|
+
filename: mediaData.filename,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function extractInteractive(msg) {
|
|
148
|
+
const interactive = msg.interactive;
|
|
149
|
+
if (!interactive)
|
|
150
|
+
return undefined;
|
|
151
|
+
const type = interactive.type === "button_reply" ? "button_reply" : "list_reply";
|
|
152
|
+
if (type === "button_reply" && interactive.button_reply) {
|
|
153
|
+
return {
|
|
154
|
+
type: "button_reply",
|
|
155
|
+
buttonReplyId: interactive.button_reply.id,
|
|
156
|
+
buttonReplyTitle: interactive.button_reply.title,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (type === "list_reply" && interactive.list_reply) {
|
|
160
|
+
return {
|
|
161
|
+
type: "list_reply",
|
|
162
|
+
listReplyId: interactive.list_reply.id,
|
|
163
|
+
listReplyTitle: interactive.list_reply.title,
|
|
164
|
+
listReplyDescription: interactive.list_reply.description,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
/** Extract normalized messages and status updates from a webhook payload */
|
|
170
|
+
function normalizeWebhookPayload(payload, connectionId, phoneNumberId) {
|
|
171
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
|
|
172
|
+
const messages = [];
|
|
173
|
+
const statuses = [];
|
|
174
|
+
const entries = (_a = payload === null || payload === void 0 ? void 0 : payload.entry) !== null && _a !== void 0 ? _a : [];
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const changes = (_b = entry === null || entry === void 0 ? void 0 : entry.changes) !== null && _b !== void 0 ? _b : [];
|
|
177
|
+
for (const change of changes) {
|
|
178
|
+
if ((change === null || change === void 0 ? void 0 : change.field) !== "messages")
|
|
179
|
+
continue;
|
|
180
|
+
const value = change === null || change === void 0 ? void 0 : change.value;
|
|
181
|
+
if (!value)
|
|
182
|
+
continue;
|
|
183
|
+
const contacts = (_c = value.contacts) !== null && _c !== void 0 ? _c : [];
|
|
184
|
+
const contactMap = new Map();
|
|
185
|
+
for (const contact of contacts) {
|
|
186
|
+
contactMap.set(contact.wa_id, (_e = (_d = contact.profile) === null || _d === void 0 ? void 0 : _d.name) !== null && _e !== void 0 ? _e : "");
|
|
187
|
+
}
|
|
188
|
+
// Process messages
|
|
189
|
+
for (const msg of (_f = value.messages) !== null && _f !== void 0 ? _f : []) {
|
|
190
|
+
const type = resolveMessageType(msg.type);
|
|
191
|
+
const from = (_g = msg.from) !== null && _g !== void 0 ? _g : "";
|
|
192
|
+
const normalized = {
|
|
193
|
+
connectionId,
|
|
194
|
+
messageId: (_h = msg.id) !== null && _h !== void 0 ? _h : "",
|
|
195
|
+
from,
|
|
196
|
+
to: phoneNumberId,
|
|
197
|
+
senderName: (_j = contactMap.get(from)) !== null && _j !== void 0 ? _j : "",
|
|
198
|
+
timestamp: Number(msg.timestamp) || 0,
|
|
199
|
+
type,
|
|
200
|
+
text: (_k = msg.text) === null || _k === void 0 ? void 0 : _k.body,
|
|
201
|
+
media: extractMedia(msg, msg.type),
|
|
202
|
+
location: msg.location
|
|
203
|
+
? {
|
|
204
|
+
latitude: msg.location.latitude,
|
|
205
|
+
longitude: msg.location.longitude,
|
|
206
|
+
name: msg.location.name,
|
|
207
|
+
address: msg.location.address,
|
|
208
|
+
}
|
|
209
|
+
: undefined,
|
|
210
|
+
contacts: msg.contacts,
|
|
211
|
+
interactive: extractInteractive(msg),
|
|
212
|
+
reaction: msg.reaction
|
|
213
|
+
? {
|
|
214
|
+
messageId: msg.reaction.message_id,
|
|
215
|
+
emoji: (_l = msg.reaction.emoji) !== null && _l !== void 0 ? _l : "",
|
|
216
|
+
}
|
|
217
|
+
: undefined,
|
|
218
|
+
raw: msg,
|
|
219
|
+
};
|
|
220
|
+
messages.push(normalized);
|
|
221
|
+
}
|
|
222
|
+
// Process statuses
|
|
223
|
+
for (const status of (_m = value.statuses) !== null && _m !== void 0 ? _m : []) {
|
|
224
|
+
statuses.push({
|
|
225
|
+
connectionId,
|
|
226
|
+
messageId: (_o = status.id) !== null && _o !== void 0 ? _o : "",
|
|
227
|
+
recipientId: (_p = status.recipient_id) !== null && _p !== void 0 ? _p : "",
|
|
228
|
+
status: (_q = status.status) !== null && _q !== void 0 ? _q : "failed",
|
|
229
|
+
timestamp: Number(status.timestamp) || 0,
|
|
230
|
+
errors: (_r = status.errors) === null || _r === void 0 ? void 0 : _r.map((e) => ({
|
|
231
|
+
code: e.code,
|
|
232
|
+
title: e.title,
|
|
233
|
+
message: e.message,
|
|
234
|
+
href: e.href,
|
|
235
|
+
})),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { messages, statuses };
|
|
241
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.autoRegisteredWhatsappHandlers = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Populated at compile time by the Flink compiler extension.
|
|
6
|
+
* Do not modify manually.
|
|
7
|
+
*/
|
|
8
|
+
exports.autoRegisteredWhatsappHandlers = [];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* flink-whatsapp send — send a test message to a WhatsApp number.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* flink-whatsapp send --token <access-token> --phone-id <phone-number-id> --to <number> "Hello world"
|
|
7
|
+
*
|
|
8
|
+
* Options:
|
|
9
|
+
* --token <token> Access token (or set WHATSAPP_ACCESS_TOKEN env var)
|
|
10
|
+
* --phone-id <id> Phone number ID (or set WHATSAPP_PHONE_NUMBER_ID env var)
|
|
11
|
+
* --to <number> Recipient phone number (with country code, e.g. 14155551234)
|
|
12
|
+
* --api-version <ver> Graph API version (default: v21.0)
|
|
13
|
+
*/
|
|
14
|
+
declare function parseArgs(argv: string[]): {
|
|
15
|
+
token: string;
|
|
16
|
+
phoneNumberId: string;
|
|
17
|
+
to: string;
|
|
18
|
+
text: string;
|
|
19
|
+
apiVersion: string;
|
|
20
|
+
};
|
|
21
|
+
declare function main(): Promise<void>;
|
package/dist/cli/send.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* flink-whatsapp send — send a test message to a WhatsApp number.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* flink-whatsapp send --token <access-token> --phone-id <phone-number-id> --to <number> "Hello world"
|
|
8
|
+
*
|
|
9
|
+
* Options:
|
|
10
|
+
* --token <token> Access token (or set WHATSAPP_ACCESS_TOKEN env var)
|
|
11
|
+
* --phone-id <id> Phone number ID (or set WHATSAPP_PHONE_NUMBER_ID env var)
|
|
12
|
+
* --to <number> Recipient phone number (with country code, e.g. 14155551234)
|
|
13
|
+
* --api-version <ver> Graph API version (default: v21.0)
|
|
14
|
+
*/
|
|
15
|
+
function parseArgs(argv) {
|
|
16
|
+
var _a, _b;
|
|
17
|
+
const args = argv.slice(2);
|
|
18
|
+
let token = (_a = process.env.WHATSAPP_ACCESS_TOKEN) !== null && _a !== void 0 ? _a : "";
|
|
19
|
+
let phoneNumberId = (_b = process.env.WHATSAPP_PHONE_NUMBER_ID) !== null && _b !== void 0 ? _b : "";
|
|
20
|
+
let to = "";
|
|
21
|
+
let apiVersion = "v21.0";
|
|
22
|
+
const textParts = [];
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
const a = args[i];
|
|
25
|
+
if (a === "--token") {
|
|
26
|
+
token = args[++i];
|
|
27
|
+
}
|
|
28
|
+
else if (a === "--phone-id") {
|
|
29
|
+
phoneNumberId = args[++i];
|
|
30
|
+
}
|
|
31
|
+
else if (a === "--to") {
|
|
32
|
+
to = args[++i];
|
|
33
|
+
}
|
|
34
|
+
else if (a === "--api-version") {
|
|
35
|
+
apiVersion = args[++i];
|
|
36
|
+
}
|
|
37
|
+
else if (!a.startsWith("--")) {
|
|
38
|
+
textParts.push(a);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const text = textParts.join(" ");
|
|
42
|
+
if (!token) {
|
|
43
|
+
console.error("Error: --token is required (or set WHATSAPP_ACCESS_TOKEN env var)");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (!phoneNumberId) {
|
|
47
|
+
console.error("Error: --phone-id is required (or set WHATSAPP_PHONE_NUMBER_ID env var)");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (!to) {
|
|
51
|
+
console.error("Error: --to is required");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
if (!text) {
|
|
55
|
+
console.error("Error: message text is required");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
return { token, phoneNumberId, to, text, apiVersion };
|
|
59
|
+
}
|
|
60
|
+
async function main() {
|
|
61
|
+
var _a, _b, _c, _d, _e;
|
|
62
|
+
const { token, phoneNumberId, to, text, apiVersion } = parseArgs(process.argv);
|
|
63
|
+
const url = `https://graph.facebook.com/${apiVersion}/${phoneNumberId}/messages`;
|
|
64
|
+
console.log(`Sending to ${to}…`);
|
|
65
|
+
const response = await fetch(url, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${token}`,
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
messaging_product: "whatsapp",
|
|
73
|
+
to,
|
|
74
|
+
type: "text",
|
|
75
|
+
text: { body: text },
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
const data = await response.json();
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const errMsg = (_b = (_a = data === null || data === void 0 ? void 0 : data.error) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : response.statusText;
|
|
81
|
+
throw new Error(`WhatsApp API error (${response.status}): ${errMsg}`);
|
|
82
|
+
}
|
|
83
|
+
const messageId = (_e = (_d = (_c = data.messages) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.id) !== null && _e !== void 0 ? _e : "unknown";
|
|
84
|
+
console.log(`Message sent (id: ${messageId})`);
|
|
85
|
+
}
|
|
86
|
+
main().catch((err) => {
|
|
87
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
+
console.error("Error:", msg || "unknown error");
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time compiler plugin descriptor for @flink-app/whatsapp-plugin.
|
|
3
|
+
*
|
|
4
|
+
* Import this from flink.config.js — it MUST NOT import from @flink-app/flink
|
|
5
|
+
* to avoid circular build-time dependencies.
|
|
6
|
+
*
|
|
7
|
+
* Usage in flink.config.js:
|
|
8
|
+
* ```js
|
|
9
|
+
* const { compilerPlugin } = require("@flink-app/whatsapp-plugin/compiler");
|
|
10
|
+
* module.exports = {
|
|
11
|
+
* compilerPlugins: [compilerPlugin()],
|
|
12
|
+
* };
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
interface FlinkCompilerPlugin {
|
|
16
|
+
package: string;
|
|
17
|
+
scanDir: string;
|
|
18
|
+
generatedFile: string;
|
|
19
|
+
registrationVar: string;
|
|
20
|
+
detectBy?: (fileContent: string, filePath: string) => boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function compilerPlugin(opts?: {
|
|
23
|
+
scanDir?: string;
|
|
24
|
+
}): FlinkCompilerPlugin;
|
|
25
|
+
export {};
|
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Build-time compiler plugin descriptor for @flink-app/whatsapp-plugin.
|
|
4
|
+
*
|
|
5
|
+
* Import this from flink.config.js — it MUST NOT import from @flink-app/flink
|
|
6
|
+
* to avoid circular build-time dependencies.
|
|
7
|
+
*
|
|
8
|
+
* Usage in flink.config.js:
|
|
9
|
+
* ```js
|
|
10
|
+
* const { compilerPlugin } = require("@flink-app/whatsapp-plugin/compiler");
|
|
11
|
+
* module.exports = {
|
|
12
|
+
* compilerPlugins: [compilerPlugin()],
|
|
13
|
+
* };
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.compilerPlugin = compilerPlugin;
|
|
18
|
+
function compilerPlugin(opts) {
|
|
19
|
+
var _a;
|
|
20
|
+
return {
|
|
21
|
+
package: "@flink-app/whatsapp-plugin",
|
|
22
|
+
scanDir: (_a = opts === null || opts === void 0 ? void 0 : opts.scanDir) !== null && _a !== void 0 ? _a : "src/whatsapp-handlers",
|
|
23
|
+
generatedFile: "generatedWhatsappHandlers",
|
|
24
|
+
registrationVar: "autoRegisteredWhatsappHandlers",
|
|
25
|
+
detectBy: (fileContent) => fileContent.includes("WhatsappHandler"),
|
|
26
|
+
};
|
|
27
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { WhatsappPluginOptions } from "./types";
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export * from "./autoRegisteredWhatsappHandlers";
|
|
4
|
+
export * from "./whatsappRouter";
|
|
5
|
+
export { toWhatsappFormat } from "./whatsappFormatting";
|
|
6
|
+
export declare function whatsappPlugin<TCtx>(options: WhatsappPluginOptions<TCtx>): {
|
|
7
|
+
id: string;
|
|
8
|
+
ctx: import("./types").WhatsappApi;
|
|
9
|
+
init: (app: {
|
|
10
|
+
ctx: TCtx;
|
|
11
|
+
expressApp?: any;
|
|
12
|
+
}) => Promise<void>;
|
|
13
|
+
};
|