nodejs-insta-private-api-mqt 1.4.7 → 1.4.8
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/README.md +776 -107
- package/dist/dist/fbns/fbns.client.js +39 -6
- package/dist/dist/index.js +2 -0
- package/dist/dist/mqttot/mqttot.client.js +70 -39
- package/dist/dist/realtime/commands/direct.commands.js +0 -2
- package/dist/dist/realtime/commands/enhanced.direct.commands.js +0 -2
- package/dist/dist/realtime/disconnect-reason.js +18 -0
- package/dist/dist/realtime/features/dm-sender.js +0 -2
- package/dist/dist/realtime/features/session-health-monitor.js +0 -3
- package/dist/dist/realtime/mixins/message-sync.mixin.js +0 -16
- package/dist/dist/realtime/realtime.client.js +55 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ By leveraging MQTT instead of Instagram's REST API, this library achieves sub-50
|
|
|
53
53
|
- **View-Once Media** - Download disappearing photos/videos before they expire
|
|
54
54
|
- **Raven (View-Once) Sending** - Send view-once and replayable photos/videos via REST
|
|
55
55
|
- **sendPhoto() / sendVideo()** - Upload and send media directly via MQTT
|
|
56
|
-
- **Session persistence** - Multi-file auth state
|
|
56
|
+
- **Session persistence** - Multi-file auth state for seamless reconnects
|
|
57
57
|
- **Automatic reconnection** - Smart error classification with type-specific backoff
|
|
58
58
|
- **Session health monitoring** - Auto-relogin, uptime tracking
|
|
59
59
|
- **Persistent logging** - File-based logging with rotation
|
|
@@ -954,7 +954,7 @@ await realtime.directCommands.sendForegroundState({
|
|
|
954
954
|
|
|
955
955
|
## Download Media from Messages
|
|
956
956
|
|
|
957
|
-
This feature provides
|
|
957
|
+
This feature provides persistent media download for Instagram DM messages.
|
|
958
958
|
|
|
959
959
|
### Quick Start: Save View-Once Photo
|
|
960
960
|
|
|
@@ -1012,93 +1012,461 @@ realtime.on('message', async (data) => {
|
|
|
1012
1012
|
|
|
1013
1013
|
## Building Instagram Bots
|
|
1014
1014
|
|
|
1015
|
-
|
|
1015
|
+
The core of any bot built with this library is the `connection.update` event. It fires every time the MQTT connection state changes and tells you exactly what happened — whether you just connected, got disconnected, or the session expired. Combined with the `message` event for incoming DMs, these two hooks are all you need for most bots.
|
|
1016
|
+
|
|
1017
|
+
Both `client.on('connection.update', ...)` and `client.ev.on('connection.update', ...)` do exactly the same thing. The `.ev` property is just an alias for the client itself — use whichever style you prefer.
|
|
1018
|
+
|
|
1019
|
+
---
|
|
1020
|
+
|
|
1021
|
+
### Minimal Working Bot
|
|
1022
|
+
|
|
1023
|
+
The smallest complete bot you can write: logs in, connects to MQTT, replies "pong" to any "ping" message, and handles session expiry gracefully.
|
|
1016
1024
|
|
|
1017
1025
|
```javascript
|
|
1018
|
-
const {
|
|
1019
|
-
|
|
1026
|
+
const {
|
|
1027
|
+
IgApiClient,
|
|
1028
|
+
RealtimeClient,
|
|
1029
|
+
useMultiFileAuthState,
|
|
1030
|
+
GraphQLSubscriptions,
|
|
1031
|
+
DisconnectReason,
|
|
1032
|
+
} = require('nodejs-insta-private-api-mqt');
|
|
1020
1033
|
|
|
1021
|
-
|
|
1034
|
+
async function main() {
|
|
1022
1035
|
const ig = new IgApiClient();
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1036
|
+
const auth = await useMultiFileAuthState('./session');
|
|
1037
|
+
|
|
1038
|
+
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
|
|
1039
|
+
|
|
1040
|
+
if (auth.hasSession()) {
|
|
1041
|
+
await auth.loadCreds(ig);
|
|
1042
|
+
const valid = await auth.isSessionValid(ig).catch(() => false);
|
|
1043
|
+
if (!valid) {
|
|
1044
|
+
const result = await ig.login({ username: 'your_username', password: 'your_password' });
|
|
1045
|
+
await auth.saveCreds(ig);
|
|
1046
|
+
}
|
|
1047
|
+
} else {
|
|
1048
|
+
const result = await ig.login({ username: 'your_username', password: 'your_password' });
|
|
1049
|
+
await auth.saveCreds(ig);
|
|
1050
|
+
}
|
|
1025
1051
|
|
|
1026
1052
|
const realtime = new RealtimeClient(ig);
|
|
1027
|
-
|
|
1028
|
-
|
|
1053
|
+
|
|
1054
|
+
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
1055
|
+
if (connection === 'connecting') {
|
|
1056
|
+
console.log('Connecting to Instagram MQTT...');
|
|
1057
|
+
} else if (connection === 'open') {
|
|
1058
|
+
console.log('Connected! Bot is live.');
|
|
1059
|
+
} else if (connection === 'close') {
|
|
1060
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
1061
|
+
if (code === DisconnectReason.loggedOut) {
|
|
1062
|
+
console.log('Session expired. Delete the ./session folder and re-run the bot.');
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
} else {
|
|
1065
|
+
console.log('Disconnected, will reconnect automatically...');
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
realtime.on('message', async (data) => {
|
|
1071
|
+
const msg = data.message || data.parsed;
|
|
1072
|
+
if (!msg?.text || !msg.thread_id) return;
|
|
1073
|
+
|
|
1074
|
+
if (msg.text.toLowerCase() === 'ping') {
|
|
1075
|
+
await realtime.directCommands.sendText({ threadId: msg.thread_id, text: 'pong!' });
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
const userId = ig.state.cookieUserId;
|
|
1029
1080
|
await realtime.connect({
|
|
1030
|
-
graphQlSubs: [
|
|
1031
|
-
|
|
1081
|
+
graphQlSubs: [
|
|
1082
|
+
userId && GraphQLSubscriptions.getDirectTypingSubscription(userId),
|
|
1083
|
+
GraphQLSubscriptions.getAppPresenceSubscription(),
|
|
1084
|
+
GraphQLSubscriptions.getDirectStatusSubscription(),
|
|
1085
|
+
].filter(Boolean),
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
await new Promise(() => {});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
main().catch(console.error);
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
---
|
|
1095
|
+
|
|
1096
|
+
### Production Bot — Full Lifecycle Handling
|
|
1097
|
+
|
|
1098
|
+
A production bot needs to survive everything: sessions that expire without warning, rate limits, network drops, and server hiccups. This example shows the full setup with every disconnect reason handled and a graceful shutdown on Ctrl+C.
|
|
1099
|
+
|
|
1100
|
+
```javascript
|
|
1101
|
+
const {
|
|
1102
|
+
IgApiClient,
|
|
1103
|
+
RealtimeClient,
|
|
1104
|
+
useMultiFileAuthState,
|
|
1105
|
+
GraphQLSubscriptions,
|
|
1106
|
+
DisconnectReason,
|
|
1107
|
+
} = require('nodejs-insta-private-api-mqt');
|
|
1108
|
+
|
|
1109
|
+
const SESSION_FOLDER = './bot_session';
|
|
1110
|
+
const USERNAME = 'your_username';
|
|
1111
|
+
const PASSWORD = 'your_password';
|
|
1112
|
+
|
|
1113
|
+
async function startBot() {
|
|
1114
|
+
const ig = new IgApiClient();
|
|
1115
|
+
const auth = await useMultiFileAuthState(SESSION_FOLDER);
|
|
1116
|
+
|
|
1117
|
+
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
|
|
1118
|
+
|
|
1119
|
+
// Restore or create session
|
|
1120
|
+
if (auth.hasSession()) {
|
|
1121
|
+
await auth.loadCreds(ig);
|
|
1122
|
+
const valid = await auth.isSessionValid(ig).catch(() => false);
|
|
1123
|
+
if (!valid) {
|
|
1124
|
+
console.log('Session expired, logging in again...');
|
|
1125
|
+
const result = await ig.login({ username: USERNAME, password: PASSWORD });
|
|
1126
|
+
await auth.saveCreds(ig);
|
|
1127
|
+
console.log('Logged in, session saved.');
|
|
1128
|
+
} else {
|
|
1129
|
+
console.log('Session restored.');
|
|
1130
|
+
}
|
|
1131
|
+
} else {
|
|
1132
|
+
console.log('No session found, logging in...');
|
|
1133
|
+
const result = await ig.login({ username: USERNAME, password: PASSWORD });
|
|
1134
|
+
await auth.saveCreds(ig);
|
|
1135
|
+
console.log('Logged in, session saved.');
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const realtime = new RealtimeClient(ig);
|
|
1139
|
+
const userId = ig.state.cookieUserId;
|
|
1140
|
+
|
|
1141
|
+
// Connection lifecycle — this is how you react to every state change
|
|
1142
|
+
realtime.ev.on('connection.update', ({ connection, lastDisconnect, isNewLogin }) => {
|
|
1143
|
+
if (connection === 'connecting') {
|
|
1144
|
+
console.log('[BOT] Connecting...');
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (connection === 'open') {
|
|
1149
|
+
console.log(`[BOT] Connected${isNewLogin ? ' (first login)' : ''}`);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (connection === 'close') {
|
|
1154
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
1155
|
+
const msg = lastDisconnect?.error?.message;
|
|
1156
|
+
|
|
1157
|
+
switch (code) {
|
|
1158
|
+
case DisconnectReason.loggedOut:
|
|
1159
|
+
// Instagram invalidated the session — must log in fresh
|
|
1160
|
+
console.error('[BOT] Logged out by Instagram. Delete session and re-run.');
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
break;
|
|
1163
|
+
|
|
1164
|
+
case DisconnectReason.rateLimited:
|
|
1165
|
+
// Too many requests — library backs off automatically
|
|
1166
|
+
console.warn('[BOT] Rate limited. Backing off before next reconnect...');
|
|
1167
|
+
break;
|
|
1168
|
+
|
|
1169
|
+
case DisconnectReason.connectionClosed:
|
|
1170
|
+
// You called realtime.disconnect() — expected
|
|
1171
|
+
console.log('[BOT] Disconnected intentionally.');
|
|
1172
|
+
break;
|
|
1173
|
+
|
|
1174
|
+
case DisconnectReason.timedOut:
|
|
1175
|
+
// Keepalive ping failed — library will reconnect
|
|
1176
|
+
console.warn('[BOT] Connection timed out, reconnecting...');
|
|
1177
|
+
break;
|
|
1178
|
+
|
|
1179
|
+
case DisconnectReason.reconnectFailed:
|
|
1180
|
+
// All retry attempts exhausted
|
|
1181
|
+
console.error(`[BOT] Reconnect failed after all attempts: ${msg}`);
|
|
1182
|
+
process.exit(1);
|
|
1183
|
+
break;
|
|
1184
|
+
|
|
1185
|
+
default:
|
|
1186
|
+
// Network drop, server error, etc. — library auto-reconnects
|
|
1187
|
+
console.warn(`[BOT] Disconnected (code ${code}): ${msg}`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
// Fires when the library successfully reconnects after a drop
|
|
1193
|
+
realtime.on('reconnected', ({ attempt }) => {
|
|
1194
|
+
console.log(`[BOT] Reconnected on attempt #${attempt}`);
|
|
1032
1195
|
});
|
|
1033
1196
|
|
|
1034
|
-
|
|
1197
|
+
// Fires when all reconnect attempts have been exhausted
|
|
1198
|
+
realtime.on('reconnect_failed', ({ attempts, lastErrorType }) => {
|
|
1199
|
+
console.error(`[BOT] Failed to reconnect after ${attempts} attempts. Last error type: ${lastErrorType}`);
|
|
1200
|
+
process.exit(1);
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
// Fires after 3+ consecutive auth failures
|
|
1204
|
+
realtime.on('auth_failure', ({ count }) => {
|
|
1205
|
+
console.error(`[BOT] Auth failed ${count} times in a row. Session is probably expired.`);
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
// Non-fatal warnings (bad payload, unknown topic, etc.)
|
|
1209
|
+
realtime.on('warning', (w) => {
|
|
1210
|
+
console.warn('[BOT] Warning:', w?.message || w);
|
|
1211
|
+
});
|
|
1035
1212
|
|
|
1213
|
+
// Incoming DMs
|
|
1036
1214
|
realtime.on('message', async (data) => {
|
|
1037
|
-
const msg = data.message;
|
|
1038
|
-
if (!msg?.text) return;
|
|
1215
|
+
const msg = data.message || data.parsed;
|
|
1216
|
+
if (!msg?.text || !msg.thread_id) return;
|
|
1039
1217
|
|
|
1040
|
-
|
|
1218
|
+
const threadId = msg.thread_id;
|
|
1219
|
+
const itemId = msg.item_id || msg.messageId;
|
|
1220
|
+
const text = msg.text.trim();
|
|
1041
1221
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1222
|
+
// Mark as seen
|
|
1223
|
+
if (itemId) {
|
|
1224
|
+
await realtime.directCommands.markAsSeen({ threadId, itemId }).catch(() => {});
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
console.log(`[MSG] ${msg.from_user_id || msg.userId}: "${text}"`);
|
|
1228
|
+
|
|
1229
|
+
if (text.toLowerCase() === 'ping') {
|
|
1230
|
+
await realtime.directCommands.sendText({ threadId, text: 'pong!' });
|
|
1047
1231
|
}
|
|
1048
1232
|
});
|
|
1049
1233
|
|
|
1234
|
+
// Typing indicators
|
|
1235
|
+
realtime.on('typing', (data) => {
|
|
1236
|
+
console.log(`[TYPING] Thread ${data?.thread_id || data?.threadId}`);
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
// Fetch inbox for iris data (needed for full message sync)
|
|
1240
|
+
let irisData = null;
|
|
1241
|
+
try {
|
|
1242
|
+
irisData = await ig.request.send({ url: '/api/v1/direct_v2/inbox/', method: 'GET' });
|
|
1243
|
+
} catch (_) {}
|
|
1244
|
+
|
|
1245
|
+
// Connect with subscriptions
|
|
1246
|
+
await realtime.connect({
|
|
1247
|
+
irisData: irisData || undefined,
|
|
1248
|
+
graphQlSubs: [
|
|
1249
|
+
userId && GraphQLSubscriptions.getDirectTypingSubscription(userId),
|
|
1250
|
+
GraphQLSubscriptions.getAppPresenceSubscription(),
|
|
1251
|
+
GraphQLSubscriptions.getDirectStatusSubscription(),
|
|
1252
|
+
userId && GraphQLSubscriptions.getAsyncAdSubscription(userId),
|
|
1253
|
+
GraphQLSubscriptions.getClientConfigUpdateSubscription(),
|
|
1254
|
+
].filter(Boolean),
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
console.log('[BOT] Bot is running. Send it a DM to test.');
|
|
1258
|
+
|
|
1259
|
+
// Graceful shutdown
|
|
1260
|
+
process.on('SIGINT', () => {
|
|
1261
|
+
console.log('[BOT] Shutting down...');
|
|
1262
|
+
realtime.disconnect();
|
|
1263
|
+
process.exit(0);
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1050
1266
|
await new Promise(() => {});
|
|
1051
|
-
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
startBot().catch(console.error);
|
|
1052
1270
|
```
|
|
1053
1271
|
|
|
1054
|
-
|
|
1272
|
+
---
|
|
1273
|
+
|
|
1274
|
+
### Smart Bot with Typing, Read Receipts, and Reactions
|
|
1275
|
+
|
|
1276
|
+
This pattern is what makes a bot feel human: mark the message as seen right away, show a typing indicator while "thinking", send the reply, then react to the original message.
|
|
1055
1277
|
|
|
1056
1278
|
```javascript
|
|
1057
1279
|
realtime.on('message', async (data) => {
|
|
1058
|
-
const msg = data.message;
|
|
1059
|
-
if (!msg?.text) return;
|
|
1280
|
+
const msg = data.message || data.parsed;
|
|
1281
|
+
if (!msg?.text || !msg.thread_id) return;
|
|
1060
1282
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
itemId: msg.item_id
|
|
1065
|
-
});
|
|
1283
|
+
const threadId = msg.thread_id;
|
|
1284
|
+
const itemId = msg.item_id || msg.messageId;
|
|
1285
|
+
const text = msg.text.toLowerCase().trim();
|
|
1066
1286
|
|
|
1067
|
-
//
|
|
1068
|
-
|
|
1069
|
-
threadId
|
|
1070
|
-
|
|
1071
|
-
|
|
1287
|
+
// 1. Mark as seen immediately (sender sees blue "Seen")
|
|
1288
|
+
if (itemId) {
|
|
1289
|
+
await realtime.directCommands.markAsSeen({ threadId, itemId }).catch(() => {});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// 2. Show typing indicator
|
|
1293
|
+
await realtime.directCommands.indicateActivity({ threadId, isActive: true });
|
|
1072
1294
|
|
|
1073
|
-
|
|
1295
|
+
// 3. Small delay to look human
|
|
1296
|
+
await new Promise(r => setTimeout(r, 1500 + Math.random() * 1500));
|
|
1074
1297
|
|
|
1075
|
-
|
|
1298
|
+
// 4. Send reply based on message content
|
|
1299
|
+
if (text === 'hi' || text === 'hello' || text === 'hey') {
|
|
1300
|
+
await realtime.directCommands.sendText({ threadId, text: 'Hey! What can I do for you?' });
|
|
1301
|
+
|
|
1302
|
+
// React to the greeting
|
|
1303
|
+
if (itemId) {
|
|
1304
|
+
await realtime.directCommands.sendReaction({
|
|
1305
|
+
threadId,
|
|
1306
|
+
itemId,
|
|
1307
|
+
reactionType: 'emoji',
|
|
1308
|
+
emoji: '👋',
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
} else if (text === 'ping') {
|
|
1312
|
+
await realtime.directCommands.sendText({ threadId, text: 'pong!' });
|
|
1313
|
+
} else if (text === 'status') {
|
|
1076
1314
|
await realtime.directCommands.sendText({
|
|
1077
|
-
threadId
|
|
1078
|
-
text: '
|
|
1315
|
+
threadId,
|
|
1316
|
+
text: `I'm online. MQTT connected: ${realtime._mqttConnected ? 'yes' : 'no'}`,
|
|
1079
1317
|
});
|
|
1080
|
-
|
|
1081
|
-
//
|
|
1082
|
-
await realtime.directCommands.
|
|
1083
|
-
threadId
|
|
1084
|
-
|
|
1085
|
-
emoji: '👋'
|
|
1318
|
+
} else {
|
|
1319
|
+
// Default reply
|
|
1320
|
+
await realtime.directCommands.sendText({
|
|
1321
|
+
threadId,
|
|
1322
|
+
text: "I got your message! I don't know how to respond to that yet.",
|
|
1086
1323
|
});
|
|
1087
1324
|
}
|
|
1088
1325
|
|
|
1089
|
-
// Stop typing
|
|
1090
|
-
await realtime.directCommands.indicateActivity({
|
|
1091
|
-
threadId: msg.thread_id,
|
|
1092
|
-
isActive: false
|
|
1093
|
-
});
|
|
1326
|
+
// 5. Stop typing indicator
|
|
1327
|
+
await realtime.directCommands.indicateActivity({ threadId, isActive: false });
|
|
1094
1328
|
});
|
|
1095
1329
|
```
|
|
1096
1330
|
|
|
1097
1331
|
---
|
|
1098
1332
|
|
|
1333
|
+
### Checking the Disconnect Reason Programmatically
|
|
1334
|
+
|
|
1335
|
+
When the connection drops, `lastDisconnect.error.output.statusCode` gives you a numeric code. The `DisconnectReason` enum maps every code to a human-readable name so you don't have to remember the numbers:
|
|
1336
|
+
|
|
1337
|
+
```javascript
|
|
1338
|
+
const { DisconnectReason } = require('nodejs-insta-private-api-mqt');
|
|
1339
|
+
|
|
1340
|
+
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
1341
|
+
if (connection !== 'close') return;
|
|
1342
|
+
|
|
1343
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
1344
|
+
const message = lastDisconnect?.error?.message;
|
|
1345
|
+
const when = lastDisconnect?.date;
|
|
1346
|
+
|
|
1347
|
+
console.log('Disconnected at:', when?.toISOString());
|
|
1348
|
+
console.log('Reason code:', code);
|
|
1349
|
+
console.log('Message:', message);
|
|
1350
|
+
|
|
1351
|
+
if (code === DisconnectReason.loggedOut) {
|
|
1352
|
+
// 401 — Instagram says your session is invalid
|
|
1353
|
+
// Only fix: delete the session folder and log in again
|
|
1354
|
+
console.log('Need to re-login');
|
|
1355
|
+
|
|
1356
|
+
} else if (code === DisconnectReason.rateLimited) {
|
|
1357
|
+
// 429 — sent too many requests too fast
|
|
1358
|
+
// The library automatically applies a longer backoff for rate limits
|
|
1359
|
+
|
|
1360
|
+
} else if (code === DisconnectReason.connectionLost) {
|
|
1361
|
+
// 408 — connection dropped with no specific reason
|
|
1362
|
+
// This is the most common disconnect — usually just a network blip
|
|
1363
|
+
|
|
1364
|
+
} else if (code === DisconnectReason.connectionClosed) {
|
|
1365
|
+
// 428 — you called realtime.disconnect() intentionally
|
|
1366
|
+
|
|
1367
|
+
} else if (code === DisconnectReason.timedOut) {
|
|
1368
|
+
// 504 — MQTT keepalive ping timed out
|
|
1369
|
+
|
|
1370
|
+
} else if (code === DisconnectReason.networkError) {
|
|
1371
|
+
// 503 — ECONNRESET, ETIMEDOUT, DNS failure, etc.
|
|
1372
|
+
|
|
1373
|
+
} else if (code === DisconnectReason.protocolError) {
|
|
1374
|
+
// 500 — Thrift or MQTT parsing failed
|
|
1375
|
+
|
|
1376
|
+
} else if (code === DisconnectReason.serverError) {
|
|
1377
|
+
// 502 — Instagram server returned a 5xx
|
|
1378
|
+
|
|
1379
|
+
} else if (code === DisconnectReason.reconnectFailed) {
|
|
1380
|
+
// 503 — all automatic reconnect attempts were exhausted
|
|
1381
|
+
process.exit(1);
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
**All DisconnectReason values at a glance:**
|
|
1387
|
+
|
|
1388
|
+
| Name | Code | When it happens |
|
|
1389
|
+
|------|------|-----------------|
|
|
1390
|
+
| `loggedOut` | 401 | Session invalid — fresh login required |
|
|
1391
|
+
| `rateLimited` | 429 | Too many requests — library backs off automatically |
|
|
1392
|
+
| `connectionLost` | 408 | Unexpected drop, no specific error info |
|
|
1393
|
+
| `connectionClosed` | 428 | You called `disconnect()` yourself |
|
|
1394
|
+
| `timedOut` | 504 | MQTT ping/keepalive timeout |
|
|
1395
|
+
| `networkError` | 503 | ECONNRESET, ETIMEDOUT, or DNS failure |
|
|
1396
|
+
| `protocolError` | 500 | Thrift or MQTT parse error |
|
|
1397
|
+
| `serverError` | 502 | Instagram returned a 5xx response |
|
|
1398
|
+
| `reconnectFailed` | 503 | All auto-reconnect attempts exhausted |
|
|
1399
|
+
|
|
1400
|
+
---
|
|
1401
|
+
|
|
1402
|
+
### Using startRealTimeListener() (Simpler Connect Method)
|
|
1403
|
+
|
|
1404
|
+
`startRealTimeListener()` is a convenience wrapper around `connect()` that automatically builds the recommended set of GraphQL subscriptions for you. It also accepts optional health monitor and logger config:
|
|
1405
|
+
|
|
1406
|
+
```javascript
|
|
1407
|
+
const realtime = new RealtimeClient(ig);
|
|
1408
|
+
|
|
1409
|
+
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
1410
|
+
// same as above
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
realtime.on('message', async (data) => {
|
|
1414
|
+
// handle incoming DMs
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
// Simplest connect — auto-builds subscriptions
|
|
1418
|
+
await realtime.startRealTimeListener();
|
|
1419
|
+
|
|
1420
|
+
// Or with health monitoring and file logging enabled:
|
|
1421
|
+
await realtime.startRealTimeListener({
|
|
1422
|
+
credentials: { username: 'your_username', password: 'your_password' },
|
|
1423
|
+
enablePersistentLogger: true,
|
|
1424
|
+
logDir: './mqtt-logs',
|
|
1425
|
+
});
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
The health monitor periodically calls `/api/v1/accounts/current_user/` to check if the session is still valid and automatically re-logs in if it detects expiry. The persistent logger writes all MQTT events to rotating files in `logDir`, which is useful for debugging disconnects that happen when you're not watching.
|
|
1429
|
+
|
|
1430
|
+
---
|
|
1431
|
+
|
|
1432
|
+
### Session Health Events
|
|
1433
|
+
|
|
1434
|
+
When the health monitor is enabled, these additional events fire on `realtime`:
|
|
1435
|
+
|
|
1436
|
+
```javascript
|
|
1437
|
+
realtime.on('health_check', ({ status, stats }) => {
|
|
1438
|
+
// status: 'ok' or 'failed'
|
|
1439
|
+
// stats: { totalUptimeHuman, uptimePercent, reconnects, ... }
|
|
1440
|
+
console.log(`Health: ${status}, uptime: ${stats.totalUptimeHuman}`);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
realtime.on('session_expired', () => {
|
|
1444
|
+
console.log('Session detected as expired by health monitor');
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
realtime.on('relogin_success', () => {
|
|
1448
|
+
console.log('Automatically re-logged in after session expiry');
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
realtime.on('relogin_failed', ({ error }) => {
|
|
1452
|
+
console.error('Auto re-login failed:', error?.message);
|
|
1453
|
+
});
|
|
1454
|
+
```
|
|
1455
|
+
|
|
1456
|
+
You can also pull stats at any time:
|
|
1457
|
+
|
|
1458
|
+
```javascript
|
|
1459
|
+
const health = realtime.getHealthStats();
|
|
1460
|
+
console.log('Uptime:', health.uptimePercent + '%');
|
|
1461
|
+
console.log('Reconnects so far:', health.reconnects);
|
|
1462
|
+
console.log('Session started:', health.sessionStart);
|
|
1463
|
+
```
|
|
1464
|
+
|
|
1465
|
+
---
|
|
1466
|
+
|
|
1099
1467
|
## Session Management
|
|
1100
1468
|
|
|
1101
|
-
### Multi-File Auth State
|
|
1469
|
+
### Multi-File Auth State
|
|
1102
1470
|
|
|
1103
1471
|
```javascript
|
|
1104
1472
|
const authState = await useMultiFileAuthState('./auth_folder');
|
|
@@ -2565,10 +2933,144 @@ Here's every repository and what it covers at a glance.
|
|
|
2565
2933
|
|
|
2566
2934
|
---
|
|
2567
2935
|
|
|
2936
|
+
### connection.update — Unified Connection State Event
|
|
2937
|
+
|
|
2938
|
+
All three clients (`RealtimeClient`, `FbnsClient`, `MQTToTClient`) emit a `connection.update` event whenever the connection state changes. Each client also exposes an `ev` property that is an alias for itself, so both `client.on(...)` and `client.ev.on(...)` work identically.
|
|
2939
|
+
|
|
2940
|
+
**Event shape:**
|
|
2941
|
+
|
|
2942
|
+
```typescript
|
|
2943
|
+
{
|
|
2944
|
+
connection: 'connecting' | 'open' | 'close';
|
|
2945
|
+
lastDisconnect?: {
|
|
2946
|
+
error: BoomError; // @hapi/boom wrapped error
|
|
2947
|
+
date: Date;
|
|
2948
|
+
};
|
|
2949
|
+
isNewLogin?: boolean; // true on the first successful connect (RealtimeClient only)
|
|
2950
|
+
}
|
|
2951
|
+
```
|
|
2952
|
+
|
|
2953
|
+
**Usage with RealtimeClient:**
|
|
2954
|
+
|
|
2955
|
+
```javascript
|
|
2956
|
+
const { RealtimeClient } = require('nodejs-insta-private-api-mqt');
|
|
2957
|
+
|
|
2958
|
+
const realtime = new RealtimeClient(ig);
|
|
2959
|
+
|
|
2960
|
+
// Both forms work identically:
|
|
2961
|
+
realtime.on('connection.update', (update) => {
|
|
2962
|
+
const { connection, lastDisconnect } = update;
|
|
2963
|
+
if (connection === 'connecting') {
|
|
2964
|
+
// attempting to connect
|
|
2965
|
+
} else if (connection === 'open') {
|
|
2966
|
+
console.log('Connected');
|
|
2967
|
+
} else if (connection === 'close') {
|
|
2968
|
+
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
2969
|
+
const message = lastDisconnect?.error?.message;
|
|
2970
|
+
console.log('Disconnected:', statusCode, message);
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
|
|
2974
|
+
// Using .ev alias:
|
|
2975
|
+
realtime.ev.on('connection.update', ({ connection }) => {
|
|
2976
|
+
console.log('State:', connection);
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
await realtime.connect({ /* options */ });
|
|
2980
|
+
```
|
|
2981
|
+
|
|
2982
|
+
**Usage with FbnsClient:**
|
|
2983
|
+
|
|
2984
|
+
```javascript
|
|
2985
|
+
const fbns = ig.fbns; // or new FbnsClient(ig)
|
|
2986
|
+
|
|
2987
|
+
fbns.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
2988
|
+
if (connection === 'open') {
|
|
2989
|
+
console.log('FBNS connected');
|
|
2990
|
+
} else if (connection === 'close') {
|
|
2991
|
+
console.log('FBNS disconnected:', lastDisconnect?.error?.message);
|
|
2992
|
+
}
|
|
2993
|
+
});
|
|
2994
|
+
```
|
|
2995
|
+
|
|
2996
|
+
**Inspecting the Boom error:**
|
|
2997
|
+
|
|
2998
|
+
```javascript
|
|
2999
|
+
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
3000
|
+
if (connection === 'close' && lastDisconnect) {
|
|
3001
|
+
const err = lastDisconnect.error;
|
|
3002
|
+
console.log(err.message); // human-readable message
|
|
3003
|
+
console.log(err.output.statusCode); // HTTP status code (503, 408, etc.)
|
|
3004
|
+
console.log(err.isBoom); // always true
|
|
3005
|
+
console.log(lastDisconnect.date.toISOString()); // when the disconnect happened
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
```
|
|
3009
|
+
|
|
3010
|
+
**DisconnectReason — structured disconnect codes:**
|
|
3011
|
+
|
|
3012
|
+
The library exports a `DisconnectReason` enum so you can branch on the precise cause without string-matching error messages:
|
|
3013
|
+
|
|
3014
|
+
```javascript
|
|
3015
|
+
const { DisconnectReason } = require('nodejs-insta-private-api-mqt');
|
|
3016
|
+
|
|
3017
|
+
realtime.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
3018
|
+
if (connection !== 'close') return;
|
|
3019
|
+
|
|
3020
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
3021
|
+
|
|
3022
|
+
switch (code) {
|
|
3023
|
+
case DisconnectReason.loggedOut:
|
|
3024
|
+
// Credentials expired — re-login required
|
|
3025
|
+
break;
|
|
3026
|
+
case DisconnectReason.rateLimited:
|
|
3027
|
+
// Too many requests — back off before reconnecting
|
|
3028
|
+
break;
|
|
3029
|
+
case DisconnectReason.connectionLost:
|
|
3030
|
+
case DisconnectReason.networkError:
|
|
3031
|
+
// Network drop — library will auto-reconnect
|
|
3032
|
+
break;
|
|
3033
|
+
case DisconnectReason.connectionClosed:
|
|
3034
|
+
// You called disconnect() intentionally
|
|
3035
|
+
break;
|
|
3036
|
+
case DisconnectReason.reconnectFailed:
|
|
3037
|
+
// All reconnect attempts exhausted
|
|
3038
|
+
break;
|
|
3039
|
+
case DisconnectReason.timedOut:
|
|
3040
|
+
// MQTT keepalive / ping timeout
|
|
3041
|
+
break;
|
|
3042
|
+
case DisconnectReason.protocolError:
|
|
3043
|
+
// Thrift/MQTT parse error
|
|
3044
|
+
break;
|
|
3045
|
+
case DisconnectReason.serverError:
|
|
3046
|
+
// Instagram backend returned 5xx
|
|
3047
|
+
break;
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
```
|
|
3051
|
+
|
|
3052
|
+
**Full DisconnectReason reference:**
|
|
3053
|
+
|
|
3054
|
+
| Key | HTTP status code | When it fires |
|
|
3055
|
+
|-----|-----------------|---------------|
|
|
3056
|
+
| `loggedOut` | 401 | Auth / session invalid — re-login required |
|
|
3057
|
+
| `rateLimited` | 429 | Instagram rate-limited the connection |
|
|
3058
|
+
| `connectionLost` | 408 | Unexpected connection drop (no error info) |
|
|
3059
|
+
| `connectionClosed` | 428 | You called `disconnect()` intentionally |
|
|
3060
|
+
| `timedOut` | 504 | MQTT keepalive / ping failure |
|
|
3061
|
+
| `networkError` | 503 | Network-level failure (ECONNRESET, ETIMEDOUT…) |
|
|
3062
|
+
| `protocolError` | 500 | Thrift / MQTT protocol parse error |
|
|
3063
|
+
| `serverError` | 502 | Instagram backend returned a 5xx response |
|
|
3064
|
+
| `reconnectFailed` | 503 | All automatic reconnect attempts exhausted |
|
|
3065
|
+
| `unknown` | 500 | Unclassified error |
|
|
3066
|
+
|
|
3067
|
+
---
|
|
3068
|
+
|
|
2568
3069
|
### RealtimeClient Events
|
|
2569
3070
|
|
|
2570
3071
|
| Event | Description |
|
|
2571
3072
|
|-------|-------------|
|
|
3073
|
+
| `connection.update` | Connection state changed (`connecting` / `open` / `close`) |
|
|
2572
3074
|
| `connected` | MQTT connected |
|
|
2573
3075
|
| `disconnected` | MQTT disconnected |
|
|
2574
3076
|
| `message` | New message received |
|
|
@@ -2656,60 +3158,130 @@ realtime.on('liveRealtimeComments', (data) => {
|
|
|
2656
3158
|
|
|
2657
3159
|
#### Complete Example: Multi-Event Listener
|
|
2658
3160
|
|
|
2659
|
-
|
|
3161
|
+
A full bot that uses every major event — connection lifecycle, messages, typing, presence, and raw MQTT data for debugging. This is a good starting template to copy and trim down to what you actually need.
|
|
2660
3162
|
|
|
2661
3163
|
```javascript
|
|
2662
|
-
const {
|
|
3164
|
+
const {
|
|
3165
|
+
IgApiClient,
|
|
3166
|
+
RealtimeClient,
|
|
3167
|
+
useMultiFileAuthState,
|
|
3168
|
+
GraphQLSubscriptions,
|
|
3169
|
+
DisconnectReason,
|
|
3170
|
+
} = require('nodejs-insta-private-api-mqt');
|
|
2663
3171
|
|
|
2664
3172
|
async function startAdvancedBot() {
|
|
2665
3173
|
const ig = new IgApiClient();
|
|
2666
3174
|
const auth = await useMultiFileAuthState('./auth_info_ig');
|
|
2667
|
-
|
|
3175
|
+
|
|
2668
3176
|
ig.state.usePresetDevice('Samsung Galaxy S25 Ultra');
|
|
2669
|
-
|
|
3177
|
+
|
|
3178
|
+
if (auth.hasSession()) {
|
|
3179
|
+
await auth.loadCreds(ig);
|
|
3180
|
+
const valid = await auth.isSessionValid(ig).catch(() => false);
|
|
3181
|
+
if (!valid) {
|
|
3182
|
+
const result = await ig.login({ username: 'your_username', password: 'your_password' });
|
|
3183
|
+
await auth.saveCreds(ig);
|
|
3184
|
+
}
|
|
3185
|
+
} else {
|
|
3186
|
+
const result = await ig.login({ username: 'your_username', password: 'your_password' });
|
|
3187
|
+
await auth.saveCreds(ig);
|
|
3188
|
+
}
|
|
3189
|
+
|
|
2670
3190
|
const realtime = new RealtimeClient(ig);
|
|
3191
|
+
const userId = ig.state.cookieUserId;
|
|
3192
|
+
|
|
3193
|
+
// Connection lifecycle — fires on every state change
|
|
3194
|
+
realtime.ev.on('connection.update', ({ connection, lastDisconnect, isNewLogin }) => {
|
|
3195
|
+
if (connection === 'connecting') {
|
|
3196
|
+
console.log('[CONN] Connecting...');
|
|
3197
|
+
} else if (connection === 'open') {
|
|
3198
|
+
console.log(`[CONN] Connected${isNewLogin ? ' (new session)' : ''}`);
|
|
3199
|
+
} else if (connection === 'close') {
|
|
3200
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
3201
|
+
const msg = lastDisconnect?.error?.message;
|
|
3202
|
+
|
|
3203
|
+
if (code === DisconnectReason.loggedOut) {
|
|
3204
|
+
console.error('[CONN] Session invalid — need to re-login');
|
|
3205
|
+
process.exit(1);
|
|
3206
|
+
} else if (code === DisconnectReason.rateLimited) {
|
|
3207
|
+
console.warn('[CONN] Rate limited, backing off...');
|
|
3208
|
+
} else if (code === DisconnectReason.reconnectFailed) {
|
|
3209
|
+
console.error('[CONN] All reconnect attempts failed:', msg);
|
|
3210
|
+
process.exit(1);
|
|
3211
|
+
} else {
|
|
3212
|
+
console.warn(`[CONN] Disconnected (${code}): ${msg}`);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
});
|
|
2671
3216
|
|
|
2672
|
-
|
|
3217
|
+
realtime.on('reconnected', ({ attempt }) => {
|
|
3218
|
+
console.log(`[CONN] Reconnected on attempt #${attempt}`);
|
|
3219
|
+
});
|
|
3220
|
+
|
|
3221
|
+
// Standard parsed message
|
|
2673
3222
|
realtime.on('message_live', (msg) => {
|
|
2674
|
-
|
|
3223
|
+
if (msg?.text) {
|
|
3224
|
+
console.log(`[MSG] ${msg.username || msg.userId}: "${msg.text}"`);
|
|
3225
|
+
}
|
|
2675
3226
|
});
|
|
2676
3227
|
|
|
2677
|
-
//
|
|
2678
|
-
realtime.on('
|
|
2679
|
-
|
|
3228
|
+
// Full message with raw data (catches all item types)
|
|
3229
|
+
realtime.on('message', async (data) => {
|
|
3230
|
+
const msg = data.message || data.parsed;
|
|
3231
|
+
if (!msg?.thread_id) return;
|
|
3232
|
+
|
|
3233
|
+
const threadId = msg.thread_id;
|
|
3234
|
+
const text = msg.text?.toLowerCase().trim();
|
|
3235
|
+
|
|
3236
|
+
if (text === 'ping') {
|
|
3237
|
+
await realtime.directCommands.sendText({ threadId, text: 'pong!' });
|
|
3238
|
+
}
|
|
2680
3239
|
});
|
|
2681
3240
|
|
|
2682
|
-
// Direct message
|
|
3241
|
+
// Direct message patch operations (add/replace/remove on thread items)
|
|
2683
3242
|
realtime.on('direct', (data) => {
|
|
2684
3243
|
if (data.op === 'add') {
|
|
2685
|
-
console.log('[
|
|
3244
|
+
console.log('[DIRECT] New item in thread');
|
|
3245
|
+
} else if (data.op === 'remove') {
|
|
3246
|
+
console.log('[DIRECT] Item removed from thread');
|
|
2686
3247
|
}
|
|
2687
3248
|
});
|
|
2688
3249
|
|
|
2689
3250
|
// Typing indicators
|
|
2690
3251
|
realtime.on('directTyping', (data) => {
|
|
2691
|
-
console.log('[
|
|
3252
|
+
console.log('[TYPING] Someone is typing...');
|
|
2692
3253
|
});
|
|
2693
3254
|
|
|
2694
3255
|
// User presence (online/offline)
|
|
2695
3256
|
realtime.on('appPresence', (data) => {
|
|
2696
|
-
console.log('[
|
|
3257
|
+
console.log('[PRESENCE] User status changed:', data?.user_id);
|
|
2697
3258
|
});
|
|
2698
3259
|
|
|
2699
|
-
//
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
});
|
|
2705
|
-
await auth.saveCreds(ig);
|
|
2706
|
-
} else {
|
|
2707
|
-
await auth.loadCreds(ig);
|
|
2708
|
-
}
|
|
3260
|
+
// Raw MQTT — useful for debugging unknown events
|
|
3261
|
+
realtime.on('realtimeSub', ({ data, topic }) => {
|
|
3262
|
+
// Only uncomment this when debugging — it's very noisy
|
|
3263
|
+
// console.log('[RAW MQTT]', topic, JSON.stringify(data).substring(0, 100));
|
|
3264
|
+
});
|
|
2709
3265
|
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
3266
|
+
// Connect with full subscription set
|
|
3267
|
+
await realtime.connect({
|
|
3268
|
+
graphQlSubs: [
|
|
3269
|
+
userId && GraphQLSubscriptions.getDirectTypingSubscription(userId),
|
|
3270
|
+
GraphQLSubscriptions.getAppPresenceSubscription(),
|
|
3271
|
+
GraphQLSubscriptions.getDirectStatusSubscription(),
|
|
3272
|
+
userId && GraphQLSubscriptions.getAsyncAdSubscription(userId),
|
|
3273
|
+
GraphQLSubscriptions.getClientConfigUpdateSubscription(),
|
|
3274
|
+
].filter(Boolean),
|
|
3275
|
+
});
|
|
3276
|
+
|
|
3277
|
+
console.log('[BOT] All event listeners active. Send a DM to test.');
|
|
3278
|
+
|
|
3279
|
+
process.on('SIGINT', () => {
|
|
3280
|
+
realtime.disconnect();
|
|
3281
|
+
process.exit(0);
|
|
3282
|
+
});
|
|
3283
|
+
|
|
3284
|
+
await new Promise(() => {});
|
|
2713
3285
|
}
|
|
2714
3286
|
|
|
2715
3287
|
startAdvancedBot().catch(console.error);
|
|
@@ -2771,29 +3343,47 @@ All delays include random jitter (0-2s) to prevent multiple bots from hammering
|
|
|
2771
3343
|
|
|
2772
3344
|
### Listening for Error Events
|
|
2773
3345
|
|
|
3346
|
+
The cleanest way to handle all error scenarios is through `connection.update` with `DisconnectReason`. But there are also a few standalone error events that fire in specific situations:
|
|
3347
|
+
|
|
2774
3348
|
```javascript
|
|
2775
|
-
const { IgApiClient, RealtimeClient } = require('nodejs-insta-private-api-mqt');
|
|
3349
|
+
const { IgApiClient, RealtimeClient, DisconnectReason } = require('nodejs-insta-private-api-mqt');
|
|
2776
3350
|
|
|
2777
3351
|
const ig = new IgApiClient();
|
|
2778
3352
|
const realtime = new RealtimeClient(ig);
|
|
2779
3353
|
|
|
2780
|
-
//
|
|
3354
|
+
// The main lifecycle hook — every disconnect comes through here with a reason code
|
|
3355
|
+
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
3356
|
+
if (connection !== 'close') return;
|
|
3357
|
+
|
|
3358
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
3359
|
+
|
|
3360
|
+
if (code === DisconnectReason.loggedOut) {
|
|
3361
|
+
// Session is dead — only a fresh login can fix this
|
|
3362
|
+
console.log('Session expired, logging in again...');
|
|
3363
|
+
// re-login logic here
|
|
3364
|
+
} else if (code === DisconnectReason.rateLimited) {
|
|
3365
|
+
console.log('Rate limited — the library is backing off automatically');
|
|
3366
|
+
} else if (code === DisconnectReason.reconnectFailed) {
|
|
3367
|
+
console.log('All reconnect attempts exhausted');
|
|
3368
|
+
process.exit(1);
|
|
3369
|
+
}
|
|
3370
|
+
});
|
|
3371
|
+
|
|
3372
|
+
// Fires after 3+ consecutive auth failures — session is almost certainly dead
|
|
2781
3373
|
realtime.on('auth_failure', ({ count, error }) => {
|
|
2782
|
-
console.log(`Auth failed ${count} times: ${error}`);
|
|
2783
|
-
console.log('Session
|
|
2784
|
-
// your re-login logic here
|
|
3374
|
+
console.log(`Auth failed ${count} times in a row: ${error}`);
|
|
3375
|
+
console.log('Session expired — need to log in again');
|
|
2785
3376
|
});
|
|
2786
3377
|
|
|
2787
|
-
//
|
|
3378
|
+
// Fires for unexpected low-level errors
|
|
2788
3379
|
realtime.on('error', (err) => {
|
|
2789
|
-
|
|
2790
|
-
console.log('Gave up reconnecting. Maybe restart the process.');
|
|
2791
|
-
}
|
|
3380
|
+
console.error('MQTT error:', err?.message || err);
|
|
2792
3381
|
});
|
|
2793
3382
|
|
|
2794
|
-
//
|
|
2795
|
-
|
|
2796
|
-
|
|
3383
|
+
// Non-fatal issues — payload parse errors, unknown topics, etc.
|
|
3384
|
+
// Good to log for debugging, usually not actionable
|
|
3385
|
+
realtime.on('warning', (w) => {
|
|
3386
|
+
console.warn('Warning:', w?.message || w);
|
|
2797
3387
|
});
|
|
2798
3388
|
```
|
|
2799
3389
|
|
|
@@ -2825,13 +3415,27 @@ The `ReconnectManager` works together with the error handler. When the connectio
|
|
|
2825
3415
|
All delays include up to 30% jitter so if you're running multiple bots they don't all reconnect at the exact same second.
|
|
2826
3416
|
|
|
2827
3417
|
```javascript
|
|
2828
|
-
realtime.on('
|
|
2829
|
-
|
|
3418
|
+
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
3419
|
+
if (connection === 'open') {
|
|
3420
|
+
console.log('Back online');
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
if (connection === 'close') {
|
|
3424
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
3425
|
+
// DisconnectReason.reconnectFailed (503) means all attempts are used up
|
|
3426
|
+
if (code === DisconnectReason.reconnectFailed) {
|
|
3427
|
+
console.log('Could not reconnect after all attempts');
|
|
3428
|
+
// restart the process, send yourself a notification, etc.
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
2830
3431
|
});
|
|
2831
3432
|
|
|
2832
|
-
realtime.on('
|
|
2833
|
-
console.log(
|
|
2834
|
-
|
|
3433
|
+
realtime.on('reconnected', ({ attempt }) => {
|
|
3434
|
+
console.log(`Back online after ${attempt} attempt(s)`);
|
|
3435
|
+
});
|
|
3436
|
+
|
|
3437
|
+
realtime.on('reconnect_failed', ({ attempts, lastErrorType }) => {
|
|
3438
|
+
console.log(`Gave up after ${attempts} attempts. Last error type: ${lastErrorType}`);
|
|
2835
3439
|
});
|
|
2836
3440
|
```
|
|
2837
3441
|
|
|
@@ -3060,6 +3664,19 @@ async function startFbns() {
|
|
|
3060
3664
|
// create the FBNS client and wire up events
|
|
3061
3665
|
const fbns = new FbnsClient(ig);
|
|
3062
3666
|
|
|
3667
|
+
// Connection lifecycle — same pattern as RealtimeClient
|
|
3668
|
+
fbns.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
3669
|
+
if (connection === 'connecting') {
|
|
3670
|
+
console.log('FBNS connecting...');
|
|
3671
|
+
} else if (connection === 'open') {
|
|
3672
|
+
console.log('FBNS connected, push notifications active');
|
|
3673
|
+
} else if (connection === 'close') {
|
|
3674
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
3675
|
+
const msg = lastDisconnect?.error?.message;
|
|
3676
|
+
console.log(`FBNS disconnected (${code}): ${msg}`);
|
|
3677
|
+
}
|
|
3678
|
+
});
|
|
3679
|
+
|
|
3063
3680
|
fbns.on('push', (notification) => {
|
|
3064
3681
|
console.log('Got push notification!');
|
|
3065
3682
|
console.log('Title:', notification.title);
|
|
@@ -3067,10 +3684,6 @@ async function startFbns() {
|
|
|
3067
3684
|
console.log('Type:', notification.collapseKey);
|
|
3068
3685
|
});
|
|
3069
3686
|
|
|
3070
|
-
fbns.on('error', (err) => {
|
|
3071
|
-
console.error('FBNS error:', err.message);
|
|
3072
|
-
});
|
|
3073
|
-
|
|
3074
3687
|
// connect - this handles everything: TLS handshake, device auth,
|
|
3075
3688
|
// token registration, and push subscription
|
|
3076
3689
|
await fbns.connect();
|
|
@@ -3150,7 +3763,14 @@ fbns.on('push', (notification) => {
|
|
|
3150
3763
|
You can run both FBNS (push notifications) and RealtimeClient (DM messaging) side by side. They use different MQTT connections - Realtime connects to `edge-mqtt.facebook.com` for DMs, while FBNS connects to `mqtt-mini.facebook.com` for push notifications.
|
|
3151
3764
|
|
|
3152
3765
|
```javascript
|
|
3153
|
-
const {
|
|
3766
|
+
const {
|
|
3767
|
+
IgApiClient,
|
|
3768
|
+
RealtimeClient,
|
|
3769
|
+
FbnsClient,
|
|
3770
|
+
useMultiFileAuthState,
|
|
3771
|
+
GraphQLSubscriptions,
|
|
3772
|
+
DisconnectReason,
|
|
3773
|
+
} = require('nodejs-insta-private-api-mqt');
|
|
3154
3774
|
|
|
3155
3775
|
async function startBot() {
|
|
3156
3776
|
const ig = new IgApiClient();
|
|
@@ -3160,26 +3780,75 @@ async function startBot() {
|
|
|
3160
3780
|
|
|
3161
3781
|
if (auth.hasSession()) {
|
|
3162
3782
|
await auth.loadCreds(ig);
|
|
3783
|
+
const valid = await auth.isSessionValid(ig).catch(() => false);
|
|
3784
|
+
if (!valid) {
|
|
3785
|
+
const result = await ig.login({ username: 'your_username', password: 'your_password' });
|
|
3786
|
+
await auth.saveCreds(ig);
|
|
3787
|
+
}
|
|
3163
3788
|
} else {
|
|
3164
|
-
await ig.login({ username: 'your_username', password: 'your_password' });
|
|
3789
|
+
const result = await ig.login({ username: 'your_username', password: 'your_password' });
|
|
3165
3790
|
await auth.saveCreds(ig);
|
|
3166
3791
|
}
|
|
3167
3792
|
|
|
3168
|
-
|
|
3793
|
+
const userId = ig.state.cookieUserId;
|
|
3794
|
+
|
|
3795
|
+
// Realtime MQTT for DMs
|
|
3169
3796
|
const realtime = new RealtimeClient(ig);
|
|
3797
|
+
|
|
3798
|
+
realtime.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
3799
|
+
if (connection === 'open') {
|
|
3800
|
+
console.log('[REALTIME] Connected');
|
|
3801
|
+
} else if (connection === 'close') {
|
|
3802
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
3803
|
+
if (code === DisconnectReason.loggedOut) {
|
|
3804
|
+
console.error('[REALTIME] Session expired — re-login required');
|
|
3805
|
+
process.exit(1);
|
|
3806
|
+
} else {
|
|
3807
|
+
console.warn(`[REALTIME] Disconnected (${code}), reconnecting...`);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
});
|
|
3811
|
+
|
|
3170
3812
|
realtime.on('message_live', (msg) => {
|
|
3171
|
-
|
|
3813
|
+
if (msg?.text) {
|
|
3814
|
+
console.log(`[DM] ${msg.username}: ${msg.text}`);
|
|
3815
|
+
}
|
|
3172
3816
|
});
|
|
3173
|
-
await realtime.startRealTimeListener();
|
|
3174
3817
|
|
|
3175
|
-
|
|
3818
|
+
await realtime.connect({
|
|
3819
|
+
graphQlSubs: [
|
|
3820
|
+
userId && GraphQLSubscriptions.getDirectTypingSubscription(userId),
|
|
3821
|
+
GraphQLSubscriptions.getAppPresenceSubscription(),
|
|
3822
|
+
GraphQLSubscriptions.getDirectStatusSubscription(),
|
|
3823
|
+
].filter(Boolean),
|
|
3824
|
+
});
|
|
3825
|
+
|
|
3826
|
+
// FBNS for push notifications
|
|
3176
3827
|
const fbns = new FbnsClient(ig);
|
|
3828
|
+
|
|
3829
|
+
fbns.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
3830
|
+
if (connection === 'open') {
|
|
3831
|
+
console.log('[FBNS] Connected, push notifications active');
|
|
3832
|
+
} else if (connection === 'close') {
|
|
3833
|
+
const code = lastDisconnect?.error?.output?.statusCode;
|
|
3834
|
+
console.warn(`[FBNS] Disconnected (${code}): ${lastDisconnect?.error?.message}`);
|
|
3835
|
+
}
|
|
3836
|
+
});
|
|
3837
|
+
|
|
3177
3838
|
fbns.on('push', (notif) => {
|
|
3178
|
-
console.log(`
|
|
3839
|
+
console.log(`[PUSH] [${notif.collapseKey}]`, notif.message || notif.title);
|
|
3179
3840
|
});
|
|
3841
|
+
|
|
3180
3842
|
await fbns.connect();
|
|
3181
3843
|
|
|
3182
3844
|
console.log('Both Realtime and FBNS are running!');
|
|
3845
|
+
|
|
3846
|
+
process.on('SIGINT', async () => {
|
|
3847
|
+
realtime.disconnect();
|
|
3848
|
+
await fbns.disconnect();
|
|
3849
|
+
process.exit(0);
|
|
3850
|
+
});
|
|
3851
|
+
|
|
3183
3852
|
await new Promise(() => {});
|
|
3184
3853
|
}
|
|
3185
3854
|
|
|
@@ -3491,7 +4160,7 @@ console.log(`Uptime: ${stats.uptimePercent}%, ${stats.reconnects} reconnects`);
|
|
|
3491
4160
|
- switchPlatform() for easy platform switching
|
|
3492
4161
|
|
|
3493
4162
|
### v5.60.8
|
|
3494
|
-
- downloadContentFromMessage() -
|
|
4163
|
+
- downloadContentFromMessage() - Persistent media download from DM messages
|
|
3495
4164
|
- View-once media extraction support
|
|
3496
4165
|
- downloadMediaBuffer() and extractMediaUrls()
|
|
3497
4166
|
|
|
@@ -3503,7 +4172,7 @@ console.log(`Uptime: ${stats.uptimePercent}%, ${stats.reconnects} reconnects`);
|
|
|
3503
4172
|
- sendPhoto() and sendVideo() for media uploads
|
|
3504
4173
|
|
|
3505
4174
|
### v5.60.2
|
|
3506
|
-
- useMultiFileAuthState() -
|
|
4175
|
+
- useMultiFileAuthState() - Multi-file session persistence
|
|
3507
4176
|
- connectFromSavedSession() method
|
|
3508
4177
|
|
|
3509
4178
|
### v5.60.0
|