@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.
@@ -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')) return null;
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
- router.post('/webhookFB', async (req, res) => {
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
- winston.verbose("(fbm) Message received from Facebook Instagram");
561
+ function processInstagramWebhook(req, res) {
562
+ winston.verbose("(ig) Processing Instagram webhook");
518
563
 
519
- let body = req.body;
520
- if (body.object === 'page') {
564
+ if (!verifyWebhookSignature(req)) {
565
+ winston.warn("(ig) Signature invalid — still responding 200 to keep webhook healthy");
566
+ }
521
567
 
522
- let page_id = body.entry[0].id;
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
- if (!info_settings) {
527
- winston.debug("(fbm) Facebook page not enabled --> Skip")
528
- return res.status(200).send('EVENT_RECEIVED');
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
- let project_id = info_settings.project_id;
532
- winston.debug("(fbm) project_id: " + project_id);
581
+ if (body.object !== 'instagram' && body.object !== 'page') {
582
+ winston.debug("(ig) Unknown webhook object: " + body.object + " — skipping");
583
+ return;
584
+ }
533
585
 
534
- let CONTENT_KEY = "instagram-" + project_id;
535
- let settings = await db.get(CONTENT_KEY);
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
- body.entry.forEach(async (entry) => {
592
+ if (!entry.messaging || !entry.messaging[0]) {
593
+ winston.verbose("(ig) Entry has no messaging events — skipping");
594
+ return;
595
+ }
538
596
 
539
- let instagramChannelMessage = entry.messaging[0];
540
- winston.debug("(fbm) webhook_event: ", instagramChannelMessage);
597
+ var entryId = entry.id;
598
+ winston.debug("(ig) Entry ID: " + entryId);
541
599
 
542
- const tlr = new TiledeskInstagramTranslator();
543
- const tdChannel = new TiledeskChannel({ settings: settings, API_URL: API_URL })
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
- const page = settings.pages.find(p => p.id === page_id);
603
+ if (!info_settings) {
604
+ winston.debug("(ig) No settings found for entry " + entryId + " — checking project by scanning");
605
+ return;
606
+ }
547
607
 
548
- let user_info = await fbClient.getUserInfo(page.access_token, instagramChannelMessage.sender.id).catch((err) => {
549
- winston.error("(fbm) getUserInfo error: ", err?.response?.data || err.message || err);
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
- instagramChannelMessage.sender.fullname = user_info.first_name + " " + user_info.last_name;
611
+ let CONTENT_KEY = "instagram-" + project_id;
612
+ let settings = await db.get(CONTENT_KEY);
554
613
 
555
- winston.debug("(fbm) page: " + page);
556
- winston.debug("(fbm) user_info: ", user_info);
614
+ var instagramChannelMessage = entry.messaging[0];
615
+ winston.debug("(ig) webhook_event: ", instagramChannelMessage);
557
616
 
558
- let message_info = {
559
- channel: TiledeskInstagramTranslator.CHANNEL_NAME,
560
- instagram: {
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
- if (instagramChannelMessage.message &&
569
- instagramChannelMessage.message.attachments &&
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
- const messageHandler = new MessageHandler();
573
- let messagesList = await messageHandler.splitMessageFromInstagram(instagramChannelMessage);
624
+ if (!pageAccessToken && settings && settings.ig_oauth_token) {
625
+ pageAccessToken = settings.ig_oauth_token.access_token;
626
+ }
574
627
 
575
- for (let message of messagesList) {
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
- let tiledeskJsonMessage = await tlr.toTiledesk(message);
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
- return res.status(500).send({ success: false, error: "Error sending exoìpiration message" });
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
- } else {
593
- let tiledeskJsonMessage = await tlr.toTiledesk(instagramChannelMessage);
594
- winston.verbose("(fbm) tiledeskJsonMessage: ", tiledeskJsonMessage);
595
-
596
- if (tiledeskJsonMessage) {
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
- // Checks the mode and token sent is correct
632
- if (mode === 'subscribe' && token === VERIFY_TOKEN) {
683
+ router.post('/webhookIG', function(req, res) {
684
+ winston.verbose("(ig) POST /webhookIG");
685
+ processInstagramWebhook(req, res);
686
+ });
633
687
 
634
- // Responds with the challenge token from the request
635
- winston.verbose('(fbm) Webhook verified');
636
- return res.status(200).send(challenge);
688
+ router.post('/webhookFB', function(req, res) {
689
+ winston.verbose("(ig) POST /webhookFB");
690
+ processInstagramWebhook(req, res);
691
+ });
637
692
 
638
- } else {
639
- // Responds with '403 Forbidden' if verify tokens do not match
640
- return res.sendStatus(403);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@resultcrafter/aimanager-instagram-connector",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "AI Manager Instagram DM connector",
5
5
  "main": "index.js",
6
6
  "scripts": {