core-services-sdk 1.3.49 → 1.3.50
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/package.json +1 -1
- package/src/instant-messages/index.js +2 -0
- package/src/instant-messages/telegram-apis/telegram-apis.js +301 -0
- package/src/instant-messages/whatsapp-apis/whatsapp-apis.js +401 -0
- package/tests/instant-messages/telegram-apis.unit.test.js +157 -0
- package/tests/instant-messages/whatsapp-apis.unit.test.js +277 -0
- package/types/instant-messages/index.d.ts +2 -0
- package/types/instant-messages/telegram-apis/telegram-apis.d.ts +105 -0
- package/types/instant-messages/whatsapp-apis/whatsapp-apis.d.ts +73 -0
package/package.json
CHANGED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { post } from '../../http/http.js'
|
|
2
|
+
|
|
3
|
+
const TELEGRAM_API_BASE_URL = 'https://api.telegram.org'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a full set of Telegram Bot API endpoint URLs
|
|
7
|
+
* based on the provided bot token and an optional base URL.
|
|
8
|
+
*
|
|
9
|
+
* This helper centralizes all Telegram endpoints used by the system,
|
|
10
|
+
* making it easier to mock, override, or customize for testing environments.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {Object} TelegramApiUrls
|
|
13
|
+
* @property {string} SEND_MESSAGE
|
|
14
|
+
* URL to send a text message to a chat using the Telegram Bot API.
|
|
15
|
+
*
|
|
16
|
+
* @property {string} FORWARD_MESSAGE
|
|
17
|
+
* URL to forward an existing message from one chat to another.
|
|
18
|
+
*
|
|
19
|
+
* @property {string} SEND_PHOTO
|
|
20
|
+
* URL to send a photo to a chat.
|
|
21
|
+
*
|
|
22
|
+
* @property {string} SEND_AUDIO
|
|
23
|
+
* URL to send an audio file.
|
|
24
|
+
*
|
|
25
|
+
* @property {string} SEND_DOCUMENT
|
|
26
|
+
* URL to send files such as PDF, DOC, ZIP, and others supported by Telegram.
|
|
27
|
+
*
|
|
28
|
+
* @property {string} SEND_STICKER
|
|
29
|
+
* URL to send a sticker.
|
|
30
|
+
*
|
|
31
|
+
* @property {string} SEND_VIDEO
|
|
32
|
+
* URL to send a video file.
|
|
33
|
+
*
|
|
34
|
+
* @property {string} SEND_VOICE
|
|
35
|
+
* URL to send a voice note.
|
|
36
|
+
*
|
|
37
|
+
* @property {string} SEND_LOCATION
|
|
38
|
+
* URL to send a geolocation point.
|
|
39
|
+
*
|
|
40
|
+
* @property {string} SEND_CHAT_ACTION
|
|
41
|
+
* URL to send a chat action (typing, uploading photo, etc).
|
|
42
|
+
*
|
|
43
|
+
* @property {string} GET_USER_PROFILE_PHOTOS
|
|
44
|
+
* URL to retrieve the profile photos of a specific user.
|
|
45
|
+
*
|
|
46
|
+
* @property {string} GET_UPDATES
|
|
47
|
+
* URL to poll for new updates (not used when using webhooks).
|
|
48
|
+
*
|
|
49
|
+
* @property {string} GET_FILE
|
|
50
|
+
* URL to fetch a file path for downloading a file uploaded to Telegram servers.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generates Telegram Bot API endpoint URLs for the given bot token.
|
|
55
|
+
*
|
|
56
|
+
* @param {Object} params
|
|
57
|
+
* @param {string} params.token
|
|
58
|
+
* The bot token obtained from BotFather.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} [params.telegramBaseUrl=TELEGRAM_API_BASE_URL]
|
|
61
|
+
* Optional override for the Telegram API base URL.
|
|
62
|
+
* Useful for testing or for proxying requests.
|
|
63
|
+
*
|
|
64
|
+
* @returns {TelegramApiUrls}
|
|
65
|
+
* A dictionary of fully resolved Telegram API endpoint URLs.
|
|
66
|
+
*/
|
|
67
|
+
export const getTelegramApiUrls = ({
|
|
68
|
+
token,
|
|
69
|
+
telegramBaseUrl = TELEGRAM_API_BASE_URL,
|
|
70
|
+
}) => ({
|
|
71
|
+
SEND_MESSAGE: `${telegramBaseUrl}/bot${token}/sendMessage`,
|
|
72
|
+
FORWARD_MESSAGE: `${telegramBaseUrl}/bot${token}/forwardMessage`,
|
|
73
|
+
SEND_PHOTO: `${telegramBaseUrl}/bot${token}/sendPhoto`,
|
|
74
|
+
SEND_AUDIO: `${telegramBaseUrl}/bot${token}/sendAudio`,
|
|
75
|
+
SEND_DOCUMENT: `${telegramBaseUrl}/bot${token}/sendDocument`,
|
|
76
|
+
SEND_STICKER: `${telegramBaseUrl}/bot${token}/sendSticker`,
|
|
77
|
+
SEND_VIDEO: `${telegramBaseUrl}/bot${token}/sendVideo`,
|
|
78
|
+
SEND_VOICE: `${telegramBaseUrl}/bot${token}/sendVoice`,
|
|
79
|
+
SEND_LOCATION: `${telegramBaseUrl}/bot${token}/sendLocation`,
|
|
80
|
+
SEND_CHAT_ACTION: `${telegramBaseUrl}/bot${token}/sendChatAction`,
|
|
81
|
+
GET_USER_PROFILE_PHOTOS: `${telegramBaseUrl}/bot${token}/getUserProfilePhotos`,
|
|
82
|
+
GET_UPDATES: `${telegramBaseUrl}/bot${token}/getUpdates`,
|
|
83
|
+
GET_FILE: `${telegramBaseUrl}/bot${token}/getFile`,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Factory that creates a set of high level Telegram Bot API helper methods.
|
|
88
|
+
*
|
|
89
|
+
* Each method sends a specific type of message (text, photo, video, document)
|
|
90
|
+
* through the Telegram Bot API using the provided bot token.
|
|
91
|
+
*
|
|
92
|
+
* This abstraction wraps the raw URL generation logic and HTTP calls,
|
|
93
|
+
* allowing higher level services to use clean method calls instead of
|
|
94
|
+
* managing endpoint URLs manually.
|
|
95
|
+
*
|
|
96
|
+
* @typedef {Object} TelegramApis
|
|
97
|
+
*
|
|
98
|
+
* @property {Function} sendMessage
|
|
99
|
+
* Sends a text message to a specific chat.
|
|
100
|
+
*
|
|
101
|
+
* @property {Function} sendButtonsGroup
|
|
102
|
+
* Sends a text message with an inline keyboard button group.
|
|
103
|
+
*
|
|
104
|
+
* @property {Function} sendPhoto
|
|
105
|
+
* Sends a photo with an optional caption.
|
|
106
|
+
*
|
|
107
|
+
* @property {Function} sendVideo
|
|
108
|
+
* Sends a video with an optional caption.
|
|
109
|
+
*
|
|
110
|
+
* @property {Function} sendAudio
|
|
111
|
+
* Sends an audio file with an optional caption.
|
|
112
|
+
*
|
|
113
|
+
* @property {Function} sendDocument
|
|
114
|
+
* Sends a document file with an optional caption.
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates Telegram API methods bound to a specific bot token.
|
|
119
|
+
*
|
|
120
|
+
* @param {Object} params
|
|
121
|
+
* @param {string} params.token
|
|
122
|
+
* Telegram bot token obtained from BotFather.
|
|
123
|
+
*
|
|
124
|
+
* @returns {TelegramApis}
|
|
125
|
+
* An object containing all supported Telegram message sending functions.
|
|
126
|
+
*/
|
|
127
|
+
export const telegramApis = ({ token }) => {
|
|
128
|
+
const APIS = getTelegramApiUrls({ token })
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
/**
|
|
132
|
+
* Sends a text message to a Telegram chat.
|
|
133
|
+
*
|
|
134
|
+
* @param {Object} params
|
|
135
|
+
* @param {string} params.text
|
|
136
|
+
* The message content.
|
|
137
|
+
*
|
|
138
|
+
* @param {number|string} params.chatId
|
|
139
|
+
* Chat identifier where the message should be sent.
|
|
140
|
+
*
|
|
141
|
+
* @param {Array<Object>} [params.entities]
|
|
142
|
+
* Optional entities for formatting (bold, URL, etc).
|
|
143
|
+
*
|
|
144
|
+
* @returns {Promise<import('../../http/http.js').HttpResponse>}
|
|
145
|
+
* Telegram API response.
|
|
146
|
+
*/
|
|
147
|
+
async sendMessage({ text, chatId, entities }) {
|
|
148
|
+
const res = await post({
|
|
149
|
+
url: APIS.SEND_MESSAGE,
|
|
150
|
+
body: {
|
|
151
|
+
chat_id: chatId,
|
|
152
|
+
text,
|
|
153
|
+
entities,
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
return res
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Sends a text message with inline keyboard buttons.
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} params
|
|
163
|
+
* @param {string} params.text
|
|
164
|
+
* The message content.
|
|
165
|
+
*
|
|
166
|
+
* @param {number|string} params.chatId
|
|
167
|
+
* Chat identifier.
|
|
168
|
+
*
|
|
169
|
+
* @param {Array<Array<Object>>} params.options
|
|
170
|
+
* Two dimensional array of inline keyboard button objects.
|
|
171
|
+
*
|
|
172
|
+
* @returns {Promise<import('../../http/http.js').HttpResponse>}
|
|
173
|
+
* Telegram API response.
|
|
174
|
+
*/
|
|
175
|
+
async sendButtonsGroup({ text, chatId, options }) {
|
|
176
|
+
const res = await post({
|
|
177
|
+
url: APIS.SEND_MESSAGE,
|
|
178
|
+
body: {
|
|
179
|
+
chat_id: chatId,
|
|
180
|
+
text,
|
|
181
|
+
reply_markup: {
|
|
182
|
+
inline_keyboard: options,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
return res
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Sends a photo message using an HTTP URL.
|
|
191
|
+
*
|
|
192
|
+
* @param {Object} params
|
|
193
|
+
* @param {number|string} params.chatId
|
|
194
|
+
* Chat identifier.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} params.photo
|
|
197
|
+
* Publicly accessible HTTP URL of the photo.
|
|
198
|
+
*
|
|
199
|
+
* @param {string} [params.caption]
|
|
200
|
+
* Optional caption for the photo.
|
|
201
|
+
*
|
|
202
|
+
* @returns {Promise<import('../../http/http.js').HttpResponse>}
|
|
203
|
+
* Telegram API response.
|
|
204
|
+
*/
|
|
205
|
+
async sendPhoto({ caption, photo, chatId }) {
|
|
206
|
+
const res = await post({
|
|
207
|
+
url: APIS.SEND_PHOTO,
|
|
208
|
+
body: {
|
|
209
|
+
chat_id: chatId,
|
|
210
|
+
caption,
|
|
211
|
+
photo,
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
return res
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Sends a video message using an HTTP URL.
|
|
219
|
+
*
|
|
220
|
+
* @param {Object} params
|
|
221
|
+
* @param {number|string} params.chatId
|
|
222
|
+
* Chat identifier.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} params.video
|
|
225
|
+
* Public video URL.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} [params.caption]
|
|
228
|
+
* Optional caption.
|
|
229
|
+
*
|
|
230
|
+
* @returns {Promise<import('../../http/http.js').HttpResponse>}
|
|
231
|
+
* Telegram API response.
|
|
232
|
+
*/
|
|
233
|
+
async sendVideo({ caption, video, chatId }) {
|
|
234
|
+
const res = await post({
|
|
235
|
+
url: APIS.SEND_VIDEO,
|
|
236
|
+
body: {
|
|
237
|
+
chat_id: chatId,
|
|
238
|
+
caption,
|
|
239
|
+
video,
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
return res
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Sends an audio message using an HTTP URL.
|
|
247
|
+
*
|
|
248
|
+
* @param {Object} params
|
|
249
|
+
* @param {number|string} params.chatId
|
|
250
|
+
* Chat identifier.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} params.audio
|
|
253
|
+
* Public audio URL.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} [params.caption]
|
|
256
|
+
* Optional caption.
|
|
257
|
+
*
|
|
258
|
+
* @returns {Promise<import('../../http/http.js').HttpResponse>}
|
|
259
|
+
* Telegram API response.
|
|
260
|
+
*/
|
|
261
|
+
async sendAudio({ caption, audio, chatId }) {
|
|
262
|
+
const res = await post({
|
|
263
|
+
url: APIS.SEND_AUDIO,
|
|
264
|
+
body: {
|
|
265
|
+
chat_id: chatId,
|
|
266
|
+
caption,
|
|
267
|
+
audio,
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
return res
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Sends a document file using an HTTP URL.
|
|
275
|
+
*
|
|
276
|
+
* @param {Object} params
|
|
277
|
+
* @param {number|string} params.chatId
|
|
278
|
+
* Chat identifier.
|
|
279
|
+
*
|
|
280
|
+
* @param {string} params.document
|
|
281
|
+
* URL to the document file.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} [params.caption]
|
|
284
|
+
* Optional caption.
|
|
285
|
+
*
|
|
286
|
+
* @returns {Promise<import('../../http/http.js').HttpResponse>}
|
|
287
|
+
* Telegram API response.
|
|
288
|
+
*/
|
|
289
|
+
async sendDocument({ caption, document, chatId }) {
|
|
290
|
+
const res = await post({
|
|
291
|
+
url: APIS.SEND_DOCUMENT,
|
|
292
|
+
body: {
|
|
293
|
+
chat_id: chatId,
|
|
294
|
+
caption,
|
|
295
|
+
document,
|
|
296
|
+
},
|
|
297
|
+
})
|
|
298
|
+
return res
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { post } from '../../http/http.js'
|
|
2
|
+
|
|
3
|
+
const WHATSAPP_API_BASE_URL = 'https://graph.facebook.com'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retrieves metadata for a WhatsApp media object.
|
|
7
|
+
*
|
|
8
|
+
* WhatsApp Cloud API does not provide the media file directly via mediaId.
|
|
9
|
+
* Instead, you must first request the metadata for the media item, which
|
|
10
|
+
* includes a temporary download URL. This URL can then be used to retrieve
|
|
11
|
+
* the actual binary content of the file.
|
|
12
|
+
*
|
|
13
|
+
* The returned metadata object typically includes:
|
|
14
|
+
* - `url` A temporary URL that allows downloading the media file
|
|
15
|
+
* - `mime_type` The detected MIME type of the media
|
|
16
|
+
* - `id` The mediaId itself
|
|
17
|
+
*
|
|
18
|
+
* Example output from WhatsApp:
|
|
19
|
+
* {
|
|
20
|
+
* "url": "https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=...",
|
|
21
|
+
* "mime_type": "image/jpeg",
|
|
22
|
+
* "id": "MEDIA_ID"
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* Note:
|
|
26
|
+
* The temporary download URL is short-lived and must be accessed quickly.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} params
|
|
29
|
+
* @param {string} params.mediaId
|
|
30
|
+
* The media identifier received in an incoming WhatsApp webhook message.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} params.token
|
|
33
|
+
* WhatsApp Cloud API access token used for authorization.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} [params.version='v21.0']
|
|
36
|
+
* The WhatsApp Cloud API Graph version to use.
|
|
37
|
+
*
|
|
38
|
+
* @returns {Promise<Object>}
|
|
39
|
+
* Resolves with the media metadata object containing `url`, `mime_type`, and `id`.
|
|
40
|
+
*
|
|
41
|
+
* @throws {Error}
|
|
42
|
+
* If the metadata request fails or WhatsApp responds with a non successful status code.
|
|
43
|
+
*/
|
|
44
|
+
export const getWhatsAppMediaInfo = async ({
|
|
45
|
+
token,
|
|
46
|
+
mediaId,
|
|
47
|
+
version = 'v21.0',
|
|
48
|
+
baseUrl = WHATSAPP_API_BASE_URL,
|
|
49
|
+
}) => {
|
|
50
|
+
const url = `${baseUrl}/${version}/${mediaId}`
|
|
51
|
+
|
|
52
|
+
const res = await fetch(url, {
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${token}`,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
throw new Error(`Failed to retrieve media info: ${res.status}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return res.json()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Downloads WhatsApp media and returns either a Buffer or a Stream,
|
|
67
|
+
* depending on mode.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} params
|
|
70
|
+
* @param {string} params.mediaId
|
|
71
|
+
* @param {string} params.token
|
|
72
|
+
* @param {'buffer' | 'stream'} params.mode
|
|
73
|
+
* @returns {Promise<Buffer|ReadableStream>}
|
|
74
|
+
*/
|
|
75
|
+
export const downloadWhatsAppMedia = async ({
|
|
76
|
+
token,
|
|
77
|
+
mediaId,
|
|
78
|
+
mode = 'buffer',
|
|
79
|
+
}) => {
|
|
80
|
+
const info = await getWhatsAppMediaInfo({ mediaId, token })
|
|
81
|
+
const { url: downloadUrl } = info
|
|
82
|
+
|
|
83
|
+
const res = await fetch(downloadUrl, {
|
|
84
|
+
headers: {
|
|
85
|
+
Authorization: `Bearer ${token}`,
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
throw new Error(`Failed to download media: ${res.status}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (mode === 'stream') {
|
|
94
|
+
return res.body
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const buffer = Buffer.from(await res.arrayBuffer())
|
|
98
|
+
return buffer
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds the WhatsApp Cloud API messages endpoint URL.
|
|
103
|
+
*
|
|
104
|
+
* This function generates the full URL required to send messages
|
|
105
|
+
* through the WhatsApp Cloud API. It concatenates the base Graph API URL,
|
|
106
|
+
* the selected API version, and the phone number ID to produce:
|
|
107
|
+
*
|
|
108
|
+
* https://graph.facebook.com/{version}/{phoneNumberId}/messages
|
|
109
|
+
*
|
|
110
|
+
* The returned URL can then be used for POST requests to send text,
|
|
111
|
+
* media, interactive messages, and more.
|
|
112
|
+
*
|
|
113
|
+
* @param {Object} params
|
|
114
|
+
* @param {string} params.phoneNumberId
|
|
115
|
+
* The WhatsApp phone number ID associated with the business account.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} [params.version='v21.0']
|
|
118
|
+
* The WhatsApp Cloud API version to use. Defaults to the current stable.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} [params.baseUrl=WHATSAPP_API_BASE_URL]
|
|
121
|
+
* Optional override for the Graph API base URL (useful for testing or proxying).
|
|
122
|
+
*
|
|
123
|
+
* @returns {string}
|
|
124
|
+
* Fully resolved WhatsApp Cloud API endpoint URL for sending messages.
|
|
125
|
+
*/
|
|
126
|
+
export const getWhatsAppApiUrls = ({
|
|
127
|
+
phoneNumberId,
|
|
128
|
+
version = 'v21.0',
|
|
129
|
+
baseUrl = WHATSAPP_API_BASE_URL,
|
|
130
|
+
}) => `${baseUrl}/${version}/${phoneNumberId}/messages`
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Factory that creates WhatsApp Cloud API helper methods.
|
|
134
|
+
*
|
|
135
|
+
* This module wraps the WhatsApp Graph API endpoints and exposes
|
|
136
|
+
* high level functions for sending text messages, interactive buttons,
|
|
137
|
+
* images, videos, documents, and audio files.
|
|
138
|
+
*
|
|
139
|
+
* Each returned method builds the correct request format according
|
|
140
|
+
* to the WhatsApp Cloud API specification.
|
|
141
|
+
*
|
|
142
|
+
* @typedef {Object} WhatsAppApis
|
|
143
|
+
*
|
|
144
|
+
* @property {Function} sendMessage
|
|
145
|
+
* Sends a plain text message to an individual WhatsApp user.
|
|
146
|
+
*
|
|
147
|
+
* @property {Function} sendButtonsGroup
|
|
148
|
+
* Sends an interactive message containing buttons.
|
|
149
|
+
*
|
|
150
|
+
* @property {Function} sendPhoto
|
|
151
|
+
* Sends an image message using a public URL.
|
|
152
|
+
*
|
|
153
|
+
* @property {Function} sendVideo
|
|
154
|
+
* Sends a video file using a public URL.
|
|
155
|
+
*
|
|
156
|
+
* @property {Function} sendDocument
|
|
157
|
+
* Sends a document message using a public URL.
|
|
158
|
+
*
|
|
159
|
+
* @property {Function} sendAudio
|
|
160
|
+
* Sends an audio file using a public URL.
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Creates a WhatsApp API client bound to a specific token, phone number ID,
|
|
165
|
+
* and Graph API version.
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} params
|
|
168
|
+
* @param {string} params.token
|
|
169
|
+
* WhatsApp Cloud API access token.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} params.phoneNumberId
|
|
172
|
+
* The phone number ID from Meta Business Manager used for sending messages.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} [params.version='v21.0']
|
|
175
|
+
* WhatsApp Graph API version to use.
|
|
176
|
+
*
|
|
177
|
+
* @returns {WhatsAppApis}
|
|
178
|
+
* A set of helper functions for interacting with the WhatsApp Cloud API.
|
|
179
|
+
*/
|
|
180
|
+
export const whatsappApis = ({ token, phoneNumberId, version = 'v21.0' }) => {
|
|
181
|
+
const url = getWhatsAppApiUrls({ phoneNumberId, version })
|
|
182
|
+
|
|
183
|
+
const headers = {
|
|
184
|
+
Authorization: `Bearer ${token}`,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const bodyBase = {
|
|
188
|
+
recipient_type: 'individual',
|
|
189
|
+
messaging_product: 'whatsapp',
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
/**
|
|
194
|
+
* Sends a text message to an individual WhatsApp user.
|
|
195
|
+
*
|
|
196
|
+
* @param {Object} params
|
|
197
|
+
* @param {string} params.chatId
|
|
198
|
+
* The WhatsApp number (in international format) of the recipient.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} params.text
|
|
201
|
+
* The message content to send.
|
|
202
|
+
*
|
|
203
|
+
* @param {boolean} [params.preview_url=true]
|
|
204
|
+
* Whether URL previews should be generated automatically.
|
|
205
|
+
*
|
|
206
|
+
* @returns {Promise<import('bot-services-libs-shared').HttpResponse>}
|
|
207
|
+
* The API response from the WhatsApp Cloud API.
|
|
208
|
+
*/
|
|
209
|
+
async sendMessage({ chatId, text, preview_url = true }) {
|
|
210
|
+
const textMessage = {
|
|
211
|
+
to: chatId,
|
|
212
|
+
type: 'text',
|
|
213
|
+
text: {
|
|
214
|
+
preview_url,
|
|
215
|
+
body: text,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const res = await post({
|
|
220
|
+
url,
|
|
221
|
+
headers,
|
|
222
|
+
body: {
|
|
223
|
+
...bodyBase,
|
|
224
|
+
...textMessage,
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
return res
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Sends an interactive buttons message to a WhatsApp user.
|
|
233
|
+
*
|
|
234
|
+
* @param {Object} params
|
|
235
|
+
* @param {string} params.chatId
|
|
236
|
+
* The recipient phone number.
|
|
237
|
+
*
|
|
238
|
+
* @param {Object} params.buttonsBody
|
|
239
|
+
* The full interactive object containing button definitions.
|
|
240
|
+
* The caller is expected to pass a structure matching
|
|
241
|
+
* WhatsApp's interactive message schema.
|
|
242
|
+
*
|
|
243
|
+
* @returns {Promise<import('bot-services-libs-shared').HttpResponse>}
|
|
244
|
+
* The API response.
|
|
245
|
+
*/
|
|
246
|
+
async sendButtonsGroup({ chatId, buttonsBody }) {
|
|
247
|
+
const res = await post({
|
|
248
|
+
url,
|
|
249
|
+
headers,
|
|
250
|
+
body: {
|
|
251
|
+
...bodyBase,
|
|
252
|
+
to: chatId,
|
|
253
|
+
type: 'interactive',
|
|
254
|
+
...buttonsBody,
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
return res
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Sends an image message using a public URL.
|
|
263
|
+
*
|
|
264
|
+
* @param {Object} params
|
|
265
|
+
* @param {string} params.chatId
|
|
266
|
+
* The phone number of the recipient.
|
|
267
|
+
*
|
|
268
|
+
* @param {string} params.photo
|
|
269
|
+
* Public URL of the image.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} [params.caption]
|
|
272
|
+
* Optional caption added to the image.
|
|
273
|
+
*
|
|
274
|
+
* @returns {Promise<import('bot-services-libs-shared').HttpResponse>}
|
|
275
|
+
* The API response.
|
|
276
|
+
*/
|
|
277
|
+
async sendPhoto({ caption, photo, chatId }) {
|
|
278
|
+
const res = await post({
|
|
279
|
+
url,
|
|
280
|
+
headers,
|
|
281
|
+
body: {
|
|
282
|
+
...bodyBase,
|
|
283
|
+
to: chatId,
|
|
284
|
+
type: 'image',
|
|
285
|
+
image: {
|
|
286
|
+
link: photo,
|
|
287
|
+
caption,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
return res
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Sends a video message using a public URL.
|
|
297
|
+
*
|
|
298
|
+
* @param {Object} params
|
|
299
|
+
* @param {string} params.chatId
|
|
300
|
+
* Recipient phone number.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} params.video
|
|
303
|
+
* Public URL to the video file.
|
|
304
|
+
*
|
|
305
|
+
* @param {string} [params.caption]
|
|
306
|
+
* Optional caption added to the video.
|
|
307
|
+
*
|
|
308
|
+
* @param {string} [params.filename]
|
|
309
|
+
* Optional filename displayed to the user.
|
|
310
|
+
*
|
|
311
|
+
* @returns {Promise<import('bot-services-libs-shared').HttpResponse>}
|
|
312
|
+
* The API response.
|
|
313
|
+
*/
|
|
314
|
+
async sendVideo({ caption, video, filename, chatId }) {
|
|
315
|
+
const res = await post({
|
|
316
|
+
url,
|
|
317
|
+
headers,
|
|
318
|
+
body: {
|
|
319
|
+
...bodyBase,
|
|
320
|
+
to: chatId,
|
|
321
|
+
type: 'video',
|
|
322
|
+
video: {
|
|
323
|
+
link: video,
|
|
324
|
+
caption,
|
|
325
|
+
...(filename ? { filename } : null),
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
return res
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Sends a document file using a public URL.
|
|
335
|
+
*
|
|
336
|
+
* @param {Object} params
|
|
337
|
+
* @param {string} params.chatId
|
|
338
|
+
* Recipient WhatsApp number.
|
|
339
|
+
*
|
|
340
|
+
* @param {string} params.document
|
|
341
|
+
* Public URL to the document.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} [params.caption]
|
|
344
|
+
* Optional text caption.
|
|
345
|
+
*
|
|
346
|
+
* @param {string} [params.filename]
|
|
347
|
+
* Optional filename shown to the user.
|
|
348
|
+
*
|
|
349
|
+
* @returns {Promise<import('bot-services-libs-shared').HttpResponse>}
|
|
350
|
+
* The API response.
|
|
351
|
+
*/
|
|
352
|
+
async sendDocument({ caption, document, filename, chatId }) {
|
|
353
|
+
const res = await post({
|
|
354
|
+
url,
|
|
355
|
+
headers,
|
|
356
|
+
body: {
|
|
357
|
+
...bodyBase,
|
|
358
|
+
to: chatId,
|
|
359
|
+
type: 'document',
|
|
360
|
+
document: {
|
|
361
|
+
link: document,
|
|
362
|
+
caption,
|
|
363
|
+
...(filename ? { filename } : null),
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
return res
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Sends an audio message using a public URL.
|
|
373
|
+
*
|
|
374
|
+
* @param {Object} params
|
|
375
|
+
* @param {string} params.chatId
|
|
376
|
+
* The phone number of the recipient.
|
|
377
|
+
*
|
|
378
|
+
* @param {string} params.audio
|
|
379
|
+
* Public URL to the audio file.
|
|
380
|
+
*
|
|
381
|
+
* @returns {Promise<import('bot-services-libs-shared').HttpResponse>}
|
|
382
|
+
* The API response.
|
|
383
|
+
*/
|
|
384
|
+
async sendAudio({ audio, chatId }) {
|
|
385
|
+
const res = await post({
|
|
386
|
+
url,
|
|
387
|
+
headers,
|
|
388
|
+
body: {
|
|
389
|
+
...bodyBase,
|
|
390
|
+
to: chatId,
|
|
391
|
+
type: 'audio',
|
|
392
|
+
audio: {
|
|
393
|
+
link: audio,
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
return res
|
|
399
|
+
},
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
getTelegramApiUrls,
|
|
5
|
+
telegramApis,
|
|
6
|
+
} from '../../src/instant-messages/telegram-apis/telegram-apis.js'
|
|
7
|
+
import * as httpModule from '../../src/http/http.js'
|
|
8
|
+
|
|
9
|
+
describe('getTelegramApiUrls', () => {
|
|
10
|
+
it('builds correct Telegram API URLs', () => {
|
|
11
|
+
const token = 'TEST_TOKEN'
|
|
12
|
+
const urls = getTelegramApiUrls({ token })
|
|
13
|
+
|
|
14
|
+
expect(urls.SEND_MESSAGE).toBe(
|
|
15
|
+
`https://api.telegram.org/bot${token}/sendMessage`,
|
|
16
|
+
)
|
|
17
|
+
expect(urls.FORWARD_MESSAGE).toBe(
|
|
18
|
+
`https://api.telegram.org/bot${token}/forwardMessage`,
|
|
19
|
+
)
|
|
20
|
+
expect(urls.SEND_PHOTO).toBe(
|
|
21
|
+
`https://api.telegram.org/bot${token}/sendPhoto`,
|
|
22
|
+
)
|
|
23
|
+
expect(urls.GET_FILE).toBe(`https://api.telegram.org/bot${token}/getFile`)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('supports custom base URL', () => {
|
|
27
|
+
const token = 'X'
|
|
28
|
+
const baseUrl = 'http://localhost:9999'
|
|
29
|
+
const urls = getTelegramApiUrls({ token, telegramBaseUrl: baseUrl })
|
|
30
|
+
|
|
31
|
+
expect(urls.SEND_MESSAGE).toBe(`${baseUrl}/bot${token}/sendMessage`)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('telegramApis', () => {
|
|
36
|
+
const token = 'MOCK_TOKEN'
|
|
37
|
+
let postMock
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
postMock = vi.spyOn(httpModule, 'post').mockResolvedValue({
|
|
41
|
+
ok: true,
|
|
42
|
+
result: 'mock-response',
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('sends text message with correct body', async () => {
|
|
47
|
+
const api = telegramApis({ token })
|
|
48
|
+
await api.sendMessage({ text: 'hello', chatId: 123 })
|
|
49
|
+
|
|
50
|
+
expect(postMock).toHaveBeenCalledWith({
|
|
51
|
+
url: `https://api.telegram.org/bot${token}/sendMessage`,
|
|
52
|
+
body: {
|
|
53
|
+
chat_id: 123,
|
|
54
|
+
text: 'hello',
|
|
55
|
+
entities: undefined,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('sends inline keyboard buttons', async () => {
|
|
61
|
+
const api = telegramApis({ token })
|
|
62
|
+
const options = [[{ text: 'A', callback_data: '1' }]]
|
|
63
|
+
|
|
64
|
+
await api.sendButtonsGroup({
|
|
65
|
+
text: 'Choose',
|
|
66
|
+
chatId: 10,
|
|
67
|
+
options,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(postMock).toHaveBeenCalledWith({
|
|
71
|
+
url: `https://api.telegram.org/bot${token}/sendMessage`,
|
|
72
|
+
body: {
|
|
73
|
+
chat_id: 10,
|
|
74
|
+
text: 'Choose',
|
|
75
|
+
reply_markup: {
|
|
76
|
+
inline_keyboard: options,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('sends a photo with caption', async () => {
|
|
83
|
+
const api = telegramApis({ token })
|
|
84
|
+
|
|
85
|
+
await api.sendPhoto({
|
|
86
|
+
chatId: 77,
|
|
87
|
+
photo: 'http://photo.jpg',
|
|
88
|
+
caption: 'hi',
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
expect(postMock).toHaveBeenCalledWith({
|
|
92
|
+
url: `https://api.telegram.org/bot${token}/sendPhoto`,
|
|
93
|
+
body: {
|
|
94
|
+
chat_id: 77,
|
|
95
|
+
caption: 'hi',
|
|
96
|
+
photo: 'http://photo.jpg',
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('sends a video', async () => {
|
|
102
|
+
const api = telegramApis({ token })
|
|
103
|
+
|
|
104
|
+
await api.sendVideo({
|
|
105
|
+
chatId: 55,
|
|
106
|
+
video: 'http://video.mp4',
|
|
107
|
+
caption: 'watch this',
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expect(postMock).toHaveBeenCalledWith({
|
|
111
|
+
url: `https://api.telegram.org/bot${token}/sendVideo`,
|
|
112
|
+
body: {
|
|
113
|
+
chat_id: 55,
|
|
114
|
+
caption: 'watch this',
|
|
115
|
+
video: 'http://video.mp4',
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('sends an audio file', async () => {
|
|
121
|
+
const api = telegramApis({ token })
|
|
122
|
+
|
|
123
|
+
await api.sendAudio({
|
|
124
|
+
chatId: 200,
|
|
125
|
+
audio: 'http://audio.mp3',
|
|
126
|
+
caption: 'listen',
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
expect(postMock).toHaveBeenCalledWith({
|
|
130
|
+
url: `https://api.telegram.org/bot${token}/sendAudio`,
|
|
131
|
+
body: {
|
|
132
|
+
chat_id: 200,
|
|
133
|
+
caption: 'listen',
|
|
134
|
+
audio: 'http://audio.mp3',
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('sends a document', async () => {
|
|
140
|
+
const api = telegramApis({ token })
|
|
141
|
+
|
|
142
|
+
await api.sendDocument({
|
|
143
|
+
chatId: 999,
|
|
144
|
+
document: 'http://file.pdf',
|
|
145
|
+
caption: 'file',
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
expect(postMock).toHaveBeenCalledWith({
|
|
149
|
+
url: `https://api.telegram.org/bot${token}/sendDocument`,
|
|
150
|
+
body: {
|
|
151
|
+
chat_id: 999,
|
|
152
|
+
caption: 'file',
|
|
153
|
+
document: 'http://file.pdf',
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
whatsappApis,
|
|
6
|
+
getWhatsAppApiUrls,
|
|
7
|
+
getWhatsAppMediaInfo,
|
|
8
|
+
downloadWhatsAppMedia,
|
|
9
|
+
} from '../../src/instant-messages/whatsapp-apis/whatsapp-apis.js'
|
|
10
|
+
|
|
11
|
+
import * as httpModule from '../../src/http/http.js'
|
|
12
|
+
|
|
13
|
+
global.fetch = vi.fn()
|
|
14
|
+
|
|
15
|
+
describe('getWhatsAppMediaInfo', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
fetch.mockReset()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns media metadata correctly', async () => {
|
|
21
|
+
const mockResponse = {
|
|
22
|
+
url: 'https://cdn.whatsapp.com/file.jpg',
|
|
23
|
+
mime_type: 'image/jpeg',
|
|
24
|
+
id: 'ABC123',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fetch.mockResolvedValue({
|
|
28
|
+
ok: true,
|
|
29
|
+
json: async () => mockResponse,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const result = await getWhatsAppMediaInfo({
|
|
33
|
+
mediaId: 'ABC123',
|
|
34
|
+
token: 'TOKEN',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
38
|
+
'https://graph.facebook.com/v21.0/ABC123',
|
|
39
|
+
{
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: 'Bearer TOKEN',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(result).toEqual(mockResponse)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('throws on non successful response', async () => {
|
|
50
|
+
fetch.mockResolvedValue({
|
|
51
|
+
ok: false,
|
|
52
|
+
status: 403,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
await expect(
|
|
56
|
+
getWhatsAppMediaInfo({
|
|
57
|
+
mediaId: 'MEDIA_ID',
|
|
58
|
+
token: 'TOKEN',
|
|
59
|
+
}),
|
|
60
|
+
).rejects.toThrow('Failed to retrieve media info: 403')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('downloadWhatsAppMedia', () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
fetch.mockReset()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('downloads media as buffer', async () => {
|
|
70
|
+
const mockMetadata = {
|
|
71
|
+
url: 'https://cdn.whatsapp.com/file.jpg',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const mockBinary = new Uint8Array([10, 20, 30])
|
|
75
|
+
|
|
76
|
+
fetch
|
|
77
|
+
.mockResolvedValueOnce({
|
|
78
|
+
ok: true,
|
|
79
|
+
json: async () => mockMetadata,
|
|
80
|
+
})
|
|
81
|
+
.mockResolvedValueOnce({
|
|
82
|
+
ok: true,
|
|
83
|
+
arrayBuffer: async () => mockBinary.buffer,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const buffer = await downloadWhatsAppMedia({
|
|
87
|
+
token: 'TOKEN',
|
|
88
|
+
mediaId: 'MEDIA_ID',
|
|
89
|
+
mode: 'buffer',
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
expect(Buffer.isBuffer(buffer)).toBe(true)
|
|
93
|
+
expect(buffer.equals(Buffer.from(mockBinary))).toBe(true)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('returns stream when mode=stream', async () => {
|
|
97
|
+
const mockStream = { fake: 'stream' }
|
|
98
|
+
|
|
99
|
+
fetch
|
|
100
|
+
.mockResolvedValueOnce({
|
|
101
|
+
ok: true,
|
|
102
|
+
json: async () => ({ url: 'http://cdn.com/x' }),
|
|
103
|
+
})
|
|
104
|
+
.mockResolvedValueOnce({
|
|
105
|
+
ok: true,
|
|
106
|
+
body: mockStream,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const stream = await downloadWhatsAppMedia({
|
|
110
|
+
token: 'TOKEN',
|
|
111
|
+
mediaId: 'ID',
|
|
112
|
+
mode: 'stream',
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(stream).toBe(mockStream)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('getWhatsAppApiUrls', () => {
|
|
120
|
+
it('generates correct messages URL', () => {
|
|
121
|
+
const url = getWhatsAppApiUrls({
|
|
122
|
+
phoneNumberId: '12345',
|
|
123
|
+
version: 'v21.0',
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect(url).toBe('https://graph.facebook.com/v21.0/12345/messages')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('supports custom base URL', () => {
|
|
130
|
+
const url = getWhatsAppApiUrls({
|
|
131
|
+
phoneNumberId: '99',
|
|
132
|
+
baseUrl: 'http://localhost:3000',
|
|
133
|
+
version: 'v21.0',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
expect(url).toBe('http://localhost:3000/v21.0/99/messages')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('whatsappApis', () => {
|
|
141
|
+
let postMock
|
|
142
|
+
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
postMock = vi
|
|
145
|
+
.spyOn(httpModule, 'post')
|
|
146
|
+
.mockResolvedValue({ ok: true, data: 'mock-response' })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const token = 'TOKEN'
|
|
150
|
+
const phoneNumberId = '111'
|
|
151
|
+
|
|
152
|
+
const baseUrl = 'https://graph.facebook.com/v21.0/111/messages'
|
|
153
|
+
|
|
154
|
+
it('sendMessage sends correct payload', async () => {
|
|
155
|
+
const api = whatsappApis({ token, phoneNumberId })
|
|
156
|
+
|
|
157
|
+
await api.sendMessage({
|
|
158
|
+
chatId: '9725000000',
|
|
159
|
+
text: 'Hello',
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
expect(postMock).toHaveBeenCalledWith({
|
|
163
|
+
url: baseUrl,
|
|
164
|
+
headers: { Authorization: 'Bearer TOKEN' },
|
|
165
|
+
body: {
|
|
166
|
+
recipient_type: 'individual',
|
|
167
|
+
messaging_product: 'whatsapp',
|
|
168
|
+
to: '9725000000',
|
|
169
|
+
type: 'text',
|
|
170
|
+
text: {
|
|
171
|
+
preview_url: true,
|
|
172
|
+
body: 'Hello',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('sendPhoto sends correct body', async () => {
|
|
179
|
+
const api = whatsappApis({ token, phoneNumberId })
|
|
180
|
+
|
|
181
|
+
await api.sendPhoto({
|
|
182
|
+
chatId: '1',
|
|
183
|
+
photo: 'http://img.jpg',
|
|
184
|
+
caption: 'Hi',
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
expect(postMock).toHaveBeenCalledWith({
|
|
188
|
+
url: baseUrl,
|
|
189
|
+
headers: { Authorization: 'Bearer TOKEN' },
|
|
190
|
+
body: {
|
|
191
|
+
recipient_type: 'individual',
|
|
192
|
+
messaging_product: 'whatsapp',
|
|
193
|
+
to: '1',
|
|
194
|
+
type: 'image',
|
|
195
|
+
image: {
|
|
196
|
+
link: 'http://img.jpg',
|
|
197
|
+
caption: 'Hi',
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('sendVideo sends filename when provided', async () => {
|
|
204
|
+
const api = whatsappApis({ token, phoneNumberId })
|
|
205
|
+
|
|
206
|
+
await api.sendVideo({
|
|
207
|
+
chatId: '77',
|
|
208
|
+
video: 'http://video.mp4',
|
|
209
|
+
caption: 'watch',
|
|
210
|
+
filename: 'clip.mp4',
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
expect(postMock).toHaveBeenCalledWith({
|
|
214
|
+
url: baseUrl,
|
|
215
|
+
headers: { Authorization: 'Bearer TOKEN' },
|
|
216
|
+
body: {
|
|
217
|
+
recipient_type: 'individual',
|
|
218
|
+
messaging_product: 'whatsapp',
|
|
219
|
+
to: '77',
|
|
220
|
+
type: 'video',
|
|
221
|
+
video: {
|
|
222
|
+
link: 'http://video.mp4',
|
|
223
|
+
caption: 'watch',
|
|
224
|
+
filename: 'clip.mp4',
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('sendDocument works without filename', async () => {
|
|
231
|
+
const api = whatsappApis({ token, phoneNumberId })
|
|
232
|
+
|
|
233
|
+
await api.sendDocument({
|
|
234
|
+
chatId: '33',
|
|
235
|
+
document: 'http://file.pdf',
|
|
236
|
+
caption: 'doc',
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
expect(postMock).toHaveBeenCalledWith({
|
|
240
|
+
url: baseUrl,
|
|
241
|
+
headers: { Authorization: 'Bearer TOKEN' },
|
|
242
|
+
body: {
|
|
243
|
+
recipient_type: 'individual',
|
|
244
|
+
messaging_product: 'whatsapp',
|
|
245
|
+
to: '33',
|
|
246
|
+
type: 'document',
|
|
247
|
+
document: {
|
|
248
|
+
link: 'http://file.pdf',
|
|
249
|
+
caption: 'doc',
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('sendAudio sends correct structure', async () => {
|
|
256
|
+
const api = whatsappApis({ token, phoneNumberId })
|
|
257
|
+
|
|
258
|
+
await api.sendAudio({
|
|
259
|
+
chatId: '555',
|
|
260
|
+
audio: 'http://a.mp3',
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
expect(postMock).toHaveBeenCalledWith({
|
|
264
|
+
url: baseUrl,
|
|
265
|
+
headers: { Authorization: 'Bearer TOKEN' },
|
|
266
|
+
body: {
|
|
267
|
+
recipient_type: 'individual',
|
|
268
|
+
messaging_product: 'whatsapp',
|
|
269
|
+
to: '555',
|
|
270
|
+
type: 'audio',
|
|
271
|
+
audio: {
|
|
272
|
+
link: 'http://a.mp3',
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export function getTelegramApiUrls({
|
|
2
|
+
token,
|
|
3
|
+
telegramBaseUrl,
|
|
4
|
+
}: {
|
|
5
|
+
token: string
|
|
6
|
+
telegramBaseUrl?: string
|
|
7
|
+
}): TelegramApiUrls
|
|
8
|
+
export function telegramApis({ token }: { token: string }): TelegramApis
|
|
9
|
+
/**
|
|
10
|
+
* Builds a full set of Telegram Bot API endpoint URLs
|
|
11
|
+
* based on the provided bot token and an optional base URL.
|
|
12
|
+
*
|
|
13
|
+
* This helper centralizes all Telegram endpoints used by the system,
|
|
14
|
+
* making it easier to mock, override, or customize for testing environments.
|
|
15
|
+
*/
|
|
16
|
+
export type TelegramApiUrls = {
|
|
17
|
+
/**
|
|
18
|
+
* URL to send a text message to a chat using the Telegram Bot API.
|
|
19
|
+
*/
|
|
20
|
+
SEND_MESSAGE: string
|
|
21
|
+
/**
|
|
22
|
+
* URL to forward an existing message from one chat to another.
|
|
23
|
+
*/
|
|
24
|
+
FORWARD_MESSAGE: string
|
|
25
|
+
/**
|
|
26
|
+
* URL to send a photo to a chat.
|
|
27
|
+
*/
|
|
28
|
+
SEND_PHOTO: string
|
|
29
|
+
/**
|
|
30
|
+
* URL to send an audio file.
|
|
31
|
+
*/
|
|
32
|
+
SEND_AUDIO: string
|
|
33
|
+
/**
|
|
34
|
+
* URL to send files such as PDF, DOC, ZIP, and others supported by Telegram.
|
|
35
|
+
*/
|
|
36
|
+
SEND_DOCUMENT: string
|
|
37
|
+
/**
|
|
38
|
+
* URL to send a sticker.
|
|
39
|
+
*/
|
|
40
|
+
SEND_STICKER: string
|
|
41
|
+
/**
|
|
42
|
+
* URL to send a video file.
|
|
43
|
+
*/
|
|
44
|
+
SEND_VIDEO: string
|
|
45
|
+
/**
|
|
46
|
+
* URL to send a voice note.
|
|
47
|
+
*/
|
|
48
|
+
SEND_VOICE: string
|
|
49
|
+
/**
|
|
50
|
+
* URL to send a geolocation point.
|
|
51
|
+
*/
|
|
52
|
+
SEND_LOCATION: string
|
|
53
|
+
/**
|
|
54
|
+
* URL to send a chat action (typing, uploading photo, etc).
|
|
55
|
+
*/
|
|
56
|
+
SEND_CHAT_ACTION: string
|
|
57
|
+
/**
|
|
58
|
+
* URL to retrieve the profile photos of a specific user.
|
|
59
|
+
*/
|
|
60
|
+
GET_USER_PROFILE_PHOTOS: string
|
|
61
|
+
/**
|
|
62
|
+
* URL to poll for new updates (not used when using webhooks).
|
|
63
|
+
*/
|
|
64
|
+
GET_UPDATES: string
|
|
65
|
+
/**
|
|
66
|
+
* URL to fetch a file path for downloading a file uploaded to Telegram servers.
|
|
67
|
+
*/
|
|
68
|
+
GET_FILE: string
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Factory that creates a set of high level Telegram Bot API helper methods.
|
|
72
|
+
*
|
|
73
|
+
* Each method sends a specific type of message (text, photo, video, document)
|
|
74
|
+
* through the Telegram Bot API using the provided bot token.
|
|
75
|
+
*
|
|
76
|
+
* This abstraction wraps the raw URL generation logic and HTTP calls,
|
|
77
|
+
* allowing higher level services to use clean method calls instead of
|
|
78
|
+
* managing endpoint URLs manually.
|
|
79
|
+
*/
|
|
80
|
+
export type TelegramApis = {
|
|
81
|
+
/**
|
|
82
|
+
* Sends a text message to a specific chat.
|
|
83
|
+
*/
|
|
84
|
+
sendMessage: Function
|
|
85
|
+
/**
|
|
86
|
+
* Sends a text message with an inline keyboard button group.
|
|
87
|
+
*/
|
|
88
|
+
sendButtonsGroup: Function
|
|
89
|
+
/**
|
|
90
|
+
* Sends a photo with an optional caption.
|
|
91
|
+
*/
|
|
92
|
+
sendPhoto: Function
|
|
93
|
+
/**
|
|
94
|
+
* Sends a video with an optional caption.
|
|
95
|
+
*/
|
|
96
|
+
sendVideo: Function
|
|
97
|
+
/**
|
|
98
|
+
* Sends an audio file with an optional caption.
|
|
99
|
+
*/
|
|
100
|
+
sendAudio: Function
|
|
101
|
+
/**
|
|
102
|
+
* Sends a document file with an optional caption.
|
|
103
|
+
*/
|
|
104
|
+
sendDocument: Function
|
|
105
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function getWhatsAppMediaInfo({
|
|
2
|
+
token,
|
|
3
|
+
mediaId,
|
|
4
|
+
version,
|
|
5
|
+
baseUrl,
|
|
6
|
+
}: {
|
|
7
|
+
mediaId: string
|
|
8
|
+
token: string
|
|
9
|
+
version?: string
|
|
10
|
+
}): Promise<any>
|
|
11
|
+
export function downloadWhatsAppMedia({
|
|
12
|
+
token,
|
|
13
|
+
mediaId,
|
|
14
|
+
mode,
|
|
15
|
+
}: {
|
|
16
|
+
mediaId: string
|
|
17
|
+
token: string
|
|
18
|
+
mode: 'buffer' | 'stream'
|
|
19
|
+
}): Promise<Buffer | ReadableStream>
|
|
20
|
+
export function getWhatsAppApiUrls({
|
|
21
|
+
phoneNumberId,
|
|
22
|
+
version,
|
|
23
|
+
baseUrl,
|
|
24
|
+
}: {
|
|
25
|
+
phoneNumberId: string
|
|
26
|
+
version?: string
|
|
27
|
+
baseUrl?: string
|
|
28
|
+
}): string
|
|
29
|
+
export function whatsappApis({
|
|
30
|
+
token,
|
|
31
|
+
phoneNumberId,
|
|
32
|
+
version,
|
|
33
|
+
}: {
|
|
34
|
+
token: string
|
|
35
|
+
phoneNumberId: string
|
|
36
|
+
version?: string
|
|
37
|
+
}): WhatsAppApis
|
|
38
|
+
/**
|
|
39
|
+
* Factory that creates WhatsApp Cloud API helper methods.
|
|
40
|
+
*
|
|
41
|
+
* This module wraps the WhatsApp Graph API endpoints and exposes
|
|
42
|
+
* high level functions for sending text messages, interactive buttons,
|
|
43
|
+
* images, videos, documents, and audio files.
|
|
44
|
+
*
|
|
45
|
+
* Each returned method builds the correct request format according
|
|
46
|
+
* to the WhatsApp Cloud API specification.
|
|
47
|
+
*/
|
|
48
|
+
export type WhatsAppApis = {
|
|
49
|
+
/**
|
|
50
|
+
* Sends a plain text message to an individual WhatsApp user.
|
|
51
|
+
*/
|
|
52
|
+
sendMessage: Function
|
|
53
|
+
/**
|
|
54
|
+
* Sends an interactive message containing buttons.
|
|
55
|
+
*/
|
|
56
|
+
sendButtonsGroup: Function
|
|
57
|
+
/**
|
|
58
|
+
* Sends an image message using a public URL.
|
|
59
|
+
*/
|
|
60
|
+
sendPhoto: Function
|
|
61
|
+
/**
|
|
62
|
+
* Sends a video file using a public URL.
|
|
63
|
+
*/
|
|
64
|
+
sendVideo: Function
|
|
65
|
+
/**
|
|
66
|
+
* Sends a document message using a public URL.
|
|
67
|
+
*/
|
|
68
|
+
sendDocument: Function
|
|
69
|
+
/**
|
|
70
|
+
* Sends an audio file using a public URL.
|
|
71
|
+
*/
|
|
72
|
+
sendAudio: Function
|
|
73
|
+
}
|