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 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 like Baileys (WhatsApp library)
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 Baileys-style media download for Instagram DM messages.
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
- ### Auto-Reply Bot Example
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 { IgApiClient, RealtimeClient } = require('nodejs-insta-private-api-mqt');
1019
- const fs = require('fs');
1026
+ const {
1027
+ IgApiClient,
1028
+ RealtimeClient,
1029
+ useMultiFileAuthState,
1030
+ GraphQLSubscriptions,
1031
+ DisconnectReason,
1032
+ } = require('nodejs-insta-private-api-mqt');
1020
1033
 
1021
- (async () => {
1034
+ async function main() {
1022
1035
  const ig = new IgApiClient();
1023
- const session = JSON.parse(fs.readFileSync('session.json'));
1024
- await ig.state.deserialize(session);
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
- const inbox = await ig.direct.getInbox();
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: ['ig_sub_direct', 'ig_sub_direct_v2_message_sync'],
1031
- irisData: inbox
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
- console.log('Bot Active\n');
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
- console.log(`[${msg.from_user_id}]: ${msg.text}`);
1218
+ const threadId = msg.thread_id;
1219
+ const itemId = msg.item_id || msg.messageId;
1220
+ const text = msg.text.trim();
1041
1221
 
1042
- if (msg.text.toLowerCase().includes('hello')) {
1043
- await realtime.directCommands.sendText({
1044
- threadId: msg.thread_id,
1045
- text: 'Hey! Thanks for reaching out!'
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
- ### Smart Bot with Reactions and Typing
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
- // Mark as seen
1062
- await realtime.directCommands.markAsSeen({
1063
- threadId: msg.thread_id,
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
- // Show typing
1068
- await realtime.directCommands.indicateActivity({
1069
- threadId: msg.thread_id,
1070
- isActive: true
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
- await new Promise(r => setTimeout(r, 2000));
1295
+ // 3. Small delay to look human
1296
+ await new Promise(r => setTimeout(r, 1500 + Math.random() * 1500));
1074
1297
 
1075
- if (msg.text.toLowerCase().includes('hi')) {
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: msg.thread_id,
1078
- text: 'Hello there!'
1315
+ threadId,
1316
+ text: `I'm online. MQTT connected: ${realtime._mqttConnected ? 'yes' : 'no'}`,
1079
1317
  });
1080
-
1081
- // React with emoji
1082
- await realtime.directCommands.sendReaction({
1083
- threadId: msg.thread_id,
1084
- itemId: msg.item_id,
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 (Baileys Style)
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
- Here's a complete example showing how to listen to multiple events:
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 { IgApiClient, RealtimeClient, useMultiFileAuthState } = require('nodejs-insta-private-api-mqt');
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
- // Standard message handling
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
- console.log(`[${msg.username}]: ${msg.text}`);
3223
+ if (msg?.text) {
3224
+ console.log(`[MSG] ${msg.username || msg.userId}: "${msg.text}"`);
3225
+ }
2675
3226
  });
2676
3227
 
2677
- // Advanced: Raw MQTT data (useful for debugging)
2678
- realtime.on('realtimeSub', ({ data }) => {
2679
- console.log('[Debug] Raw MQTT:', JSON.stringify(data).substring(0, 200));
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 updates with parsed data
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('[Direct] New item added');
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('[Typing] Someone is typing...');
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('[Presence] User status changed');
3257
+ console.log('[PRESENCE] User status changed:', data?.user_id);
2697
3258
  });
2698
3259
 
2699
- // Login and connect
2700
- if (!auth.hasSession()) {
2701
- await ig.login({
2702
- username: 'your_username',
2703
- password: 'your_password'
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
- await realtime.startRealTimeListener();
2711
-
2712
- console.log('Advanced bot with full MQTT events is running!');
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
- // fires after 3 consecutive auth failures - probably time to re-login
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 is probably expired, need to login again');
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
- // fires when all 15 retry attempts are used up
3378
+ // Fires for unexpected low-level errors
2788
3379
  realtime.on('error', (err) => {
2789
- if (err.message.includes('Max retries')) {
2790
- console.log('Gave up reconnecting. Maybe restart the process.');
2791
- }
3380
+ console.error('MQTT error:', err?.message || err);
2792
3381
  });
2793
3382
 
2794
- // non-fatal stuff - good to log but usually not actionable
2795
- realtime.on('warning', ({ type, topic, error }) => {
2796
- console.log(`Warning [${type}] on ${topic}: ${error}`);
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('reconnected', () => {
2829
- console.log('Back online after a disconnect');
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('reconnect_failed', () => {
2833
- console.log('Could not reconnect after all attempts');
2834
- // maybe send yourself a notification, restart the process, etc.
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 { IgApiClient, RealtimeClient, FbnsClient, useMultiFileAuthState } = require('nodejs-insta-private-api-mqt');
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
- // start realtime for DMs
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
- console.log(`DM from ${msg.username}: ${msg.text}`);
3813
+ if (msg?.text) {
3814
+ console.log(`[DM] ${msg.username}: ${msg.text}`);
3815
+ }
3172
3816
  });
3173
- await realtime.startRealTimeListener();
3174
3817
 
3175
- // start FBNS for push notifications
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(`Push [${notif.collapseKey}]:`, notif.message || notif.title);
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() - Baileys-style media download
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() - Baileys-style session persistence
4175
+ - useMultiFileAuthState() - Multi-file session persistence
3507
4176
  - connectFromSavedSession() method
3508
4177
 
3509
4178
  ### v5.60.0