@resultcrafter/aimanager-instagram-connector 0.1.2 → 0.2.1
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 +218 -0
- package/index.js +122 -119
- package/package.json +1 -1
- package/template/configure.html +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
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 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
|
+
|
|
70
|
+
function generateState(sessionId, projectId) {
|
|
71
|
+
var data = {
|
|
72
|
+
sessionId: sessionId || crypto.randomBytes(16).toString('hex'),
|
|
73
|
+
project_id: projectId,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
nonce: crypto.randomBytes(8).toString('hex')
|
|
76
|
+
};
|
|
77
|
+
var cfg = getConfig();
|
|
78
|
+
var hmac = crypto.createHmac('sha256', cfg.stateSecret);
|
|
79
|
+
hmac.update(JSON.stringify(data));
|
|
80
|
+
var state = Buffer.from(JSON.stringify({
|
|
81
|
+
data: data,
|
|
82
|
+
signature: hmac.digest('hex')
|
|
83
|
+
})).toString('base64url');
|
|
84
|
+
return state;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function verifyState(state) {
|
|
88
|
+
try {
|
|
89
|
+
var cfg = getConfig();
|
|
90
|
+
var decoded = JSON.parse(Buffer.from(state, 'base64url').toString());
|
|
91
|
+
var hmac = crypto.createHmac('sha256', cfg.stateSecret);
|
|
92
|
+
hmac.update(JSON.stringify(decoded.data));
|
|
93
|
+
if (decoded.signature !== hmac.digest('hex')) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (Date.now() - decoded.data.timestamp > 600000) return null;
|
|
97
|
+
return decoded.data;
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getAuthorizationUrl(projectId, token, appId) {
|
|
104
|
+
var cfg = getConfig();
|
|
105
|
+
if (!cfg.appId) throw new Error('INSTAGRAM_APP_ID not configured');
|
|
106
|
+
if (!cfg.redirectUri) throw new Error('INSTAGRAM_REDIRECT_URI not configured');
|
|
107
|
+
var state = generateState(null, projectId);
|
|
108
|
+
var params = new URLSearchParams({
|
|
109
|
+
client_id: cfg.appId,
|
|
110
|
+
redirect_uri: cfg.redirectUri,
|
|
111
|
+
scope: cfg.scopes,
|
|
112
|
+
response_type: 'code',
|
|
113
|
+
state: state
|
|
114
|
+
});
|
|
115
|
+
return { url: INSTAGRAM_OAUTH_BASE + '/authorize?' + params.toString(), state: state };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function exchangeCodeForToken(code) {
|
|
119
|
+
var cfg = getConfig();
|
|
120
|
+
if (!cfg.appId || !cfg.appSecret) throw new Error('Instagram app credentials not configured');
|
|
121
|
+
var tokenRes = await axios.post(INSTAGRAM_API_BASE + '/access_token',
|
|
122
|
+
new URLSearchParams({
|
|
123
|
+
client_id: cfg.appId,
|
|
124
|
+
client_secret: cfg.appSecret,
|
|
125
|
+
grant_type: 'authorization_code',
|
|
126
|
+
redirect_uri: cfg.redirectUri,
|
|
127
|
+
code: code
|
|
128
|
+
}),
|
|
129
|
+
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
|
130
|
+
);
|
|
131
|
+
var shortToken = tokenRes.data.access_token;
|
|
132
|
+
var userId = tokenRes.data.user_id;
|
|
133
|
+
var longRes = await axios.get(INSTAGRAM_GRAPH_BASE + '/access_token', {
|
|
134
|
+
params: { grant_type: 'ig_exchange_token', client_secret: cfg.appSecret, access_token: shortToken }
|
|
135
|
+
});
|
|
136
|
+
var longToken = longRes.data.access_token;
|
|
137
|
+
var userInfo = await getUserInfo(longToken);
|
|
138
|
+
return {
|
|
139
|
+
access_token: longToken,
|
|
140
|
+
token_type: longRes.data.token_type,
|
|
141
|
+
expires_in: longRes.data.expires_in,
|
|
142
|
+
user_id: userId,
|
|
143
|
+
instagram_business_id: userInfo.instagram_business_id || userInfo.user_id || userId,
|
|
144
|
+
username: userInfo.username,
|
|
145
|
+
user_info: userInfo,
|
|
146
|
+
created_at: Date.now(),
|
|
147
|
+
expires_at: Date.now() + (longRes.data.expires_in * 1000)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function refreshToken(currentToken, existingTokenData) {
|
|
152
|
+
var cfg = getConfig();
|
|
153
|
+
var res = await axios.get(INSTAGRAM_GRAPH_BASE + '/refresh_access_token', {
|
|
154
|
+
params: { grant_type: 'ig_refresh_token', access_token: currentToken }
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
access_token: res.data.access_token,
|
|
158
|
+
token_type: res.data.token_type,
|
|
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,
|
|
164
|
+
created_at: Date.now(),
|
|
165
|
+
expires_at: Date.now() + (res.data.expires_in * 1000)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function getUserInfo(accessToken) {
|
|
170
|
+
try {
|
|
171
|
+
var res = await axios.get(INSTAGRAM_GRAPH_BASE + '/v23.0/me', {
|
|
172
|
+
params: {
|
|
173
|
+
fields: 'id,user_id,username,name,account_type,media_count,followers_count,follows_count,profile_picture_url',
|
|
174
|
+
access_token: accessToken
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
app_scoped_id: res.data.id,
|
|
179
|
+
instagram_business_id: res.data.user_id,
|
|
180
|
+
username: res.data.username,
|
|
181
|
+
name: res.data.name,
|
|
182
|
+
account_type: res.data.account_type,
|
|
183
|
+
profile_picture_url: res.data.profile_picture_url
|
|
184
|
+
};
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return { username: null };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function getTokenStatus(db, projectId) {
|
|
191
|
+
var settings = await db.get('instagram-' + projectId);
|
|
192
|
+
if (!settings || !settings.ig_oauth_token) {
|
|
193
|
+
return { connected: false, instagram_username: null };
|
|
194
|
+
}
|
|
195
|
+
var token = decryptToken(settings.ig_oauth_token);
|
|
196
|
+
if (!token) {
|
|
197
|
+
return { connected: false, instagram_username: null };
|
|
198
|
+
}
|
|
199
|
+
var daysRemaining = token.expires_at ? Math.floor((token.expires_at - Date.now()) / (1000 * 60 * 60 * 24)) : null;
|
|
200
|
+
return {
|
|
201
|
+
connected: true,
|
|
202
|
+
instagram_username: token.username || settings.instagram_username || 'Instagram Account',
|
|
203
|
+
user_id: token.instagram_business_id || token.user_id,
|
|
204
|
+
days_remaining: daysRemaining,
|
|
205
|
+
needs_refresh: daysRemaining !== null ? daysRemaining < 7 : false
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
getAuthorizationUrl: getAuthorizationUrl,
|
|
211
|
+
exchangeCodeForToken: exchangeCodeForToken,
|
|
212
|
+
refreshToken: refreshToken,
|
|
213
|
+
getUserInfo: getUserInfo,
|
|
214
|
+
verifyState: verifyState,
|
|
215
|
+
getTokenStatus: getTokenStatus,
|
|
216
|
+
encryptToken: encryptToken,
|
|
217
|
+
decryptToken: decryptToken
|
|
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');
|
|
@@ -16,6 +17,7 @@ const { TiledeskInstagramTranslator } = require('./aimanager/TiledeskInstagramTr
|
|
|
16
17
|
const { TiledeskSubscriptionClient } = require('./aimanager/TiledeskSubscriptionClient');
|
|
17
18
|
const { FacebookClient } = require('./aimanager/FacebookClient');
|
|
18
19
|
const { MessageHandler } = require('./aimanager/MessageHandler');
|
|
20
|
+
const instagramOAuth = require('./aimanager/InstagramOAuth');
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
// mongo
|
|
@@ -110,13 +112,14 @@ router.post('/install', async (req, res) => {
|
|
|
110
112
|
query: {
|
|
111
113
|
"project_id": project_id,
|
|
112
114
|
"app_id": app_id,
|
|
113
|
-
"token": token
|
|
115
|
+
"token": token,
|
|
116
|
+
"oauth_url": BASE_URL + '/auth/instagram/start'
|
|
114
117
|
}
|
|
115
118
|
}));
|
|
116
119
|
|
|
117
120
|
}).catch((err) => {
|
|
118
|
-
winston.error("(
|
|
119
|
-
winston.error("(
|
|
121
|
+
winston.error("(ig) installation error: ", err.data)
|
|
122
|
+
winston.error("(ig) installation error: " + err.data)
|
|
120
123
|
res.send("An error occurred during the installation");
|
|
121
124
|
})
|
|
122
125
|
|
|
@@ -184,6 +187,8 @@ router.get('/configure', async (req, res) => {
|
|
|
184
187
|
return []
|
|
185
188
|
})
|
|
186
189
|
|
|
190
|
+
var tokenStatus = await instagramOAuth.getTokenStatus(db, project_id);
|
|
191
|
+
|
|
187
192
|
if (settings) {
|
|
188
193
|
winston.debug("(fbm) settings found: ", settings);
|
|
189
194
|
|
|
@@ -192,7 +197,7 @@ router.get('/configure', async (req, res) => {
|
|
|
192
197
|
var replacements = {
|
|
193
198
|
app_version: pjson.version,
|
|
194
199
|
project_id: project_id,
|
|
195
|
-
ig_token:
|
|
200
|
+
ig_token: tokenStatus.connected ? tokenStatus.instagram_username : null,
|
|
196
201
|
token: token,
|
|
197
202
|
app_id: app_id,
|
|
198
203
|
endpoint: BASE_URL,
|
|
@@ -201,9 +206,10 @@ router.get('/configure', async (req, res) => {
|
|
|
201
206
|
department_id: settings.department_id,
|
|
202
207
|
departments: departments,
|
|
203
208
|
brand_name: BRAND_NAME,
|
|
204
|
-
instagram_username:
|
|
209
|
+
instagram_username: tokenStatus.instagram_username || 'Instagram Account',
|
|
205
210
|
show_info_message: settings.show_info_message !== undefined ? settings.show_info_message : true,
|
|
206
|
-
subscription_id: settings.subscription_id
|
|
211
|
+
subscription_id: settings.subscription_id,
|
|
212
|
+
oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token + '&app_id=' + app_id
|
|
207
213
|
}
|
|
208
214
|
var html = template(replacements)
|
|
209
215
|
res.send(html);
|
|
@@ -221,7 +227,8 @@ router.get('/configure', async (req, res) => {
|
|
|
221
227
|
app_id: app_id,
|
|
222
228
|
endpoint: BASE_URL,
|
|
223
229
|
departments: departments,
|
|
224
|
-
brand_name: BRAND_NAME
|
|
230
|
+
brand_name: BRAND_NAME,
|
|
231
|
+
oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token + '&app_id=' + app_id
|
|
225
232
|
}
|
|
226
233
|
var html = template(replacements);
|
|
227
234
|
res.send(html);
|
|
@@ -232,7 +239,7 @@ router.get('/configure', async (req, res) => {
|
|
|
232
239
|
})
|
|
233
240
|
|
|
234
241
|
router.post('/update', async (req, res) => {
|
|
235
|
-
winston.verbose("(
|
|
242
|
+
winston.verbose("(ig) /update");
|
|
236
243
|
|
|
237
244
|
let project_id = req.body.project_id;
|
|
238
245
|
let token = req.body.token;
|
|
@@ -247,10 +254,11 @@ router.post('/update', async (req, res) => {
|
|
|
247
254
|
await db.set(CONTENT_KEY, settings);
|
|
248
255
|
}
|
|
249
256
|
|
|
250
|
-
|
|
257
|
+
var tokenStatus = await instagramOAuth.getTokenStatus(db, project_id);
|
|
258
|
+
|
|
251
259
|
const tdChannel = new TiledeskChannel({ settings: { project_id: project_id, token: token }, API_URL: API_URL });
|
|
252
260
|
let departments = await tdChannel.getDepartments(token);
|
|
253
|
-
winston.verbose("(
|
|
261
|
+
winston.verbose("(ig) found " + departments.length + " departments")
|
|
254
262
|
|
|
255
263
|
readHTMLFile('/configure.html', (err, html) => {
|
|
256
264
|
|
|
@@ -258,7 +266,7 @@ router.post('/update', async (req, res) => {
|
|
|
258
266
|
var replacements = {
|
|
259
267
|
app_version: pjson.version,
|
|
260
268
|
project_id: project_id,
|
|
261
|
-
ig_token:
|
|
269
|
+
ig_token: tokenStatus.connected ? tokenStatus.instagram_username : null,
|
|
262
270
|
token: token,
|
|
263
271
|
app_id: app_id,
|
|
264
272
|
endpoint: BASE_URL,
|
|
@@ -268,8 +276,9 @@ router.post('/update', async (req, res) => {
|
|
|
268
276
|
departments: departments,
|
|
269
277
|
brand_name: BRAND_NAME,
|
|
270
278
|
show_info_message: settings ? (settings.show_info_message !== undefined ? settings.show_info_message : true) : true,
|
|
271
|
-
instagram_username:
|
|
272
|
-
subscription_id: settings ? settings.subscription_id : null
|
|
279
|
+
instagram_username: tokenStatus.instagram_username || 'Instagram Account',
|
|
280
|
+
subscription_id: settings ? settings.subscription_id : null,
|
|
281
|
+
oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token + '&app_id=' + app_id
|
|
273
282
|
}
|
|
274
283
|
var html = template(replacements)
|
|
275
284
|
res.send(html);
|
|
@@ -278,7 +287,7 @@ router.post('/update', async (req, res) => {
|
|
|
278
287
|
})
|
|
279
288
|
|
|
280
289
|
router.post('/update_advanced', async (req, res) => {
|
|
281
|
-
winston.verbose("(
|
|
290
|
+
winston.verbose("(ig) /update_advanced");
|
|
282
291
|
|
|
283
292
|
let project_id = req.body.project_id;
|
|
284
293
|
let show_info_message = req.body.show_info_message === 'on';
|
|
@@ -291,6 +300,8 @@ router.post('/update_advanced', async (req, res) => {
|
|
|
291
300
|
await db.set(CONTENT_KEY, settings);
|
|
292
301
|
}
|
|
293
302
|
|
|
303
|
+
var tokenStatus = await instagramOAuth.getTokenStatus(db, project_id);
|
|
304
|
+
|
|
294
305
|
const tdChannel = new TiledeskChannel({ settings: { project_id: project_id, token: req.body.token }, API_URL: API_URL });
|
|
295
306
|
let departments = await tdChannel.getDepartments(req.body.token).catch(() => []);
|
|
296
307
|
|
|
@@ -299,7 +310,7 @@ router.post('/update_advanced', async (req, res) => {
|
|
|
299
310
|
var replacements = {
|
|
300
311
|
app_version: pjson.version,
|
|
301
312
|
project_id: project_id,
|
|
302
|
-
ig_token:
|
|
313
|
+
ig_token: tokenStatus.connected ? tokenStatus.instagram_username : null,
|
|
303
314
|
token: req.body.token,
|
|
304
315
|
endpoint: BASE_URL,
|
|
305
316
|
pages: settings ? settings.pages : null,
|
|
@@ -308,8 +319,9 @@ router.post('/update_advanced', async (req, res) => {
|
|
|
308
319
|
departments: departments,
|
|
309
320
|
brand_name: BRAND_NAME,
|
|
310
321
|
show_info_message: show_info_message,
|
|
311
|
-
instagram_username:
|
|
312
|
-
subscription_id: settings ? settings.subscription_id : null
|
|
322
|
+
instagram_username: tokenStatus.instagram_username || 'Instagram Account',
|
|
323
|
+
subscription_id: settings ? settings.subscription_id : null,
|
|
324
|
+
oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + req.body.token
|
|
313
325
|
}
|
|
314
326
|
var html = template(replacements)
|
|
315
327
|
res.send(html);
|
|
@@ -501,12 +513,45 @@ router.post('/tiledesk', async (req, res) => {
|
|
|
501
513
|
return res.status(200).send({ message: "sent" });
|
|
502
514
|
})
|
|
503
515
|
|
|
516
|
+
function verifyWebhookSignature(req) {
|
|
517
|
+
var signature = req.headers['x-hub-signature-256'];
|
|
518
|
+
if (!signature) {
|
|
519
|
+
winston.error("(ig) Missing x-hub-signature-256 header");
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
if (!APP_SECRET) {
|
|
523
|
+
winston.warn("(ig) APP_SECRET not configured — skipping signature validation");
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
var algo = 'sha256';
|
|
527
|
+
var expected = signature.replace('sha256=', '');
|
|
528
|
+
var hmac = crypto.createHmac(algo, APP_SECRET);
|
|
529
|
+
hmac.update(JSON.stringify(req.body));
|
|
530
|
+
var computed = hmac.digest('hex');
|
|
531
|
+
var valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(computed));
|
|
532
|
+
if (!valid) {
|
|
533
|
+
winston.error("(ig) Webhook signature mismatch — expected=" + expected + " computed=" + computed);
|
|
534
|
+
if (process.env.DEBUG_SIGNATURES === 'true') {
|
|
535
|
+
winston.debug("(ig) Raw body for signature: " + JSON.stringify(req.body));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return valid;
|
|
539
|
+
}
|
|
540
|
+
|
|
504
541
|
router.post('/webhookFB', async (req, res) => {
|
|
505
542
|
|
|
506
|
-
winston.verbose("(
|
|
543
|
+
winston.verbose("(ig) Webhook received");
|
|
507
544
|
|
|
508
|
-
|
|
509
|
-
|
|
545
|
+
if (!verifyWebhookSignature(req)) {
|
|
546
|
+
return res.sendStatus(403);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
res.status(200).send('EVENT_RECEIVED');
|
|
550
|
+
|
|
551
|
+
setImmediate(async () => {
|
|
552
|
+
try {
|
|
553
|
+
let body = req.body;
|
|
554
|
+
winston.verbose("(ig) Processing webhook asynchronously");
|
|
510
555
|
|
|
511
556
|
let page_id = body.entry[0].id;
|
|
512
557
|
let PAGE_KEY = "instagram-page-" + page_id;
|
|
@@ -595,9 +640,10 @@ router.post('/webhookFB', async (req, res) => {
|
|
|
595
640
|
}
|
|
596
641
|
|
|
597
642
|
})
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
643
|
+
} catch (err) {
|
|
644
|
+
winston.error("(ig) Async webhook processing error: ", err?.response?.data || err.message || err);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
601
647
|
})
|
|
602
648
|
|
|
603
649
|
router.get('/webhookFB', async (req, res) => {
|
|
@@ -632,93 +678,63 @@ router.get('/webhookFB', async (req, res) => {
|
|
|
632
678
|
return res.sendStatus(403);
|
|
633
679
|
})
|
|
634
680
|
|
|
635
|
-
router.get('/
|
|
636
|
-
winston.verbose("(
|
|
681
|
+
router.get('/auth/instagram/start', async (req, res) => {
|
|
682
|
+
winston.verbose("(ig) /auth/instagram/start");
|
|
637
683
|
|
|
638
|
-
let project_id =
|
|
639
|
-
|
|
640
|
-
let
|
|
641
|
-
let app_id = JSON.parse(req.query.state).app_id;
|
|
642
|
-
|
|
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 });
|
|
684
|
+
let project_id = req.query.project_id;
|
|
685
|
+
let token = req.query.token;
|
|
686
|
+
let app_id = req.query.app_id;
|
|
645
687
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
event: 'message.create.request.channel.instagram',
|
|
688
|
+
if (!project_id) {
|
|
689
|
+
return res.status(400).send('Missing project_id parameter');
|
|
649
690
|
}
|
|
650
691
|
|
|
651
|
-
let subscription, access_token, pages_list;
|
|
652
692
|
try {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
693
|
+
var auth = instagramOAuth.getAuthorizationUrl(project_id, token, app_id);
|
|
694
|
+
winston.info("(ig) Redirecting to Instagram OAuth for project " + project_id);
|
|
695
|
+
res.redirect(auth.url);
|
|
656
696
|
} catch (err) {
|
|
657
|
-
winston.error("(
|
|
658
|
-
|
|
697
|
+
winston.error("(ig) OAuth start error: ", err.message);
|
|
698
|
+
res.status(500).send('OAuth configuration error: ' + err.message);
|
|
659
699
|
}
|
|
700
|
+
});
|
|
660
701
|
|
|
661
|
-
|
|
662
|
-
winston.
|
|
663
|
-
winston.debug("(fbm) pages_list: " + pages_list);
|
|
664
|
-
|
|
665
|
-
let CONTENT_KEY = "instagram-" + project_id;
|
|
666
|
-
|
|
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
|
-
})
|
|
702
|
+
router.get('/oauth-callback', async (req, res) => {
|
|
703
|
+
winston.verbose("(ig) /oauth-callback");
|
|
684
704
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
secret: subscription.secret,
|
|
691
|
-
pages: pages,
|
|
692
|
-
app_id: app_id
|
|
705
|
+
var code = req.query.code;
|
|
706
|
+
var stateStr = req.query.state;
|
|
707
|
+
if (!code || !stateStr) {
|
|
708
|
+
winston.error("(ig) Missing code or state parameter");
|
|
709
|
+
return res.status(400).send("Missing authorization code or state");
|
|
693
710
|
}
|
|
694
711
|
|
|
695
|
-
|
|
712
|
+
var state = instagramOAuth.verifyState(stateStr);
|
|
713
|
+
if (!state) {
|
|
714
|
+
winston.error("(ig) Invalid or expired state");
|
|
715
|
+
return res.status(400).send("Invalid or expired state parameter. Please try connecting again.");
|
|
716
|
+
}
|
|
696
717
|
|
|
697
|
-
|
|
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);
|
|
718
|
+
var projectId = state.project_id;
|
|
702
719
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
secret: settings.secret,
|
|
715
|
-
pages: settings.pages
|
|
716
|
-
}
|
|
717
|
-
var html = template(replacements)
|
|
718
|
-
res.send(html);
|
|
719
|
-
})
|
|
720
|
-
*/
|
|
720
|
+
try {
|
|
721
|
+
var tokenData = await instagramOAuth.exchangeCodeForToken(code);
|
|
722
|
+
winston.info("(ig) Token obtained for project " + projectId + ", user: " + (tokenData.username || 'unknown'));
|
|
723
|
+
|
|
724
|
+
var CONTENT_KEY = "instagram-" + projectId;
|
|
725
|
+
var settings = await db.get(CONTENT_KEY) || {};
|
|
726
|
+
settings.ig_oauth_token = instagramOAuth.encryptToken(tokenData);
|
|
727
|
+
settings.instagram_username = tokenData.username;
|
|
728
|
+
settings.instagram_business_id = tokenData.instagram_business_id;
|
|
729
|
+
settings.connected = true;
|
|
730
|
+
await db.set(CONTENT_KEY, settings);
|
|
721
731
|
|
|
732
|
+
var redirect_uri = DASHBOARD_BASE_URL + "/#/project/" + projectId + "/integrations?name=instagram";
|
|
733
|
+
res.redirect(redirect_uri);
|
|
734
|
+
} catch (err) {
|
|
735
|
+
winston.error("(ig) OAuth callback error: ", err?.response?.data || err.message || err);
|
|
736
|
+
res.status(500).send("Authentication failed: " + (err.message || 'Unknown error'));
|
|
737
|
+
}
|
|
722
738
|
})
|
|
723
739
|
|
|
724
740
|
router.post('/enablePage', async (req, res) => {
|
|
@@ -858,38 +874,25 @@ router.post('/disablePage', async (req, res) => {
|
|
|
858
874
|
})
|
|
859
875
|
|
|
860
876
|
router.post('/disconnect', async (req, res) => {
|
|
861
|
-
winston.verbose("(
|
|
877
|
+
winston.verbose("(ig) /disconnect");
|
|
862
878
|
|
|
863
879
|
let project_id = req.body.project_id;
|
|
864
880
|
let token = req.body.token;
|
|
865
|
-
let subscription_id = req.body.subscription_id;
|
|
866
881
|
|
|
867
882
|
let CONTENT_KEY = "instagram-" + project_id;
|
|
868
|
-
|
|
869
883
|
let settings = await db.get(CONTENT_KEY);
|
|
870
884
|
|
|
871
|
-
if (settings
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
});
|
|
885
|
+
if (settings) {
|
|
886
|
+
settings.ig_oauth_token = null;
|
|
887
|
+
settings.instagram_username = null;
|
|
888
|
+
settings.instagram_business_id = null;
|
|
889
|
+
settings.connected = false;
|
|
890
|
+
await db.set(CONTENT_KEY, settings);
|
|
891
|
+
winston.info("(ig) OAuth token cleared for project " + project_id);
|
|
888
892
|
}
|
|
889
893
|
|
|
890
894
|
const tdChannel = new TiledeskChannel({ settings: { project_id: project_id, token: token }, API_URL: API_URL })
|
|
891
895
|
let departments = await tdChannel.getDepartments(token).catch(() => []);
|
|
892
|
-
winston.debug("(fbm) found " + departments.length + " departments")
|
|
893
896
|
|
|
894
897
|
readHTMLFile('/configure.html', (err, html) => {
|
|
895
898
|
var template = handlebars.compile(html);
|
|
@@ -900,12 +903,12 @@ router.post('/disconnect', async (req, res) => {
|
|
|
900
903
|
token: token,
|
|
901
904
|
endpoint: BASE_URL,
|
|
902
905
|
departments: departments,
|
|
903
|
-
brand_name: BRAND_NAME
|
|
906
|
+
brand_name: BRAND_NAME,
|
|
907
|
+
oauth_url: BASE_URL + '/auth/instagram/start?project_id=' + project_id + '&token=' + token
|
|
904
908
|
}
|
|
905
909
|
var html = template(replacements)
|
|
906
910
|
return res.send(html);
|
|
907
911
|
})
|
|
908
|
-
|
|
909
912
|
})
|
|
910
913
|
|
|
911
914
|
function startApp(settings, callback) {
|
package/package.json
CHANGED
package/template/configure.html
CHANGED
|
@@ -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 = '
|
|
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() {
|