@resultcrafter/aimanager-instagram-connector 0.1.2 → 0.2.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.
@@ -0,0 +1,155 @@
1
+ var crypto = require('crypto');
2
+ var axios = require('axios');
3
+
4
+ var INSTAGRAM_OAUTH_BASE = 'https://www.instagram.com/oauth';
5
+ var INSTAGRAM_API_BASE = 'https://api.instagram.com/oauth';
6
+ var INSTAGRAM_GRAPH_BASE = 'https://graph.instagram.com';
7
+
8
+ function getConfig() {
9
+ return {
10
+ appId: process.env.INSTAGRAM_APP_ID,
11
+ appSecret: process.env.INSTAGRAM_APP_SECRET,
12
+ redirectUri: process.env.INSTAGRAM_REDIRECT_URI,
13
+ stateSecret: process.env.OAUTH_STATE_SECRET || process.env.INSTAGRAM_APP_SECRET || 'default_secret',
14
+ scopes: ['instagram_business_basic', 'instagram_business_manage_messages'].join(',')
15
+ };
16
+ }
17
+
18
+ function generateState(sessionId, projectId) {
19
+ var data = {
20
+ sessionId: sessionId || crypto.randomBytes(16).toString('hex'),
21
+ project_id: projectId,
22
+ timestamp: Date.now(),
23
+ nonce: crypto.randomBytes(8).toString('hex')
24
+ };
25
+ var cfg = getConfig();
26
+ var hmac = crypto.createHmac('sha256', cfg.stateSecret);
27
+ hmac.update(JSON.stringify(data));
28
+ var state = Buffer.from(JSON.stringify({
29
+ data: data,
30
+ signature: hmac.digest('hex')
31
+ })).toString('base64url');
32
+ return state;
33
+ }
34
+
35
+ function verifyState(state) {
36
+ try {
37
+ var cfg = getConfig();
38
+ var decoded = JSON.parse(Buffer.from(state, 'base64url').toString());
39
+ var hmac = crypto.createHmac('sha256', cfg.stateSecret);
40
+ hmac.update(JSON.stringify(decoded.data));
41
+ if (decoded.signature !== hmac.digest('hex')) return null;
42
+ if (Date.now() - decoded.data.timestamp > 600000) return null;
43
+ return decoded.data;
44
+ } catch (e) {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function getAuthorizationUrl(projectId, token, appId) {
50
+ var cfg = getConfig();
51
+ if (!cfg.appId) throw new Error('INSTAGRAM_APP_ID not configured');
52
+ if (!cfg.redirectUri) throw new Error('INSTAGRAM_REDIRECT_URI not configured');
53
+ var state = generateState(null, projectId);
54
+ var params = new URLSearchParams({
55
+ client_id: cfg.appId,
56
+ redirect_uri: cfg.redirectUri,
57
+ scope: cfg.scopes,
58
+ response_type: 'code',
59
+ state: state
60
+ });
61
+ return { url: INSTAGRAM_OAUTH_BASE + '/authorize?' + params.toString(), state: state };
62
+ }
63
+
64
+ async function exchangeCodeForToken(code) {
65
+ var cfg = getConfig();
66
+ if (!cfg.appId || !cfg.appSecret) throw new Error('Instagram app credentials not configured');
67
+ var tokenRes = await axios.post(INSTAGRAM_API_BASE + '/access_token',
68
+ new URLSearchParams({
69
+ client_id: cfg.appId,
70
+ client_secret: cfg.appSecret,
71
+ grant_type: 'authorization_code',
72
+ redirect_uri: cfg.redirectUri,
73
+ code: code
74
+ }),
75
+ { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
76
+ );
77
+ var shortToken = tokenRes.data.access_token;
78
+ var userId = tokenRes.data.user_id;
79
+ var longRes = await axios.get(INSTAGRAM_GRAPH_BASE + '/access_token', {
80
+ params: { grant_type: 'ig_exchange_token', client_secret: cfg.appSecret, access_token: shortToken }
81
+ });
82
+ var longToken = longRes.data.access_token;
83
+ var userInfo = await getUserInfo(longToken);
84
+ return {
85
+ access_token: longToken,
86
+ token_type: longRes.data.token_type,
87
+ expires_in: longRes.data.expires_in,
88
+ user_id: userId,
89
+ instagram_business_id: userInfo.instagram_business_id || userInfo.user_id || userId,
90
+ username: userInfo.username,
91
+ user_info: userInfo,
92
+ created_at: Date.now(),
93
+ expires_at: Date.now() + (longRes.data.expires_in * 1000)
94
+ };
95
+ }
96
+
97
+ async function refreshToken(currentToken) {
98
+ var cfg = getConfig();
99
+ var res = await axios.get(INSTAGRAM_GRAPH_BASE + '/refresh_access_token', {
100
+ params: { grant_type: 'ig_refresh_token', access_token: currentToken }
101
+ });
102
+ return {
103
+ access_token: res.data.access_token,
104
+ token_type: res.data.token_type,
105
+ expires_in: res.data.expires_in,
106
+ created_at: Date.now(),
107
+ expires_at: Date.now() + (res.data.expires_in * 1000)
108
+ };
109
+ }
110
+
111
+ async function getUserInfo(accessToken) {
112
+ try {
113
+ var res = await axios.get(INSTAGRAM_GRAPH_BASE + '/v23.0/me', {
114
+ params: {
115
+ fields: 'id,user_id,username,name,account_type,media_count,followers_count,follows_count,profile_picture_url',
116
+ access_token: accessToken
117
+ }
118
+ });
119
+ return {
120
+ app_scoped_id: res.data.id,
121
+ instagram_business_id: res.data.user_id,
122
+ username: res.data.username,
123
+ name: res.data.name,
124
+ account_type: res.data.account_type,
125
+ profile_picture_url: res.data.profile_picture_url
126
+ };
127
+ } catch (e) {
128
+ return { username: null };
129
+ }
130
+ }
131
+
132
+ async function getTokenStatus(db, projectId) {
133
+ var settings = await db.get('instagram-' + projectId);
134
+ if (!settings || !settings.ig_oauth_token) {
135
+ return { connected: false, instagram_username: null };
136
+ }
137
+ var token = settings.ig_oauth_token;
138
+ var daysRemaining = token.expires_at ? Math.floor((token.expires_at - Date.now()) / (1000 * 60 * 60 * 24)) : null;
139
+ return {
140
+ connected: true,
141
+ instagram_username: token.username || settings.instagram_username || 'Instagram Account',
142
+ user_id: token.instagram_business_id || token.user_id,
143
+ days_remaining: daysRemaining,
144
+ needs_refresh: daysRemaining !== null ? daysRemaining < 7 : false
145
+ };
146
+ }
147
+
148
+ module.exports = {
149
+ getAuthorizationUrl: getAuthorizationUrl,
150
+ exchangeCodeForToken: exchangeCodeForToken,
151
+ refreshToken: refreshToken,
152
+ getUserInfo: getUserInfo,
153
+ verifyState: verifyState,
154
+ getTokenStatus: getTokenStatus
155
+ };
package/index.js CHANGED
@@ -16,6 +16,7 @@ const { TiledeskInstagramTranslator } = require('./aimanager/TiledeskInstagramTr
16
16
  const { TiledeskSubscriptionClient } = require('./aimanager/TiledeskSubscriptionClient');
17
17
  const { FacebookClient } = require('./aimanager/FacebookClient');
18
18
  const { MessageHandler } = require('./aimanager/MessageHandler');
19
+ const instagramOAuth = require('./aimanager/InstagramOAuth');
19
20
 
20
21
 
21
22
  // mongo
@@ -110,13 +111,14 @@ router.post('/install', async (req, res) => {
110
111
  query: {
111
112
  "project_id": project_id,
112
113
  "app_id": app_id,
113
- "token": token
114
+ "token": token,
115
+ "oauth_url": BASE_URL + '/auth/instagram/start'
114
116
  }
115
117
  }));
116
118
 
117
119
  }).catch((err) => {
118
- winston.error("(fbm) installation error: ", err.data)
119
- winston.error("(fbm) installation error: " + err.data)
120
+ winston.error("(ig) installation error: ", err.data)
121
+ winston.error("(ig) installation error: " + err.data)
120
122
  res.send("An error occurred during the installation");
121
123
  })
122
124
 
@@ -184,6 +186,8 @@ router.get('/configure', async (req, res) => {
184
186
  return []
185
187
  })
186
188
 
189
+ var tokenStatus = await instagramOAuth.getTokenStatus(db, project_id);
190
+
187
191
  if (settings) {
188
192
  winston.debug("(fbm) settings found: ", settings);
189
193
 
@@ -192,7 +196,7 @@ router.get('/configure', async (req, res) => {
192
196
  var replacements = {
193
197
  app_version: pjson.version,
194
198
  project_id: project_id,
195
- ig_token: settings.ig_token || settings.access_token,
199
+ ig_token: tokenStatus.connected ? tokenStatus.instagram_username : null,
196
200
  token: token,
197
201
  app_id: app_id,
198
202
  endpoint: BASE_URL,
@@ -201,9 +205,10 @@ router.get('/configure', async (req, res) => {
201
205
  department_id: settings.department_id,
202
206
  departments: departments,
203
207
  brand_name: BRAND_NAME,
204
- instagram_username: settings.instagram_username || settings.user_info?.username || 'Instagram Account',
208
+ instagram_username: tokenStatus.instagram_username || 'Instagram Account',
205
209
  show_info_message: settings.show_info_message !== undefined ? settings.show_info_message : true,
206
- subscription_id: settings.subscription_id
210
+ subscription_id: settings.subscription_id,
211
+ oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token + '&app_id=' + app_id
207
212
  }
208
213
  var html = template(replacements)
209
214
  res.send(html);
@@ -221,7 +226,8 @@ router.get('/configure', async (req, res) => {
221
226
  app_id: app_id,
222
227
  endpoint: BASE_URL,
223
228
  departments: departments,
224
- brand_name: BRAND_NAME
229
+ brand_name: BRAND_NAME,
230
+ oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token + '&app_id=' + app_id
225
231
  }
226
232
  var html = template(replacements);
227
233
  res.send(html);
@@ -232,7 +238,7 @@ router.get('/configure', async (req, res) => {
232
238
  })
233
239
 
234
240
  router.post('/update', async (req, res) => {
235
- winston.verbose("(fbm) /update");
241
+ winston.verbose("(ig) /update");
236
242
 
237
243
  let project_id = req.body.project_id;
238
244
  let token = req.body.token;
@@ -247,10 +253,11 @@ router.post('/update', async (req, res) => {
247
253
  await db.set(CONTENT_KEY, settings);
248
254
  }
249
255
 
250
- // get departments
256
+ var tokenStatus = await instagramOAuth.getTokenStatus(db, project_id);
257
+
251
258
  const tdChannel = new TiledeskChannel({ settings: { project_id: project_id, token: token }, API_URL: API_URL });
252
259
  let departments = await tdChannel.getDepartments(token);
253
- winston.verbose("(fbm) found " + departments.length + " departments")
260
+ winston.verbose("(ig) found " + departments.length + " departments")
254
261
 
255
262
  readHTMLFile('/configure.html', (err, html) => {
256
263
 
@@ -258,7 +265,7 @@ router.post('/update', async (req, res) => {
258
265
  var replacements = {
259
266
  app_version: pjson.version,
260
267
  project_id: project_id,
261
- ig_token: settings ? (settings.ig_token || settings.access_token) : null,
268
+ ig_token: tokenStatus.connected ? tokenStatus.instagram_username : null,
262
269
  token: token,
263
270
  app_id: app_id,
264
271
  endpoint: BASE_URL,
@@ -268,8 +275,9 @@ router.post('/update', async (req, res) => {
268
275
  departments: departments,
269
276
  brand_name: BRAND_NAME,
270
277
  show_info_message: settings ? (settings.show_info_message !== undefined ? settings.show_info_message : true) : true,
271
- instagram_username: settings ? (settings.instagram_username || settings.user_info?.username || 'Instagram Account') : null,
272
- subscription_id: settings ? settings.subscription_id : null
278
+ instagram_username: tokenStatus.instagram_username || 'Instagram Account',
279
+ subscription_id: settings ? settings.subscription_id : null,
280
+ oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token + '&app_id=' + app_id
273
281
  }
274
282
  var html = template(replacements)
275
283
  res.send(html);
@@ -278,7 +286,7 @@ router.post('/update', async (req, res) => {
278
286
  })
279
287
 
280
288
  router.post('/update_advanced', async (req, res) => {
281
- winston.verbose("(fbm) /update_advanced");
289
+ winston.verbose("(ig) /update_advanced");
282
290
 
283
291
  let project_id = req.body.project_id;
284
292
  let show_info_message = req.body.show_info_message === 'on';
@@ -291,6 +299,8 @@ router.post('/update_advanced', async (req, res) => {
291
299
  await db.set(CONTENT_KEY, settings);
292
300
  }
293
301
 
302
+ var tokenStatus = await instagramOAuth.getTokenStatus(db, project_id);
303
+
294
304
  const tdChannel = new TiledeskChannel({ settings: { project_id: project_id, token: req.body.token }, API_URL: API_URL });
295
305
  let departments = await tdChannel.getDepartments(req.body.token).catch(() => []);
296
306
 
@@ -299,7 +309,7 @@ router.post('/update_advanced', async (req, res) => {
299
309
  var replacements = {
300
310
  app_version: pjson.version,
301
311
  project_id: project_id,
302
- ig_token: settings ? (settings.ig_token || settings.access_token) : null,
312
+ ig_token: tokenStatus.connected ? tokenStatus.instagram_username : null,
303
313
  token: req.body.token,
304
314
  endpoint: BASE_URL,
305
315
  pages: settings ? settings.pages : null,
@@ -308,8 +318,9 @@ router.post('/update_advanced', async (req, res) => {
308
318
  departments: departments,
309
319
  brand_name: BRAND_NAME,
310
320
  show_info_message: show_info_message,
311
- instagram_username: settings ? (settings.instagram_username || settings.user_info?.username || 'Instagram Account') : null,
312
- subscription_id: settings ? settings.subscription_id : null
321
+ instagram_username: tokenStatus.instagram_username || 'Instagram Account',
322
+ subscription_id: settings ? settings.subscription_id : null,
323
+ oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + req.body.token
313
324
  }
314
325
  var html = template(replacements)
315
326
  res.send(html);
@@ -632,93 +643,63 @@ router.get('/webhookFB', async (req, res) => {
632
643
  return res.sendStatus(403);
633
644
  })
634
645
 
635
- router.get('/oauth', async (req, res) => {
636
- winston.verbose("(fbm) /oauth")
637
-
638
- let project_id = JSON.parse(req.query.state).project_id;
639
- var code = req.query.code;
640
- let token = JSON.parse(req.query.state).token;
641
- let app_id = JSON.parse(req.query.state).app_id;
646
+ router.get('/auth/instagram/start', async (req, res) => {
647
+ winston.verbose("(ig) /auth/instagram/start");
642
648
 
643
- const tdClient = new TiledeskSubscriptionClient({ API_URL: API_URL, project_id: project_id, token: token })
644
- const fbClient = new FacebookClient({ GRAPH_URL: GRAPH_URL, FB_APP_ID: FB_APP_ID, APP_SECRET: APP_SECRET, BASE_URL: BASE_URL });
649
+ let project_id = req.query.project_id;
650
+ let token = req.query.token;
651
+ let app_id = req.query.app_id;
645
652
 
646
- const subscription_info = {
647
- target: BASE_URL + '/tiledesk',
648
- event: 'message.create.request.channel.instagram',
653
+ if (!project_id) {
654
+ return res.status(400).send('Missing project_id parameter');
649
655
  }
650
656
 
651
- let subscription, access_token, pages_list;
652
657
  try {
653
- subscription = await tdClient.subscribe(subscription_info);
654
- access_token = await fbClient.getAccessTokenFromCode(code);
655
- pages_list = await fbClient.getPages(access_token);
658
+ var auth = instagramOAuth.getAuthorizationUrl(project_id, token, app_id);
659
+ winston.info("(ig) Redirecting to Instagram OAuth for project " + project_id);
660
+ res.redirect(auth.url);
656
661
  } catch (err) {
657
- winston.error("(fbm) /oauth error: ", err?.response?.data || err.message || err);
658
- return res.status(500).send("Error during Facebook connection. Please verify the authorization and retry.");
662
+ winston.error("(ig) OAuth start error: ", err.message);
663
+ res.status(500).send('OAuth configuration error: ' + err.message);
659
664
  }
665
+ });
660
666
 
661
- winston.debug("(fbm) subscription: " + subscription);
662
- winston.debug("(fbm) access_token: " + access_token);
663
- winston.debug("(fbm) pages_list: " + pages_list);
664
-
665
- let CONTENT_KEY = "instagram-" + project_id;
667
+ router.get('/oauth-callback', async (req, res) => {
668
+ winston.verbose("(ig) /oauth-callback");
666
669
 
667
- let pages = [];
668
- pages_list.forEach(async (single_page) => {
669
- let page = {
670
- id: single_page.id,
671
- name: single_page.name,
672
- access_token: single_page.access_token,
673
- category: single_page.category,
674
- active: false
675
- }
676
- pages.push(page);
677
- try {
678
- let event_sub = await fbClient.messageEventSubscription(single_page.id, single_page.access_token);
679
- winston.debug("(fbm) event subscription: " + event_sub?.status + " " + event_sub?.statusText)
680
- } catch (err) {
681
- winston.error("(fbm) event subscription error: ", err?.response?.data || err.message || err);
682
- }
683
- })
684
-
685
- let settings = {
686
- project_id: project_id,
687
- token: token,
688
- access_token: access_token,
689
- subscription_id: subscription._id,
690
- secret: subscription.secret,
691
- pages: pages,
692
- app_id: app_id
670
+ var code = req.query.code;
671
+ var stateStr = req.query.state;
672
+ if (!code || !stateStr) {
673
+ winston.error("(ig) Missing code or state parameter");
674
+ return res.status(400).send("Missing authorization code or state");
693
675
  }
694
676
 
695
- await db.set(CONTENT_KEY, settings);
677
+ var state = instagramOAuth.verifyState(stateStr);
678
+ if (!state) {
679
+ winston.error("(ig) Invalid or expired state");
680
+ return res.status(400).send("Invalid or expired state parameter. Please try connecting again.");
681
+ }
696
682
 
697
- //let settings_retrived = await db.get(CONTENT_KEY);
698
- //var redirect_uri = DASHBOARD_BASE_URL + "/#/project/" + project_id + "/app-store-install/" + app_id + "/run";
699
- var redirect_uri = DASHBOARD_BASE_URL + "/#/project/" + project_id + "/integrations?name=instagram";
700
- console.log("(fbm) redirect_uri: ", redirect_uri);
701
- res.redirect(redirect_uri);
683
+ var projectId = state.project_id;
702
684
 
703
- /*
704
- readHTMLFile('/configure.html', (err, html) => {
705
- var template = handlebars.compile(html);
706
- var replacements = {
707
- app_version: pjson.version,
708
- project_id: project_id,
709
- connected: true,
710
- token: token,
711
- app_id: app_id,
712
- endpoint: BASE_URL,
713
- subscription_id: settings.subscription_id,
714
- secret: settings.secret,
715
- pages: settings.pages
716
- }
717
- var html = template(replacements)
718
- res.send(html);
719
- })
720
- */
685
+ try {
686
+ var tokenData = await instagramOAuth.exchangeCodeForToken(code);
687
+ winston.info("(ig) Token obtained for project " + projectId + ", user: " + (tokenData.username || 'unknown'));
688
+
689
+ var CONTENT_KEY = "instagram-" + projectId;
690
+ var settings = await db.get(CONTENT_KEY) || {};
691
+ settings.ig_oauth_token = tokenData;
692
+ settings.instagram_username = tokenData.username;
693
+ settings.instagram_business_id = tokenData.instagram_business_id;
694
+ settings.connected = true;
695
+ await db.set(CONTENT_KEY, settings);
721
696
 
697
+ var redirect_uri = DASHBOARD_BASE_URL + "/#/project/" + projectId + "/integrations?name=instagram";
698
+ res.redirect(redirect_uri);
699
+ } catch (err) {
700
+ winston.error("(ig) OAuth callback error: ", err?.response?.data || err.message || err);
701
+ res.status(500).send("Authentication failed: " + (err.message || 'Unknown error'));
702
+ }
722
703
  })
723
704
 
724
705
  router.post('/enablePage', async (req, res) => {
@@ -858,38 +839,25 @@ router.post('/disablePage', async (req, res) => {
858
839
  })
859
840
 
860
841
  router.post('/disconnect', async (req, res) => {
861
- winston.verbose("(fbm) /disconnect");
842
+ winston.verbose("(ig) /disconnect");
862
843
 
863
844
  let project_id = req.body.project_id;
864
845
  let token = req.body.token;
865
- let subscription_id = req.body.subscription_id;
866
846
 
867
847
  let CONTENT_KEY = "instagram-" + project_id;
868
-
869
848
  let settings = await db.get(CONTENT_KEY);
870
849
 
871
- if (settings && settings.pages) {
872
- const active_page = settings.pages.find(p => p.active === true);
873
- if (active_page) {
874
- let PAGE_KEY = "instagram-page-" + active_page.id;
875
- await db.remove(PAGE_KEY)
876
- winston.debug("(fbm) Page deleted.");
877
- }
878
- }
879
-
880
- await db.remove(CONTENT_KEY)
881
- winston.verbose("(fbm) Content deleted.");
882
-
883
- const tdClient = new TiledeskSubscriptionClient({ API_URL: API_URL, project_id: project_id, token: token })
884
- if (settings && settings.subscription_id) {
885
- tdClient.unsubscribe(settings.subscription_id).catch((err) => {
886
- winston.error("(fbm) unsubscribe error: ", err);
887
- });
850
+ if (settings) {
851
+ settings.ig_oauth_token = null;
852
+ settings.instagram_username = null;
853
+ settings.instagram_business_id = null;
854
+ settings.connected = false;
855
+ await db.set(CONTENT_KEY, settings);
856
+ winston.info("(ig) OAuth token cleared for project " + project_id);
888
857
  }
889
858
 
890
859
  const tdChannel = new TiledeskChannel({ settings: { project_id: project_id, token: token }, API_URL: API_URL })
891
860
  let departments = await tdChannel.getDepartments(token).catch(() => []);
892
- winston.debug("(fbm) found " + departments.length + " departments")
893
861
 
894
862
  readHTMLFile('/configure.html', (err, html) => {
895
863
  var template = handlebars.compile(html);
@@ -900,12 +868,12 @@ router.post('/disconnect', async (req, res) => {
900
868
  token: token,
901
869
  endpoint: BASE_URL,
902
870
  departments: departments,
903
- brand_name: BRAND_NAME
871
+ brand_name: BRAND_NAME,
872
+ oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token
904
873
  }
905
874
  var html = template(replacements)
906
875
  return res.send(html);
907
876
  })
908
-
909
877
  })
910
878
 
911
879
  function startApp(settings, callback) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@resultcrafter/aimanager-instagram-connector",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "AI Manager Instagram DM connector",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -247,7 +247,7 @@
247
247
  var height = 700;
248
248
  var left = (screen.width - width) / 2;
249
249
  var top = (screen.height - height) / 2;
250
- var oauthUrl = './auth/instagram/start?project_id={{ project_id }}&token={{ token }}&app_id={{ app_id }}';
250
+ var oauthUrl = '{{ oauth_url }}';
251
251
  var popup = window.open(oauthUrl, 'instagram-oauth', 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top);
252
252
 
253
253
  var timer = setInterval(function() {