@resultcrafter/aimanager-instagram-connector 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aimanager/InstagramOAuth.js +67 -4
- package/index.js +162 -99
- package/package.json +1 -1
|
@@ -15,6 +15,58 @@ function getConfig() {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function getEncryptionKey() {
|
|
19
|
+
return process.env.TOKEN_ENCRYPTION_KEY || process.env.OAUTH_STATE_SECRET || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getCipherKey() {
|
|
23
|
+
var key = getEncryptionKey();
|
|
24
|
+
if (!key) return null;
|
|
25
|
+
return crypto.createHash('sha256').update(key).digest();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function encrypt(data) {
|
|
29
|
+
var cipherKey = getCipherKey();
|
|
30
|
+
if (!cipherKey) return null;
|
|
31
|
+
var text = JSON.stringify(data);
|
|
32
|
+
var iv = crypto.randomBytes(16);
|
|
33
|
+
var cipher = crypto.createCipheriv('aes-256-gcm', cipherKey, iv);
|
|
34
|
+
var encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
|
|
35
|
+
var authTag = cipher.getAuthTag();
|
|
36
|
+
return {
|
|
37
|
+
encrypted: encrypted.toString('base64'),
|
|
38
|
+
iv: iv.toString('base64'),
|
|
39
|
+
authTag: authTag.toString('base64')
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function decrypt(encryptedData) {
|
|
44
|
+
var cipherKey = getCipherKey();
|
|
45
|
+
if (!cipherKey) return null;
|
|
46
|
+
var decipher = crypto.createDecipheriv('aes-256-gcm', cipherKey, Buffer.from(encryptedData.iv, 'base64'));
|
|
47
|
+
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'base64'));
|
|
48
|
+
var decrypted = Buffer.concat([
|
|
49
|
+
decipher.update(Buffer.from(encryptedData.encrypted, 'base64')),
|
|
50
|
+
decipher.final()
|
|
51
|
+
]);
|
|
52
|
+
return JSON.parse(decrypted.toString('utf8'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function encryptToken(tokenData) {
|
|
56
|
+
var encrypted = encrypt(tokenData);
|
|
57
|
+
if (encrypted) return encrypted;
|
|
58
|
+
return tokenData;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function decryptToken(stored) {
|
|
62
|
+
if (!stored) return null;
|
|
63
|
+
if (stored.encrypted && stored.iv && stored.authTag) {
|
|
64
|
+
var decrypted = decrypt(stored);
|
|
65
|
+
if (decrypted) return decrypted;
|
|
66
|
+
}
|
|
67
|
+
return stored;
|
|
68
|
+
}
|
|
69
|
+
|
|
18
70
|
function generateState(sessionId, projectId) {
|
|
19
71
|
var data = {
|
|
20
72
|
sessionId: sessionId || crypto.randomBytes(16).toString('hex'),
|
|
@@ -38,7 +90,9 @@ function verifyState(state) {
|
|
|
38
90
|
var decoded = JSON.parse(Buffer.from(state, 'base64url').toString());
|
|
39
91
|
var hmac = crypto.createHmac('sha256', cfg.stateSecret);
|
|
40
92
|
hmac.update(JSON.stringify(decoded.data));
|
|
41
|
-
if (decoded.signature !== hmac.digest('hex'))
|
|
93
|
+
if (decoded.signature !== hmac.digest('hex')) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
42
96
|
if (Date.now() - decoded.data.timestamp > 600000) return null;
|
|
43
97
|
return decoded.data;
|
|
44
98
|
} catch (e) {
|
|
@@ -94,7 +148,7 @@ async function exchangeCodeForToken(code) {
|
|
|
94
148
|
};
|
|
95
149
|
}
|
|
96
150
|
|
|
97
|
-
async function refreshToken(currentToken) {
|
|
151
|
+
async function refreshToken(currentToken, existingTokenData) {
|
|
98
152
|
var cfg = getConfig();
|
|
99
153
|
var res = await axios.get(INSTAGRAM_GRAPH_BASE + '/refresh_access_token', {
|
|
100
154
|
params: { grant_type: 'ig_refresh_token', access_token: currentToken }
|
|
@@ -103,6 +157,10 @@ async function refreshToken(currentToken) {
|
|
|
103
157
|
access_token: res.data.access_token,
|
|
104
158
|
token_type: res.data.token_type,
|
|
105
159
|
expires_in: res.data.expires_in,
|
|
160
|
+
user_id: existingTokenData ? existingTokenData.user_id : undefined,
|
|
161
|
+
instagram_business_id: existingTokenData ? existingTokenData.instagram_business_id : undefined,
|
|
162
|
+
username: existingTokenData ? existingTokenData.username : undefined,
|
|
163
|
+
user_info: existingTokenData ? existingTokenData.user_info : undefined,
|
|
106
164
|
created_at: Date.now(),
|
|
107
165
|
expires_at: Date.now() + (res.data.expires_in * 1000)
|
|
108
166
|
};
|
|
@@ -134,7 +192,10 @@ async function getTokenStatus(db, projectId) {
|
|
|
134
192
|
if (!settings || !settings.ig_oauth_token) {
|
|
135
193
|
return { connected: false, instagram_username: null };
|
|
136
194
|
}
|
|
137
|
-
var token = settings.ig_oauth_token;
|
|
195
|
+
var token = decryptToken(settings.ig_oauth_token);
|
|
196
|
+
if (!token) {
|
|
197
|
+
return { connected: false, instagram_username: null };
|
|
198
|
+
}
|
|
138
199
|
var daysRemaining = token.expires_at ? Math.floor((token.expires_at - Date.now()) / (1000 * 60 * 60 * 24)) : null;
|
|
139
200
|
return {
|
|
140
201
|
connected: true,
|
|
@@ -151,5 +212,7 @@ module.exports = {
|
|
|
151
212
|
refreshToken: refreshToken,
|
|
152
213
|
getUserInfo: getUserInfo,
|
|
153
214
|
verifyState: verifyState,
|
|
154
|
-
getTokenStatus: getTokenStatus
|
|
215
|
+
getTokenStatus: getTokenStatus,
|
|
216
|
+
encryptToken: encryptToken,
|
|
217
|
+
decryptToken: decryptToken
|
|
155
218
|
};
|
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const bodyParser = require("body-parser");
|
|
|
5
5
|
const handlebars = require('handlebars');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const fs = require('fs');
|
|
8
|
+
const crypto = require('crypto');
|
|
8
9
|
const pjson = require('./package.json');
|
|
9
10
|
const winston = require('./winston');
|
|
10
11
|
const url = require('url');
|
|
@@ -512,136 +513,198 @@ router.post('/tiledesk', async (req, res) => {
|
|
|
512
513
|
return res.status(200).send({ message: "sent" });
|
|
513
514
|
})
|
|
514
515
|
|
|
515
|
-
|
|
516
|
+
function convertTestWebhook(body) {
|
|
517
|
+
if (body.field && body.value) {
|
|
518
|
+
winston.info("(ig) Converting test webhook from Meta dashboard");
|
|
519
|
+
var messaging = [];
|
|
520
|
+
if (body.value && body.value.messages && body.value.messages.data) {
|
|
521
|
+
body.value.messages.data.forEach(function(msg) {
|
|
522
|
+
messaging.push({
|
|
523
|
+
sender: { id: msg.from ? msg.from.id : 'test_sender' },
|
|
524
|
+
message: { text: msg.text || '[Test message from Meta]' }
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
object: 'instagram',
|
|
530
|
+
entry: [{ id: 'test_entry', messaging: messaging }]
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function verifyWebhookSignature(req) {
|
|
537
|
+
var signature = req.headers['x-hub-signature-256'];
|
|
538
|
+
if (!signature) {
|
|
539
|
+
winston.warn("(ig) Missing x-hub-signature-256 header — processing anyway");
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
if (!APP_SECRET) {
|
|
543
|
+
winston.warn("(ig) APP_SECRET not configured — skipping signature validation");
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
var algo = 'sha256';
|
|
547
|
+
var expected = signature.replace('sha256=', '');
|
|
548
|
+
var hmac = crypto.createHmac(algo, APP_SECRET);
|
|
549
|
+
hmac.update(JSON.stringify(req.body));
|
|
550
|
+
var computed = hmac.digest('hex');
|
|
551
|
+
var valid = (expected === computed);
|
|
552
|
+
if (!valid) {
|
|
553
|
+
winston.error("(ig) Webhook signature mismatch — expected=" + expected + " computed=" + computed);
|
|
554
|
+
if (process.env.DEBUG_SIGNATURES === 'true') {
|
|
555
|
+
winston.debug("(ig) Raw body for signature: " + JSON.stringify(req.body));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return valid;
|
|
559
|
+
}
|
|
516
560
|
|
|
517
|
-
|
|
561
|
+
function processInstagramWebhook(req, res) {
|
|
562
|
+
winston.verbose("(ig) Processing Instagram webhook");
|
|
518
563
|
|
|
519
|
-
|
|
520
|
-
|
|
564
|
+
if (!verifyWebhookSignature(req)) {
|
|
565
|
+
winston.warn("(ig) Signature invalid — still responding 200 to keep webhook healthy");
|
|
566
|
+
}
|
|
521
567
|
|
|
522
|
-
|
|
523
|
-
let PAGE_KEY = "instagram-page-" + page_id;
|
|
524
|
-
let info_settings = await db.get(PAGE_KEY);
|
|
568
|
+
res.status(200).send('EVENT_RECEIVED');
|
|
525
569
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
570
|
+
setImmediate(async () => {
|
|
571
|
+
try {
|
|
572
|
+
let body = req.body;
|
|
573
|
+
winston.verbose("(ig) Processing webhook asynchronously");
|
|
574
|
+
|
|
575
|
+
var testBody = convertTestWebhook(body);
|
|
576
|
+
if (testBody) {
|
|
577
|
+
winston.info("(ig) Using converted test webhook body");
|
|
578
|
+
body = testBody;
|
|
579
|
+
}
|
|
530
580
|
|
|
531
|
-
|
|
532
|
-
|
|
581
|
+
if (body.object !== 'instagram' && body.object !== 'page') {
|
|
582
|
+
winston.debug("(ig) Unknown webhook object: " + body.object + " — skipping");
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
533
585
|
|
|
534
|
-
|
|
535
|
-
|
|
586
|
+
body.entry.forEach(async (entry) => {
|
|
587
|
+
if (entry.changes) {
|
|
588
|
+
winston.verbose("(ig) Non-message event (changes): " + JSON.stringify(entry.changes).substring(0, 200));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
536
591
|
|
|
537
|
-
|
|
592
|
+
if (!entry.messaging || !entry.messaging[0]) {
|
|
593
|
+
winston.verbose("(ig) Entry has no messaging events — skipping");
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
538
596
|
|
|
539
|
-
|
|
540
|
-
|
|
597
|
+
var entryId = entry.id;
|
|
598
|
+
winston.debug("(ig) Entry ID: " + entryId);
|
|
541
599
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const fbClient = new FacebookClient({ GRAPH_URL: GRAPH_URL, FB_APP_ID: FB_APP_ID, APP_SECRET: APP_SECRET, BASE_URL: BASE_URL });
|
|
600
|
+
var PAGE_KEY = "instagram-page-" + entryId;
|
|
601
|
+
var info_settings = await db.get(PAGE_KEY);
|
|
545
602
|
|
|
546
|
-
|
|
603
|
+
if (!info_settings) {
|
|
604
|
+
winston.debug("(ig) No settings found for entry " + entryId + " — checking project by scanning");
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
547
607
|
|
|
548
|
-
|
|
549
|
-
winston.
|
|
550
|
-
return { first_name: "User", last_name: instagramChannelMessage.sender.id };
|
|
551
|
-
});
|
|
608
|
+
var project_id = info_settings.project_id;
|
|
609
|
+
winston.debug("(ig) project_id: " + project_id);
|
|
552
610
|
|
|
553
|
-
|
|
611
|
+
let CONTENT_KEY = "instagram-" + project_id;
|
|
612
|
+
let settings = await db.get(CONTENT_KEY);
|
|
554
613
|
|
|
555
|
-
|
|
556
|
-
|
|
614
|
+
var instagramChannelMessage = entry.messaging[0];
|
|
615
|
+
winston.debug("(ig) webhook_event: ", instagramChannelMessage);
|
|
557
616
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
page_id: page_id,
|
|
562
|
-
sender_id: instagramChannelMessage.sender.id,
|
|
563
|
-
firstname: user_info.first_name,
|
|
564
|
-
lastname: user_info.last_name
|
|
565
|
-
}
|
|
566
|
-
}
|
|
617
|
+
const tlr = new TiledeskInstagramTranslator();
|
|
618
|
+
const tdChannel = new TiledeskChannel({ settings: settings, API_URL: API_URL });
|
|
619
|
+
const fbClient = new FacebookClient({ GRAPH_URL: GRAPH_URL, FB_APP_ID: FB_APP_ID, APP_SECRET: APP_SECRET, BASE_URL: BASE_URL });
|
|
567
620
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
instagramChannelMessage.message.attachments.length > 1) {
|
|
621
|
+
const page = settings && settings.pages ? settings.pages.find(p => p.id === entryId) : null;
|
|
622
|
+
var pageAccessToken = page ? page.access_token : null;
|
|
571
623
|
|
|
572
|
-
|
|
573
|
-
|
|
624
|
+
if (!pageAccessToken && settings && settings.ig_oauth_token) {
|
|
625
|
+
pageAccessToken = settings.ig_oauth_token.access_token;
|
|
626
|
+
}
|
|
574
627
|
|
|
575
|
-
|
|
628
|
+
var user_info = { first_name: "User", last_name: instagramChannelMessage.sender.id };
|
|
629
|
+
if (pageAccessToken) {
|
|
630
|
+
user_info = await fbClient.getUserInfo(pageAccessToken, instagramChannelMessage.sender.id).catch(function(err) {
|
|
631
|
+
winston.error("(ig) getUserInfo error: ", err?.response?.data || err.message || err);
|
|
632
|
+
return { first_name: "User", last_name: instagramChannelMessage.sender.id };
|
|
633
|
+
});
|
|
634
|
+
}
|
|
576
635
|
|
|
577
|
-
|
|
578
|
-
winston.verbose("(fbm) tiledeskJsonMessage: ", tiledeskJsonMessage);
|
|
636
|
+
instagramChannelMessage.sender.fullname = user_info.first_name + " " + user_info.last_name;
|
|
579
637
|
|
|
638
|
+
var message_info = {
|
|
639
|
+
channel: TiledeskInstagramTranslator.CHANNEL_NAME,
|
|
640
|
+
instagram: {
|
|
641
|
+
page_id: entryId,
|
|
642
|
+
sender_id: instagramChannelMessage.sender.id,
|
|
643
|
+
firstname: user_info.first_name,
|
|
644
|
+
lastname: user_info.last_name
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
if (instagramChannelMessage.message &&
|
|
649
|
+
instagramChannelMessage.message.attachments &&
|
|
650
|
+
instagramChannelMessage.message.attachments.length > 1) {
|
|
651
|
+
|
|
652
|
+
const messageHandler = new MessageHandler();
|
|
653
|
+
let messagesList = await messageHandler.splitMessageFromInstagram(instagramChannelMessage);
|
|
654
|
+
|
|
655
|
+
for (let message of messagesList) {
|
|
656
|
+
let tiledeskJsonMessage = await tlr.toTiledesk(message);
|
|
657
|
+
if (tiledeskJsonMessage) {
|
|
658
|
+
try {
|
|
659
|
+
await tdChannel.send(tiledeskJsonMessage, message_info, settings.department_id);
|
|
660
|
+
} catch (err) {
|
|
661
|
+
winston.error("(ig) Error sending split message: ", err);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
} else {
|
|
666
|
+
let tiledeskJsonMessage = await tlr.toTiledesk(instagramChannelMessage);
|
|
580
667
|
if (tiledeskJsonMessage) {
|
|
581
668
|
try {
|
|
582
669
|
await tdChannel.send(tiledeskJsonMessage, message_info, settings.department_id);
|
|
583
|
-
winston.verbose("(fbm) Message sent to AI Manager")
|
|
584
670
|
} catch (err) {
|
|
585
|
-
|
|
671
|
+
winston.error("(ig) Error sending message: ", err);
|
|
586
672
|
}
|
|
587
|
-
} else {
|
|
588
|
-
winston.verbose("(fbm) tiledeskJsonMessage is undefined!")
|
|
589
673
|
}
|
|
590
674
|
}
|
|
675
|
+
});
|
|
591
676
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
try {
|
|
598
|
-
await tdChannel.send(tiledeskJsonMessage, message_info, settings.department_id);
|
|
599
|
-
winston.verbose("(fbm) Message sent to AI Manager")
|
|
600
|
-
} catch (err) {
|
|
601
|
-
return res.status(500).send({ success: false, error: "Error sending message" });
|
|
602
|
-
}
|
|
603
|
-
} else {
|
|
604
|
-
winston.verbose("(fbm) tiledeskJsonMessage is undefined!")
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
})
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
return res.status(200).send('EVENT_RECEIVED');
|
|
612
|
-
})
|
|
613
|
-
|
|
614
|
-
router.get('/webhookFB', async (req, res) => {
|
|
615
|
-
|
|
616
|
-
winston.verbose("(fbm) Verify the webhook... ", req.query);
|
|
617
|
-
|
|
618
|
-
// Parse the query params
|
|
619
|
-
let mode = req.query['hub.mode'];
|
|
620
|
-
let token = req.query['hub.verify_token'];
|
|
621
|
-
let challenge = req.query['hub.challenge'];
|
|
622
|
-
|
|
623
|
-
winston.verbose("(fbm) token: " + token);
|
|
624
|
-
winston.verbose("(fbm) verify token: " + VERIFY_TOKEN);
|
|
625
|
-
winston.verbose("(fbm) mode: " + mode)
|
|
626
|
-
winston.verbose("(fbm) challenge: " + challenge)
|
|
627
|
-
|
|
628
|
-
// Checks if a token and mode is in the query string of the request
|
|
629
|
-
if (mode && token) {
|
|
677
|
+
} catch (err) {
|
|
678
|
+
winston.error("(ig) Async webhook processing error: ", err?.response?.data || err.message || err);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}
|
|
630
682
|
|
|
631
|
-
|
|
632
|
-
|
|
683
|
+
router.post('/webhookIG', function(req, res) {
|
|
684
|
+
winston.verbose("(ig) POST /webhookIG");
|
|
685
|
+
processInstagramWebhook(req, res);
|
|
686
|
+
});
|
|
633
687
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
688
|
+
router.post('/webhookFB', function(req, res) {
|
|
689
|
+
winston.verbose("(ig) POST /webhookFB");
|
|
690
|
+
processInstagramWebhook(req, res);
|
|
691
|
+
});
|
|
637
692
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
693
|
+
function handleWebhookVerify(req, res) {
|
|
694
|
+
winston.verbose("(ig) Verify webhook... ", req.query);
|
|
695
|
+
var mode = req.query['hub.mode'];
|
|
696
|
+
var token = req.query['hub.verify_token'];
|
|
697
|
+
var challenge = req.query['hub.challenge'];
|
|
698
|
+
if (mode && token && mode === 'subscribe' && token === VERIFY_TOKEN) {
|
|
699
|
+
winston.info("(ig) Webhook verified");
|
|
700
|
+
return res.status(200).send(challenge);
|
|
642
701
|
}
|
|
702
|
+
winston.warn("(ig) Webhook verify failed — mode=" + mode + " token match=" + (token === VERIFY_TOKEN));
|
|
643
703
|
return res.sendStatus(403);
|
|
644
|
-
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
router.get('/webhookIG', function(req, res) { handleWebhookVerify(req, res); });
|
|
707
|
+
router.get('/webhookFB', function(req, res) { handleWebhookVerify(req, res); });
|
|
645
708
|
|
|
646
709
|
router.get('/auth/instagram/start', async (req, res) => {
|
|
647
710
|
winston.verbose("(ig) /auth/instagram/start");
|
|
@@ -688,7 +751,7 @@ router.get('/oauth-callback', async (req, res) => {
|
|
|
688
751
|
|
|
689
752
|
var CONTENT_KEY = "instagram-" + projectId;
|
|
690
753
|
var settings = await db.get(CONTENT_KEY) || {};
|
|
691
|
-
settings.ig_oauth_token = tokenData;
|
|
754
|
+
settings.ig_oauth_token = instagramOAuth.encryptToken(tokenData);
|
|
692
755
|
settings.instagram_username = tokenData.username;
|
|
693
756
|
settings.instagram_business_id = tokenData.instagram_business_id;
|
|
694
757
|
settings.connected = true;
|