@open-wa/wa-automate 4.54.6 → 4.55.0
Sign up to get free protection for your applications and to get access to all the features.
- package/bin/config-schema.json +1 -1
- package/bin/oas-type-schemas.json +1 -1
- package/dist/api/Client.d.ts +1 -1
- package/dist/api/Client.js +1 -1
- package/dist/api/model/config.d.ts +5 -0
- package/dist/cli/integrations/chatwoot.d.ts +42 -0
- package/dist/cli/integrations/chatwoot.js +515 -302
- package/dist/controllers/initializer.d.ts +1 -1
- package/dist/controllers/initializer.js +4 -1
- package/package.json +1 -1
@@ -18,6 +18,7 @@ const __1 = require("../..");
|
|
18
18
|
const axios_1 = __importDefault(require("axios"));
|
19
19
|
const form_data_1 = __importDefault(require("form-data"));
|
20
20
|
const mime_types_1 = __importDefault(require("mime-types"));
|
21
|
+
const tools_1 = require("../../utils/tools");
|
21
22
|
const contactReg = {
|
22
23
|
//WID : chatwoot contact ID
|
23
24
|
"example@c.us": "1"
|
@@ -29,18 +30,53 @@ const convoReg = {
|
|
29
30
|
const ignoreMap = {
|
30
31
|
"example_message_id": true
|
31
32
|
};
|
33
|
+
let chatwootClient;
|
32
34
|
exports.chatwoot_webhook_check_event_name = "cli.integrations.chatwoot.check";
|
35
|
+
function parseIdAndScore(input) {
|
36
|
+
const regex = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}):([1-5])$/;
|
37
|
+
const match = regex.exec(input);
|
38
|
+
if (match) {
|
39
|
+
return {
|
40
|
+
id: match[1],
|
41
|
+
score: parseInt(match[2], 10)
|
42
|
+
};
|
43
|
+
}
|
44
|
+
else {
|
45
|
+
return null;
|
46
|
+
}
|
47
|
+
}
|
33
48
|
const chatwootMiddleware = (cliConfig, client) => {
|
34
49
|
return (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
35
50
|
const processMesssage = () => __awaiter(void 0, void 0, void 0, function* () {
|
51
|
+
var _a, _b, _c, _d;
|
36
52
|
const promises = [];
|
37
53
|
const { body } = req;
|
38
54
|
if (!body)
|
39
55
|
return;
|
56
|
+
if (body.event == "conversation_status_changed" && body.status == "resolved") {
|
57
|
+
__1.log.info("Trying to send CSAT");
|
58
|
+
/**
|
59
|
+
* CSAT requested
|
60
|
+
*/
|
61
|
+
let basicCsatMsgData = (_a = body.messages) === null || _a === void 0 ? void 0 : _a.find(m => (m === null || m === void 0 ? void 0 : m.content_type) === "input_csat");
|
62
|
+
if (!basicCsatMsgData) {
|
63
|
+
/**
|
64
|
+
* CSAT Missing from this webhook. Try to find it by getting all messages and filtering by csat and with a ts of more than the webhook event - 5s (just in case the csat was somehow sent before the convo was "resolved")
|
65
|
+
*/
|
66
|
+
const msgs = yield chatwootClient.getAllInboxMessages(body.id);
|
67
|
+
basicCsatMsgData = msgs.find(m => m.content_type === 'input_csat' && m.created_at > (body.timestamp - 5));
|
68
|
+
}
|
69
|
+
if (!basicCsatMsgData)
|
70
|
+
return;
|
71
|
+
const _to = `${(((_b = body === null || body === void 0 ? void 0 : body.custom_attributes) === null || _b === void 0 ? void 0 : _b.wanumber) || (((_d = (_c = body.meta) === null || _c === void 0 ? void 0 : _c.sender) === null || _d === void 0 ? void 0 : _d.phone_number) || "").replace('+', '')).replace('@c.us', '')}@c.us`;
|
72
|
+
if (_to)
|
73
|
+
promises.push(chatwootClient.sendCSAT(basicCsatMsgData, _to));
|
74
|
+
}
|
40
75
|
if (!body.conversation)
|
41
76
|
return;
|
42
|
-
const m = body.conversation.messages[0];
|
43
77
|
const contact = (body.conversation.meta.sender.phone_number || "").replace('+', '');
|
78
|
+
const to = `${contact}@c.us`;
|
79
|
+
const m = body.conversation.messages[0];
|
44
80
|
if (body.message_type === "incoming" ||
|
45
81
|
body.private ||
|
46
82
|
body.event !== "message_created" ||
|
@@ -48,7 +84,6 @@ const chatwootMiddleware = (cliConfig, client) => {
|
|
48
84
|
!contact)
|
49
85
|
return;
|
50
86
|
const { attachments, content } = m;
|
51
|
-
const to = `${contact}@c.us`;
|
52
87
|
if (!convoReg[to])
|
53
88
|
convoReg[to] = body.conversation.id;
|
54
89
|
if ((attachments === null || attachments === void 0 ? void 0 : attachments.length) > 0) {
|
@@ -101,7 +136,6 @@ const chatwootMiddleware = (cliConfig, client) => {
|
|
101
136
|
res.status(200).send(processAndSendResult);
|
102
137
|
}
|
103
138
|
catch (error) {
|
104
|
-
console.log("🚀 ~ file: chatwoot.ts ~ line 62 ~ return ~ error", error);
|
105
139
|
res.status(400).send(error);
|
106
140
|
}
|
107
141
|
return;
|
@@ -109,329 +143,508 @@ const chatwootMiddleware = (cliConfig, client) => {
|
|
109
143
|
};
|
110
144
|
exports.chatwootMiddleware = chatwootMiddleware;
|
111
145
|
const setupChatwootOutgoingMessageHandler = (cliConfig, client) => __awaiter(void 0, void 0, void 0, function* () {
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
146
|
+
chatwootClient = new ChatwootClient(cliConfig, client);
|
147
|
+
yield chatwootClient.init();
|
148
|
+
return;
|
149
|
+
});
|
150
|
+
exports.setupChatwootOutgoingMessageHandler = setupChatwootOutgoingMessageHandler;
|
151
|
+
class ChatwootClient {
|
152
|
+
constructor(cliConfig, client) {
|
153
|
+
const u = cliConfig.chatwootUrl; //e.g `"localhost:3000/api/v1/accounts/3"
|
154
|
+
this.api_access_token = cliConfig.chatwootApiAccessToken;
|
155
|
+
const _u = new URL(u);
|
156
|
+
this.origin = _u.origin;
|
157
|
+
this.port = Number(_u.port || 80);
|
158
|
+
this.client = client;
|
159
|
+
this.expectedSelfWebhookUrl = cliConfig.apiHost ? `${cliConfig.apiHost}/chatwoot ` : `${cliConfig.host.includes('http') ? '' : `http${cliConfig.https || (cliConfig.cert && cliConfig.privkey) ? 's' : ''}://`}${cliConfig.host}:${cliConfig.port}/chatwoot `;
|
160
|
+
this.expectedSelfWebhookUrl = this.expectedSelfWebhookUrl.trim();
|
161
|
+
this.key = cliConfig.key;
|
162
|
+
if (cliConfig.key)
|
163
|
+
this.expectedSelfWebhookUrl = `${this.expectedSelfWebhookUrl}?api_key=${cliConfig.key}`;
|
164
|
+
const [accountId, inboxId] = (u.match(/\/(app|(api\/v1))\/accounts\/\d*(\/(inbox|inboxes)\/\d*)?/g) || [''])[0].split('/').filter(Number);
|
165
|
+
this.inboxId = inboxId || u.match(/inboxes\/\d*/g) && u.match(/inboxes\/\d*/g)[0].replace('inboxes/', '');
|
166
|
+
this.accountId = accountId;
|
167
|
+
this.forceUpdateCwWebhook = cliConfig.forceUpdateCwWebhook;
|
168
|
+
}
|
169
|
+
cwReq(method, path, data, _headers) {
|
170
|
+
return __awaiter(this, void 0, void 0, function* () {
|
171
|
+
const { origin, accountId, api_access_token } = this;
|
172
|
+
const url = `${origin}/api/v1/accounts/${accountId}/${path}`.replace('app.bentonow.com', 'chat.bentonow.com');
|
173
|
+
const response = yield (0, axios_1.default)({
|
174
|
+
method,
|
175
|
+
data,
|
176
|
+
url,
|
177
|
+
headers: Object.assign({ api_access_token }, _headers)
|
178
|
+
}).catch(error => {
|
179
|
+
var _a, _b;
|
180
|
+
__1.log.error(`CW REQ ERROR: ${(_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status} ${(_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.message}`, error === null || error === void 0 ? void 0 : error.toJSON());
|
181
|
+
throw error;
|
182
|
+
});
|
183
|
+
__1.log.info(`CW REQUEST: ${response.status} ${method} ${url} ${JSON.stringify(data)}`);
|
184
|
+
return response;
|
185
|
+
});
|
186
|
+
}
|
127
187
|
/**
|
128
|
-
*
|
188
|
+
* Ensures the chatwoot integration is setup properly.
|
129
189
|
*/
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
if (!inboxId) {
|
139
|
-
__1.log.info(`CHATWOOT INTEGRATION: inbox ID missing. Attempting to find correct inbox....`);
|
140
|
-
/**
|
141
|
-
* Find the inbox with the correct setup.
|
142
|
-
*/
|
143
|
-
const inboxArray = (yield axios_1.default.get(`${origin}/api/v1/accounts/${accountId}/inboxes`, { headers: { api_access_token } })).data.payload;
|
144
|
-
const possibleInbox = inboxArray.find(inbox => { var _a; return ((_a = inbox === null || inbox === void 0 ? void 0 : inbox.additional_attributes) === null || _a === void 0 ? void 0 : _a.hostAccountNumber) === accountNumber; });
|
145
|
-
if (possibleInbox) {
|
146
|
-
__1.log.info(`CHATWOOT INTEGRATION: found inbox: ${JSON.stringify(possibleInbox)}`);
|
147
|
-
__1.log.info(`CHATWOOT INTEGRATION: found inbox id: ${possibleInbox.channel_id}`);
|
148
|
-
inboxId = possibleInbox.channel_id;
|
149
|
-
}
|
150
|
-
else {
|
151
|
-
__1.log.info(`CHATWOOT INTEGRATION: inbox not found. Attempting to create inbox....`);
|
190
|
+
init() {
|
191
|
+
return __awaiter(this, void 0, void 0, function* () {
|
192
|
+
__1.log.info(`Setting up chatwoot integration: ${this.u}`);
|
193
|
+
const accountNumber = this.accountNumber = yield this.client.getHostNumber();
|
194
|
+
const { api_access_token, origin } = this;
|
195
|
+
let { inboxId, accountId } = this;
|
196
|
+
const proms = [];
|
197
|
+
// const accountId = u.match(/accounts\/\d*/g) && u.match(/accounts\/\d*/g)[0].replace('accounts/', '')
|
152
198
|
/**
|
153
|
-
*
|
199
|
+
* Is the inbox and or account id undefined??
|
154
200
|
*/
|
155
|
-
|
156
|
-
|
201
|
+
if (!accountId) {
|
202
|
+
__1.log.info(`CHATWOOT INTEGRATION: account ID missing. Attempting to infer from access token....`);
|
203
|
+
/**
|
204
|
+
* If the account ID is undefined then get the account ID from the access_token
|
205
|
+
*/
|
206
|
+
accountId = (yield axios_1.default.get(`${origin}/api/v1/profile`, { headers: { api_access_token } })).data.account_id;
|
207
|
+
__1.log.info(`CHATWOOT INTEGRATION: Got account ID: ${accountId}`);
|
208
|
+
this.accountId = accountId;
|
209
|
+
}
|
210
|
+
if (!inboxId) {
|
211
|
+
__1.log.info(`CHATWOOT INTEGRATION: inbox ID missing. Attempting to find correct inbox....`);
|
212
|
+
/**
|
213
|
+
* Find the inbox with the correct setup.
|
214
|
+
*/
|
215
|
+
const inboxArray = (yield axios_1.default.get(`${origin}/api/v1/accounts/${accountId}/inboxes`, { headers: { api_access_token } })).data.payload;
|
216
|
+
const possibleInbox = inboxArray.find(inbox => { var _a; return ((_a = inbox === null || inbox === void 0 ? void 0 : inbox.additional_attributes) === null || _a === void 0 ? void 0 : _a.hostAccountNumber) === accountNumber; });
|
217
|
+
if (possibleInbox) {
|
218
|
+
__1.log.info(`CHATWOOT INTEGRATION: found inbox: ${JSON.stringify(possibleInbox)}`);
|
219
|
+
__1.log.info(`CHATWOOT INTEGRATION: found inbox id: ${possibleInbox.id}`);
|
220
|
+
inboxId = possibleInbox.id;
|
221
|
+
this.inboxId = inboxId;
|
222
|
+
}
|
223
|
+
else {
|
224
|
+
__1.log.info(`CHATWOOT INTEGRATION: inbox not found. Attempting to create inbox....`);
|
225
|
+
/**
|
226
|
+
* Create inbox
|
227
|
+
*/
|
228
|
+
const { data: new_inbox } = (yield axios_1.default.post(`${origin}/api/v1/accounts/${accountId}/inboxes`, {
|
229
|
+
"name": `open-wa-${accountNumber}`,
|
230
|
+
"channel": {
|
231
|
+
"phone_number": `${accountNumber}`,
|
232
|
+
"type": "api",
|
233
|
+
"webhook_url": this.expectedSelfWebhookUrl,
|
234
|
+
"additional_attributes": {
|
235
|
+
"sessionId": this.client.getSessionId(),
|
236
|
+
"hostAccountNumber": `${accountNumber}`
|
237
|
+
}
|
238
|
+
}
|
239
|
+
}, { headers: { api_access_token } }));
|
240
|
+
inboxId = new_inbox.id;
|
241
|
+
__1.log.info(`CHATWOOT INTEGRATION: inbox created. id: ${inboxId}`);
|
242
|
+
this.inboxId = inboxId;
|
243
|
+
}
|
244
|
+
}
|
245
|
+
let { data: get_inbox } = yield this.cwReq('get', `inboxes/${inboxId}`);
|
246
|
+
/**
|
247
|
+
* Update the webhook
|
248
|
+
*/
|
249
|
+
const updatePayload = {
|
157
250
|
"channel": {
|
158
|
-
"phone_number": `${accountNumber}`,
|
159
|
-
"type": "api",
|
160
|
-
"webhook_url": expectedSelfWebhookUrl,
|
161
251
|
"additional_attributes": {
|
162
|
-
"sessionId": client.getSessionId(),
|
163
|
-
"hostAccountNumber": `${accountNumber}
|
252
|
+
"sessionId": this.client.getSessionId(),
|
253
|
+
"hostAccountNumber": `${this.accountNumber}`,
|
254
|
+
"instanceId": `${this.client.getInstanceId()}`
|
164
255
|
}
|
165
256
|
}
|
166
|
-
}
|
167
|
-
|
168
|
-
|
169
|
-
|
257
|
+
};
|
258
|
+
if (this.forceUpdateCwWebhook)
|
259
|
+
updatePayload.channel['webhook_url'] = this.expectedSelfWebhookUrl;
|
260
|
+
const updateInboxPromise = this.cwReq('patch', `inboxes/${this.inboxId}`, updatePayload);
|
261
|
+
if (this.forceUpdateCwWebhook)
|
262
|
+
get_inbox = (yield updateInboxPromise).data;
|
263
|
+
else
|
264
|
+
proms.push(updateInboxPromise);
|
265
|
+
/**
|
266
|
+
* Get the inbox and test it.
|
267
|
+
*/
|
268
|
+
if (!((get_inbox === null || get_inbox === void 0 ? void 0 : get_inbox.webhook_url) || "").includes("/chatwoot"))
|
269
|
+
console.log("Please set the chatwoot inbox webhook to this sessions URL with path /chatwoot");
|
270
|
+
/**
|
271
|
+
* Check the webhook URL
|
272
|
+
*/
|
273
|
+
const chatwootWebhookCheck = () => __awaiter(this, void 0, void 0, function* () {
|
274
|
+
let checkCodePromise;
|
275
|
+
const cancelCheckProm = () => (checkCodePromise.cancel && typeof checkCodePromise.cancel === "function") && checkCodePromise.cancel();
|
276
|
+
try {
|
277
|
+
const wUrl = get_inbox.webhook_url.split('?')[0].replace(/\/+$/, "").trim();
|
278
|
+
const checkWhURL = `${wUrl}${wUrl.endsWith("/") ? '' : `/`}checkWebhook${this.key ? `?api_key=${this.key}` : ''}`;
|
279
|
+
__1.log.info(`Verifying webhook url: ${checkWhURL}`);
|
280
|
+
const checkCode = uuid_apikey_1.default.create().apiKey; //random generated string
|
281
|
+
yield new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
|
282
|
+
var _a;
|
283
|
+
checkCodePromise = __1.ev.waitFor(exports.chatwoot_webhook_check_event_name, 5000).catch(reject);
|
284
|
+
yield axios_1.default.post(checkWhURL, {
|
285
|
+
checkCode
|
286
|
+
}, { headers: { api_key: this.key || '' } }).catch(reject);
|
287
|
+
const checkCodeResponse = yield checkCodePromise;
|
288
|
+
if (checkCodeResponse && ((_a = checkCodeResponse[0]) === null || _a === void 0 ? void 0 : _a.checkCode) == checkCode)
|
289
|
+
resolve(true);
|
290
|
+
else
|
291
|
+
reject(`Webhook check code is incorrect. Expected ${checkCode} - incoming ${((checkCodeResponse || [])[0] || {}).checkCode}`);
|
292
|
+
}));
|
293
|
+
__1.log.info('Chatwoot webhook verification successful');
|
294
|
+
}
|
295
|
+
catch (error) {
|
296
|
+
cancelCheckProm();
|
297
|
+
const e = `Unable to verify the chatwoot webhook URL on this inbox: ${error.message}`;
|
298
|
+
console.error(e);
|
299
|
+
__1.log.error(e);
|
300
|
+
}
|
301
|
+
finally {
|
302
|
+
cancelCheckProm();
|
303
|
+
}
|
304
|
+
});
|
305
|
+
proms.push(chatwootWebhookCheck());
|
306
|
+
const setOnMessageProm = this.client.onMessage(this.processWAMessage.bind(this));
|
307
|
+
const setOnAckProm = this.client.onAck((ackEvent) => __awaiter(this, void 0, void 0, function* () {
|
308
|
+
if (ignoreMap[ackEvent.id] && typeof ignoreMap[ackEvent.id] === 'number' && ackEvent.ack > ignoreMap[ackEvent.id]) {
|
309
|
+
// delete ignoreMap[ackEvent.id]
|
310
|
+
return;
|
311
|
+
}
|
312
|
+
if (ackEvent.ack >= 1 && ackEvent.isNewMsg && ackEvent.self === "in") {
|
313
|
+
if (ignoreMap[ackEvent.id])
|
314
|
+
return;
|
315
|
+
ignoreMap[ackEvent.id] = ackEvent.ack;
|
316
|
+
const _message = yield this.client.getMessageById(ackEvent.id);
|
317
|
+
return yield this.processWAMessage(_message);
|
318
|
+
}
|
319
|
+
return;
|
320
|
+
}));
|
321
|
+
proms.push(setOnMessageProm);
|
322
|
+
proms.push(setOnAckProm);
|
323
|
+
yield Promise.all(proms);
|
324
|
+
this.inboxId = inboxId;
|
325
|
+
this.accountId = accountId;
|
326
|
+
});
|
170
327
|
}
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
var _a, _b;
|
181
|
-
__1.log.error(`CW REQ ERROR: ${(_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status} ${(_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.message}`, error === null || error === void 0 ? void 0 : error.toJSON());
|
182
|
-
throw error;
|
328
|
+
getAllInboxMessages(convoIdOrContactId) {
|
329
|
+
return __awaiter(this, void 0, void 0, function* () {
|
330
|
+
/**
|
331
|
+
* Check if it's a contact by traversing the convo reg.
|
332
|
+
*/
|
333
|
+
const convoId = convoReg[convoIdOrContactId] || convoIdOrContactId;
|
334
|
+
const { data } = yield this.cwReq('get', `conversations/${convoId}/messages`);
|
335
|
+
const messages = data.payload;
|
336
|
+
return messages;
|
183
337
|
});
|
184
|
-
|
185
|
-
return response;
|
186
|
-
});
|
187
|
-
let { data: get_inbox } = yield cwReq('get', `inboxes/${inboxId}`);
|
188
|
-
/**
|
189
|
-
* Update the webhook
|
190
|
-
*/
|
191
|
-
const updatePayload = {
|
192
|
-
"channel": {
|
193
|
-
"additional_attributes": {
|
194
|
-
"sessionId": client.getSessionId(),
|
195
|
-
"hostAccountNumber": `${accountNumber}`,
|
196
|
-
"instanceId": `${client.getInstanceId()}`
|
197
|
-
}
|
198
|
-
}
|
199
|
-
};
|
200
|
-
if (cliConfig.forceUpdateCwWebhook)
|
201
|
-
updatePayload.channel['webhook_url'] = expectedSelfWebhookUrl;
|
202
|
-
const updateInboxPromise = cwReq('patch', `inboxes/${inboxId}`, updatePayload);
|
203
|
-
if (cliConfig.forceUpdateCwWebhook)
|
204
|
-
get_inbox = (yield updateInboxPromise).data;
|
205
|
-
else
|
206
|
-
proms.push(updateInboxPromise);
|
207
|
-
/**
|
208
|
-
* Get the inbox and test it.
|
209
|
-
*/
|
210
|
-
if (!((get_inbox === null || get_inbox === void 0 ? void 0 : get_inbox.webhook_url) || "").includes("/chatwoot"))
|
211
|
-
console.log("Please set the chatwoot inbox webhook to this sessions URL with path /chatwoot");
|
338
|
+
}
|
212
339
|
/**
|
213
|
-
*
|
340
|
+
* Get the original chatwoot message object. Useful for getting CSAT full message details.
|
341
|
+
*
|
342
|
+
* @param convoIdOrContactId the owa contact ID or the convo ID. Better if you use convo ID.
|
343
|
+
* @param messageId
|
344
|
+
* @returns
|
214
345
|
*/
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
const checkCode = uuid_apikey_1.default.create().apiKey; //random generated string
|
223
|
-
yield new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {
|
224
|
-
var _a;
|
225
|
-
checkCodePromise = __1.ev.waitFor(exports.chatwoot_webhook_check_event_name, 5000).catch(reject);
|
226
|
-
yield axios_1.default.post(checkWhURL, {
|
227
|
-
checkCode
|
228
|
-
}, { headers: { api_key: cliConfig.key || '' } }).catch(reject);
|
229
|
-
const checkCodeResponse = yield checkCodePromise;
|
230
|
-
if (checkCodeResponse && ((_a = checkCodeResponse[0]) === null || _a === void 0 ? void 0 : _a.checkCode) == checkCode)
|
231
|
-
resolve(true);
|
232
|
-
else
|
233
|
-
reject(`Webhook check code is incorrect. Expected ${checkCode} - incoming ${((checkCodeResponse || [])[0] || {}).checkCode}`);
|
234
|
-
}));
|
235
|
-
__1.log.info('Chatwoot webhook verification successful');
|
236
|
-
}
|
237
|
-
catch (error) {
|
238
|
-
cancelCheckProm();
|
239
|
-
const e = `Unable to verify the chatwoot webhook URL on this inbox: ${error.message}`;
|
240
|
-
console.error(e);
|
241
|
-
__1.log.error(e);
|
242
|
-
}
|
243
|
-
finally {
|
244
|
-
cancelCheckProm();
|
245
|
-
}
|
246
|
-
});
|
247
|
-
proms.push(chatwootWebhookCheck());
|
346
|
+
getMessageObject(convoIdOrContactId, messageId) {
|
347
|
+
return __awaiter(this, void 0, void 0, function* () {
|
348
|
+
const msgs = yield this.getAllInboxMessages(`${convoIdOrContactId}`);
|
349
|
+
const foundMsg = msgs.find(x => x.id == messageId);
|
350
|
+
return foundMsg;
|
351
|
+
});
|
352
|
+
}
|
248
353
|
/**
|
249
354
|
* Get Contacts and conversations
|
250
355
|
*/
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
356
|
+
searchContact(number) {
|
357
|
+
return __awaiter(this, void 0, void 0, function* () {
|
358
|
+
try {
|
359
|
+
const n = number.replace('@c.us', '');
|
360
|
+
const { data } = yield this.cwReq('get', `contacts/search?q=${n}&sort=phone_number`);
|
361
|
+
if (data.payload.length) {
|
362
|
+
return data.payload.find(x => (x.phone_number || "").includes(n)) || false;
|
363
|
+
}
|
364
|
+
else
|
365
|
+
false;
|
257
366
|
}
|
258
|
-
|
259
|
-
|
260
|
-
}
|
261
|
-
catch (error) {
|
262
|
-
return;
|
263
|
-
}
|
264
|
-
});
|
265
|
-
const getContactConversation = (number) => __awaiter(void 0, void 0, void 0, function* () {
|
266
|
-
try {
|
267
|
-
const { data } = yield cwReq('get', `contacts/${contactReg[number]}/conversations`);
|
268
|
-
const allContactConversations = data.payload.filter(c => c.inbox_id === inboxId).sort((a, b) => a.id - b.id);
|
269
|
-
const [opened, notOpen] = [allContactConversations.filter(c => c.status === 'open'), allContactConversations.filter(c => c.status != 'open')];
|
270
|
-
const hasOpenConvo = opened[0] ? true : false;
|
271
|
-
const resolvedConversation = opened[0] || notOpen[0];
|
272
|
-
if (!hasOpenConvo) {
|
273
|
-
//reopen convo
|
274
|
-
yield openConversation(resolvedConversation.id);
|
367
|
+
catch (error) {
|
368
|
+
return;
|
275
369
|
}
|
276
|
-
return resolvedConversation;
|
277
|
-
}
|
278
|
-
catch (error) {
|
279
|
-
return;
|
280
|
-
}
|
281
|
-
});
|
282
|
-
const createConversation = (contact_id) => __awaiter(void 0, void 0, void 0, function* () {
|
283
|
-
try {
|
284
|
-
const { data } = yield cwReq('post', `conversations`, {
|
285
|
-
contact_id,
|
286
|
-
"inbox_id": inboxId
|
287
|
-
});
|
288
|
-
return data;
|
289
|
-
}
|
290
|
-
catch (error) {
|
291
|
-
return;
|
292
|
-
}
|
293
|
-
});
|
294
|
-
const createContact = (contact) => __awaiter(void 0, void 0, void 0, function* () {
|
295
|
-
try {
|
296
|
-
const { data } = yield cwReq('post', `contacts`, {
|
297
|
-
"identifier": contact.id,
|
298
|
-
"name": contact.formattedName || contact.id,
|
299
|
-
"phone_number": `+${contact.id.replace('@c.us', '')}`,
|
300
|
-
"avatar_url": contact.profilePicThumbObj.eurl
|
301
|
-
});
|
302
|
-
return data.payload.contact;
|
303
|
-
}
|
304
|
-
catch (error) {
|
305
|
-
return;
|
306
|
-
}
|
307
|
-
});
|
308
|
-
const openConversation = (conversationId, status = "opened") => __awaiter(void 0, void 0, void 0, function* () {
|
309
|
-
try {
|
310
|
-
const { data } = yield cwReq('post', `conversations/${conversationId}/messages`, {
|
311
|
-
status
|
312
|
-
});
|
313
|
-
return data;
|
314
|
-
}
|
315
|
-
catch (error) {
|
316
|
-
return;
|
317
|
-
}
|
318
|
-
});
|
319
|
-
const sendConversationMessage = (content, contactId, message) => __awaiter(void 0, void 0, void 0, function* () {
|
320
|
-
__1.log.info(`INCOMING MESSAGE ${contactId}: ${content} ${message.id}`);
|
321
|
-
try {
|
322
|
-
const { data } = yield cwReq('post', `conversations/${convoReg[contactId]}/messages`, {
|
323
|
-
content,
|
324
|
-
"message_type": message.fromMe ? "outgoing" : "incoming",
|
325
|
-
"private": false
|
326
|
-
});
|
327
|
-
return data;
|
328
|
-
}
|
329
|
-
catch (error) {
|
330
|
-
return;
|
331
|
-
}
|
332
|
-
});
|
333
|
-
const sendAttachmentMessage = (content, contactId, message) => __awaiter(void 0, void 0, void 0, function* () {
|
334
|
-
// decrypt message
|
335
|
-
const file = yield client.decryptMedia(message);
|
336
|
-
__1.log.info(`INCOMING MESSAGE ATTACHMENT ${contactId}: ${content} ${message.id}`);
|
337
|
-
let formData = new form_data_1.default();
|
338
|
-
formData.append('attachments[]', Buffer.from(file.split(',')[1], 'base64'), {
|
339
|
-
knownLength: 1,
|
340
|
-
filename: `${message.t}.${mime_types_1.default.extension(message.mimetype)}`,
|
341
|
-
contentType: (file.match(/[^:\s*]\w+\/[\w-+\d.]+(?=[;| ])/) || ["application/octet-stream"])[0]
|
342
370
|
});
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
return;
|
358
|
-
}
|
359
|
-
/**
|
360
|
-
* Does the contact exist in chatwoot?
|
361
|
-
*/
|
362
|
-
if (!contactReg[message.chatId]) {
|
363
|
-
const contact = yield searchContact(message.chatId);
|
364
|
-
if (contact) {
|
365
|
-
contactReg[message.chatId] = contact.id;
|
371
|
+
}
|
372
|
+
getContactConversation(number) {
|
373
|
+
return __awaiter(this, void 0, void 0, function* () {
|
374
|
+
try {
|
375
|
+
const { data } = yield this.cwReq('get', `contacts/${contactReg[number]}/conversations`);
|
376
|
+
const allContactConversations = data.payload.filter(c => `${c.inbox_id}` == `${this.inboxId}`).sort((a, b) => a.id - b.id);
|
377
|
+
const [opened, notOpen] = [allContactConversations.filter(c => c.status === 'open'), allContactConversations.filter(c => c.status != 'open')];
|
378
|
+
const hasOpenConvo = opened[0] ? true : false;
|
379
|
+
const resolvedConversation = opened[0] || notOpen[0];
|
380
|
+
if (!hasOpenConvo) {
|
381
|
+
//reopen convo
|
382
|
+
yield this.openConversation(resolvedConversation.id);
|
383
|
+
}
|
384
|
+
return resolvedConversation;
|
366
385
|
}
|
367
|
-
|
368
|
-
|
369
|
-
contactReg[message.chatId] = (yield createContact(message.sender)).id;
|
386
|
+
catch (error) {
|
387
|
+
return;
|
370
388
|
}
|
371
|
-
}
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
389
|
+
});
|
390
|
+
}
|
391
|
+
createConversation(contact_id) {
|
392
|
+
return __awaiter(this, void 0, void 0, function* () {
|
393
|
+
try {
|
394
|
+
const { data } = yield this.cwReq('post', `conversations`, {
|
395
|
+
contact_id,
|
396
|
+
"inbox_id": this.inboxId
|
397
|
+
});
|
398
|
+
return data;
|
376
399
|
}
|
377
|
-
|
378
|
-
|
379
|
-
convoReg[message.chatId] = (yield createConversation(contactReg[message.chatId])).id;
|
400
|
+
catch (error) {
|
401
|
+
return;
|
380
402
|
}
|
381
|
-
}
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
403
|
+
});
|
404
|
+
}
|
405
|
+
createContact(contact) {
|
406
|
+
return __awaiter(this, void 0, void 0, function* () {
|
407
|
+
try {
|
408
|
+
const { data } = yield this.cwReq('post', `contacts`, {
|
409
|
+
"identifier": contact.id,
|
410
|
+
"name": contact.formattedName || contact.id,
|
411
|
+
"phone_number": `+${contact.id.replace('@c.us', '')}`,
|
412
|
+
"avatar_url": contact.profilePicThumbObj.eurl,
|
413
|
+
"custom_attributes": Object.assign({ "wa:number": `${contact.id.replace('@c.us', '')}` }, contact)
|
414
|
+
});
|
415
|
+
return data.payload.contact;
|
416
|
+
}
|
417
|
+
catch (error) {
|
418
|
+
return;
|
419
|
+
}
|
420
|
+
});
|
421
|
+
}
|
422
|
+
openConversation(conversationId, status = "opened") {
|
423
|
+
return __awaiter(this, void 0, void 0, function* () {
|
424
|
+
try {
|
425
|
+
const { data } = yield this.cwReq('post', `conversations/${conversationId}/messages`, {
|
426
|
+
status
|
427
|
+
});
|
428
|
+
return data;
|
429
|
+
}
|
430
|
+
catch (error) {
|
431
|
+
return;
|
432
|
+
}
|
433
|
+
});
|
434
|
+
}
|
435
|
+
sendConversationMessage(content, contactId, message) {
|
436
|
+
return __awaiter(this, void 0, void 0, function* () {
|
437
|
+
__1.log.info(`WA=>CW ${contactId}: ${content} ${message.id}`);
|
438
|
+
try {
|
439
|
+
const { data } = yield this.cwReq('post', `conversations/${convoReg[contactId]}/messages`, {
|
440
|
+
content,
|
441
|
+
"message_type": message.fromMe ? "outgoing" : "incoming",
|
442
|
+
"private": false,
|
443
|
+
echo_id: message.id
|
444
|
+
});
|
445
|
+
return data;
|
446
|
+
}
|
447
|
+
catch (error) {
|
448
|
+
return;
|
449
|
+
}
|
450
|
+
});
|
451
|
+
}
|
452
|
+
sendAttachmentMessage(content, contactId, message) {
|
453
|
+
return __awaiter(this, void 0, void 0, function* () {
|
454
|
+
// decrypt message
|
455
|
+
const file = yield this.client.decryptMedia(message);
|
456
|
+
__1.log.info(`INCOMING MESSAGE ATTACHMENT ${contactId}: ${content} ${message.id}`);
|
457
|
+
const formData = new form_data_1.default();
|
458
|
+
formData.append('attachments[]', Buffer.from(file.split(',')[1], 'base64'), {
|
459
|
+
knownLength: 1,
|
460
|
+
filename: `${message.t}.${mime_types_1.default.extension(message.mimetype)}`,
|
461
|
+
contentType: (file.match(/[^:\s*]\w+\/[\w-+\d.]+(?=[;| ])/) || ["application/octet-stream"])[0]
|
462
|
+
});
|
463
|
+
formData.append('content', content);
|
464
|
+
formData.append('message_type', 'incoming');
|
465
|
+
try {
|
466
|
+
const { data } = yield this.cwReq('post', `conversations/${convoReg[contactId]}/messages`, formData, formData.getHeaders());
|
467
|
+
return data;
|
468
|
+
}
|
469
|
+
catch (error) {
|
470
|
+
return;
|
471
|
+
}
|
472
|
+
});
|
473
|
+
}
|
474
|
+
processWAMessage(message) {
|
475
|
+
var _a;
|
476
|
+
return __awaiter(this, void 0, void 0, function* () {
|
477
|
+
let isNewConversation = false;
|
478
|
+
if (message.chatId.includes('g')) {
|
479
|
+
//chatwoot integration does not support group chats
|
480
|
+
return;
|
481
|
+
}
|
482
|
+
/**
|
483
|
+
* Does the contact exist in chatwoot?
|
484
|
+
*/
|
485
|
+
if (!contactReg[message.chatId]) {
|
486
|
+
const contact = yield this.searchContact(message.chatId);
|
487
|
+
if (contact) {
|
488
|
+
contactReg[message.chatId] = contact.id;
|
401
489
|
}
|
402
490
|
else {
|
403
|
-
|
404
|
-
|
491
|
+
//create the contact
|
492
|
+
contactReg[message.chatId] = (yield this.createContact(message.sender)).id;
|
405
493
|
}
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
494
|
+
}
|
495
|
+
if (!convoReg[message.chatId]) {
|
496
|
+
const conversation = yield this.getContactConversation(message.chatId);
|
497
|
+
if (conversation) {
|
498
|
+
convoReg[message.chatId] = conversation.id;
|
499
|
+
}
|
500
|
+
else {
|
501
|
+
//create the conversation
|
502
|
+
convoReg[message.chatId] = (yield this.createConversation(contactReg[message.chatId])).id;
|
503
|
+
isNewConversation = convoReg[message.chatId];
|
504
|
+
}
|
505
|
+
}
|
506
|
+
/**
|
507
|
+
* Does the conversation exist in
|
508
|
+
*/
|
509
|
+
let text = message.body;
|
510
|
+
let hasAttachments = false;
|
511
|
+
switch (message.type) {
|
512
|
+
case 'list_response':
|
513
|
+
/**
|
514
|
+
* Possible CSAT response:
|
515
|
+
*/
|
516
|
+
yield this.processCSATResponse(message);
|
517
|
+
break;
|
518
|
+
case 'location':
|
519
|
+
text = `Location Message:\n\n${message.loc}\n\nhttps://www.google.com/maps?q=${message.lat},${message.lng}`;
|
520
|
+
break;
|
521
|
+
case 'buttons_response':
|
522
|
+
text = message.selectedButtonId;
|
523
|
+
break;
|
524
|
+
case 'document':
|
525
|
+
case 'image':
|
526
|
+
case 'audio':
|
527
|
+
case 'ptt':
|
528
|
+
case 'video':
|
529
|
+
if (message.cloudUrl) {
|
530
|
+
text = `FILE:\t${message.cloudUrl}\n\nMESSAGE:\t${message.text}`;
|
531
|
+
}
|
532
|
+
else {
|
533
|
+
text = message.text;
|
534
|
+
hasAttachments = true;
|
535
|
+
}
|
536
|
+
break;
|
537
|
+
default:
|
538
|
+
text = ((_a = message === null || message === void 0 ? void 0 : message.ctwaContext) === null || _a === void 0 ? void 0 : _a.sourceUrl) ? `${message.body}\n\n${message.ctwaContext.sourceUrl}` : message.body || "__UNHANDLED__";
|
539
|
+
break;
|
540
|
+
}
|
541
|
+
const newCWMessage = hasAttachments ? yield this.sendAttachmentMessage(text, message.chatId, message) : yield this.sendConversationMessage(text, message.chatId, message);
|
542
|
+
if (isNewConversation !== false) {
|
543
|
+
/**
|
544
|
+
* Wait 3 seconds before trying to check for an automated message
|
545
|
+
*/
|
546
|
+
yield (0, tools_1.timeout)(3000);
|
547
|
+
/**
|
548
|
+
* Check the messages to see if a message_type: 3 comes through after the initial message;
|
549
|
+
*/
|
550
|
+
const msgs = yield this.getAllInboxMessages(`${isNewConversation}`);
|
551
|
+
if (!msgs)
|
552
|
+
return;
|
553
|
+
/**
|
554
|
+
* Message IDs are numbers (for now)
|
555
|
+
*/
|
556
|
+
const possibleWelcomeMessage = msgs.filter(m => m.id > newCWMessage.id).find(m => m.message_type === 3 && m.content_type !== 'input_csat');
|
557
|
+
if (!possibleWelcomeMessage)
|
558
|
+
return;
|
559
|
+
/**
|
560
|
+
* Ok reply with the welcome message now
|
561
|
+
*/
|
562
|
+
yield this.client.sendText(message.chatId, possibleWelcomeMessage.content || "...");
|
563
|
+
}
|
564
|
+
});
|
565
|
+
}
|
566
|
+
processCSATResponse(message) {
|
567
|
+
return __awaiter(this, void 0, void 0, function* () {
|
568
|
+
const csatResponse = parseIdAndScore(message.listResponse.rowId);
|
569
|
+
if (!csatResponse)
|
425
570
|
return;
|
571
|
+
if (csatResponse.id && csatResponse.score) {
|
572
|
+
__1.log.info(`CW:CSAT RESPONSE: ${csatResponse.id} ${csatResponse.score}`);
|
573
|
+
/**
|
574
|
+
* PUT request to report CSAT response
|
575
|
+
*/
|
576
|
+
yield axios_1.default.put(`https://app.chatwoot.com/public/api/v1/csat_survey/${csatResponse.id}`, {
|
577
|
+
"message": {
|
578
|
+
"submitted_values": {
|
579
|
+
"csat_survey_response": {
|
580
|
+
"rating": csatResponse.score
|
581
|
+
}
|
582
|
+
}
|
583
|
+
}
|
584
|
+
}).catch(e => { });
|
426
585
|
}
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
return
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
586
|
+
});
|
587
|
+
}
|
588
|
+
sendCSAT(incomlpeteCsatMessage, to) {
|
589
|
+
return __awaiter(this, void 0, void 0, function* () {
|
590
|
+
const extractCsatLink = str => (str.match(/(http|ftp|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/gi) || [])[0];
|
591
|
+
/**
|
592
|
+
* First check if the given csat message object has the link, if not, get the whole csat object
|
593
|
+
*/
|
594
|
+
let csatMessage;
|
595
|
+
if (!extractCsatLink(incomlpeteCsatMessage === null || incomlpeteCsatMessage === void 0 ? void 0 : incomlpeteCsatMessage.content)) {
|
596
|
+
csatMessage = yield chatwootClient.getMessageObject(incomlpeteCsatMessage.conversation_id, incomlpeteCsatMessage.id);
|
597
|
+
}
|
598
|
+
else {
|
599
|
+
csatMessage = incomlpeteCsatMessage;
|
600
|
+
}
|
601
|
+
if (!csatMessage)
|
602
|
+
return;
|
603
|
+
const lic = false; //this.client.getLicenseType()
|
604
|
+
const link = extractCsatLink(csatMessage.content);
|
605
|
+
const u = new URL(link);
|
606
|
+
const csatID = u.pathname.replace('/survey/responses/', '');
|
607
|
+
__1.log.info(`SENDING CSAT ${to} ${csatMessage.content}`);
|
608
|
+
if (!lic) {
|
609
|
+
/**
|
610
|
+
* Send as a normal text message with the link
|
611
|
+
*/
|
612
|
+
yield this.client.sendLinkWithAutoPreview(to, link, csatMessage.content);
|
613
|
+
}
|
614
|
+
else {
|
615
|
+
yield this.client.sendListMessage(to, [
|
616
|
+
{
|
617
|
+
title: "Please rate from 1 - 5",
|
618
|
+
rows: [
|
619
|
+
{
|
620
|
+
"title": "😞",
|
621
|
+
"description": "1",
|
622
|
+
rowId: `${csatID}:1`
|
623
|
+
},
|
624
|
+
{
|
625
|
+
"title": "😑",
|
626
|
+
"description": "2",
|
627
|
+
rowId: `${csatID}:2`
|
628
|
+
},
|
629
|
+
{
|
630
|
+
"title": "😐",
|
631
|
+
"description": "3",
|
632
|
+
rowId: `${csatID}:3`
|
633
|
+
},
|
634
|
+
{
|
635
|
+
"title": "😀",
|
636
|
+
"description": "4",
|
637
|
+
rowId: `${csatID}:4`
|
638
|
+
},
|
639
|
+
{
|
640
|
+
"title": "😍",
|
641
|
+
"description": "5",
|
642
|
+
rowId: `${csatID}:5`
|
643
|
+
}
|
644
|
+
]
|
645
|
+
}
|
646
|
+
], "Customer Survey", "Please rate this conversation", 'Help Us Improve');
|
647
|
+
}
|
648
|
+
});
|
649
|
+
}
|
650
|
+
}
|