@eryxenx/insta-chat 2.0.10 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/DOCS.md +53 -11
- package/README.md +83 -3
- package/index.d.ts +103 -0
- package/index.js +6 -0
- package/package.json +2 -2
- package/src/api/realtime/listen.js +3 -1
- package/src/api/users/getProfileBio.js +33 -0
- package/src/api/users/getProfilePictureV2.js +54 -0
- package/src/api/users/getSelfInfo.js +27 -0
- package/src/api/users/getUserFriendship.js +42 -0
- package/src/api/users/getUserID.js +31 -0
- package/src/api/users/getUserInfoV2.js +62 -0
- package/src/api/users/getUserProfile.js +26 -0
- package/src/api.js +116 -2
- package/src/app/InstaClient.js +23 -0
- package/src/app/createFcaClient.js +13 -1
- package/src/constants.js +11 -1
- package/src/methods/inbox.js +82 -1
- package/src/methods/user.js +191 -4
- package/src/profileParse.js +250 -0
- package/src/realtime/bridge.js +76 -0
- package/src/realtime/client.js +376 -0
- package/src/realtime/listener.js +86 -0
- package/src/realtime/mqttot.js +609 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* insta-chat - Unofficial Instagram Chat API
|
|
3
|
+
* Copyright (c) 2026 EryXenX (https://github.com/EryXenX)
|
|
4
|
+
* Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
|
|
5
|
+
* Original author credit must be retained in all copies and derivatives.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Instagram's realtime broker (edge-mqtt.facebook.com) speaks MQTToT with
|
|
9
|
+
// cookie_auth: the CONNECT password is "sessionid=<sessionid>" and the session
|
|
10
|
+
// cookie authenticates the connection, so no app login is needed. This client
|
|
11
|
+
// keeps that connection open and turns the broker's Direct message-sync and
|
|
12
|
+
// realtime-sub publishes into plain events.
|
|
13
|
+
//
|
|
14
|
+
// Message-sync (topic 146) is the Direct stream: the broker pushes Relay-style
|
|
15
|
+
// patches ({ path: "/direct_v2/threads/<threadID>", op, value }) as messages
|
|
16
|
+
// arrive, which is what makes listen() push instead of poll. Topics:
|
|
17
|
+
// 88 pubsub · 133 send-response · 135 iris-sub-response
|
|
18
|
+
// 146 message-sync · 149 realtime-sub · 150 region-hint
|
|
19
|
+
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
const { EventEmitter } = require("events");
|
|
23
|
+
const crypto = require("crypto");
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
MQTToTConnection,
|
|
27
|
+
SocketMQTToTTransport,
|
|
28
|
+
compressPayload,
|
|
29
|
+
writeConnectPacket,
|
|
30
|
+
writeDisconnectPacket,
|
|
31
|
+
writePingreqPacket,
|
|
32
|
+
writePublishPacket,
|
|
33
|
+
writeSubscribePacket,
|
|
34
|
+
decodePacket,
|
|
35
|
+
tryDecompressPayload,
|
|
36
|
+
parseJsonPayload
|
|
37
|
+
} = require("./mqttot");
|
|
38
|
+
|
|
39
|
+
const REALTIME_HOST = "edge-mqtt.facebook.com";
|
|
40
|
+
const IG_REALTIME_APP_ID = 567067343352427;
|
|
41
|
+
const REALTIME_SUBSCRIBE_TOPICS = [88, 135, 149, 150, 133, 146];
|
|
42
|
+
|
|
43
|
+
// An Android app identity. The broker expects the app's own UA + app version in
|
|
44
|
+
// the CONNECT's appSpecificInfo; a web UA is rejected.
|
|
45
|
+
const ANDROID_UA =
|
|
46
|
+
"Instagram 449.0.0.52.84 Android (34/14; 480dpi; 1344x2992; Google/google; Pixel 8 Pro; husky; husky; en_US; 1079242191)";
|
|
47
|
+
const APP_VERSION = "449.0.0.52.84";
|
|
48
|
+
const CAPABILITIES = "3brTv10=";
|
|
49
|
+
const LOCALE = "en_US";
|
|
50
|
+
|
|
51
|
+
const TOPICS = {
|
|
52
|
+
PUBSUB: "88",
|
|
53
|
+
SEND_MESSAGE: "132",
|
|
54
|
+
SEND_MESSAGE_RESPONSE: "133",
|
|
55
|
+
IRIS_SUB: "134",
|
|
56
|
+
IRIS_SUB_RESPONSE: "135",
|
|
57
|
+
MESSAGE_SYNC: "146",
|
|
58
|
+
REALTIME_SUB: "149",
|
|
59
|
+
REGION_HINT: "150"
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
class RealtimeClient extends EventEmitter {
|
|
63
|
+
constructor(context, options = {}) {
|
|
64
|
+
super();
|
|
65
|
+
this.context = context || {};
|
|
66
|
+
this.options = options;
|
|
67
|
+
this.host = options.host || REALTIME_HOST;
|
|
68
|
+
this.transport = options.transport || new SocketMQTToTTransport(this.host, 443, { timeout: options.socketTimeout || 30000 });
|
|
69
|
+
this.connected = false;
|
|
70
|
+
this._packetId = 0;
|
|
71
|
+
this._loop = null;
|
|
72
|
+
this._pingTimer = null;
|
|
73
|
+
this._stopRequested = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_deviceId() {
|
|
77
|
+
const id = this.context.deviceId || this.context.phoneId || this.context.uuid || "";
|
|
78
|
+
if (!id) throw new Error("realtime: deviceId is required");
|
|
79
|
+
return String(id);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_sessionId() {
|
|
83
|
+
const raw = this.context.sessionid;
|
|
84
|
+
if (!raw) throw new Error("realtime: sessionid is required");
|
|
85
|
+
// The browser stores the sessionid URL-encoded (32715034217%3A...); the
|
|
86
|
+
// broker wants the decoded value.
|
|
87
|
+
try {
|
|
88
|
+
return decodeURIComponent(String(raw));
|
|
89
|
+
} catch (_) {
|
|
90
|
+
return String(raw);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
buildConnection() {
|
|
95
|
+
const deviceId = this._deviceId();
|
|
96
|
+
const userAgent = this.context.userAgent || ANDROID_UA;
|
|
97
|
+
const appVersion = this.context.appVersion || APP_VERSION;
|
|
98
|
+
return new MQTToTConnection({
|
|
99
|
+
clientIdentifier: deviceId.slice(0, 20),
|
|
100
|
+
clientInfo: {
|
|
101
|
+
userId: this.context.userId || 0,
|
|
102
|
+
userAgent,
|
|
103
|
+
clientCapabilities: 183,
|
|
104
|
+
endpointCapabilities: 0,
|
|
105
|
+
publishFormat: 1,
|
|
106
|
+
noAutomaticForeground: false,
|
|
107
|
+
makeUserAvailableInForeground: true,
|
|
108
|
+
deviceId,
|
|
109
|
+
isInitiallyForeground: true,
|
|
110
|
+
networkType: 1,
|
|
111
|
+
networkSubtype: 0,
|
|
112
|
+
clientMqttSessionId: Date.now() & 0xffffffff,
|
|
113
|
+
subscribeTopics: REALTIME_SUBSCRIBE_TOPICS,
|
|
114
|
+
clientType: "cookie_auth",
|
|
115
|
+
appId: IG_REALTIME_APP_ID,
|
|
116
|
+
deviceSecret: "",
|
|
117
|
+
clientStack: 3
|
|
118
|
+
},
|
|
119
|
+
password: `sessionid=${this._sessionId()}`,
|
|
120
|
+
appSpecificInfo: {
|
|
121
|
+
app_version: appVersion,
|
|
122
|
+
"X-IG-Capabilities": CAPABILITIES,
|
|
123
|
+
everclear_subscriptions: JSON.stringify({
|
|
124
|
+
inapp_notification_subscribe_comment: "17899377895239777",
|
|
125
|
+
inapp_notification_subscribe_comment_mention_and_reply: "17899377895239777",
|
|
126
|
+
video_call_participant_state_delivery: "17977239895057311",
|
|
127
|
+
presence_subscribe: "17846944882223835"
|
|
128
|
+
}),
|
|
129
|
+
"User-Agent": userAgent,
|
|
130
|
+
"Accept-Language": LOCALE.replace("_", "-"),
|
|
131
|
+
platform: "android",
|
|
132
|
+
ig_mqtt_route: "django",
|
|
133
|
+
pubsub_msg_type_blacklist: "direct, typing_type",
|
|
134
|
+
auth_cache_enabled: "0"
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async connect() {
|
|
140
|
+
if (this.connected) return this;
|
|
141
|
+
await this.transport.connect();
|
|
142
|
+
await this.transport.send(writeConnectPacket(this.buildConnection()));
|
|
143
|
+
const packet = decodePacket(await this.transport.recvPacket());
|
|
144
|
+
if (packet.packetType !== "connack" || packet.returnCode !== 0) {
|
|
145
|
+
throw new Error(`Realtime MQTT connect failed (return code ${packet.returnCode})`);
|
|
146
|
+
}
|
|
147
|
+
this.connected = true;
|
|
148
|
+
this._stopRequested = false;
|
|
149
|
+
this.emit("connected", { host: this.host });
|
|
150
|
+
this._startPing();
|
|
151
|
+
this._startLoop();
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_startPing() {
|
|
156
|
+
const keepAliveMs = this.options.keepAliveMs || 30000;
|
|
157
|
+
this._pingTimer = setInterval(() => {
|
|
158
|
+
if (this.connected) this.transport.send(writePingreqPacket()).catch(() => {});
|
|
159
|
+
}, keepAliveMs);
|
|
160
|
+
if (this._pingTimer.unref) this._pingTimer.unref();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_startLoop() {
|
|
164
|
+
this._loop = (async () => {
|
|
165
|
+
while (this.connected) {
|
|
166
|
+
let packet;
|
|
167
|
+
try {
|
|
168
|
+
packet = decodePacket(await this.transport.recvPacket());
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (!this._stopRequested) this.emit("error", err);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
this._handlePacket(packet);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
this.emit("error", err);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
this.connected = false;
|
|
180
|
+
this.emit("disconnected", {});
|
|
181
|
+
})();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_handlePacket(packet) {
|
|
185
|
+
if (packet.packetType !== "publish" || packet.topic == null) return;
|
|
186
|
+
this.dispatch(packet.topic, packet.payload);
|
|
187
|
+
if (packet.qos === 1 && packet.packetId != null) {
|
|
188
|
+
this.transport.send(Buffer.concat([Buffer.from([0x40, 0x02]), uint16(packet.packetId)])).catch(() => {});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
dispatch(topic, payload) {
|
|
193
|
+
const body = tryDecompressPayload(payload);
|
|
194
|
+
let parsed;
|
|
195
|
+
if (
|
|
196
|
+
topic === TOPICS.SEND_MESSAGE_RESPONSE ||
|
|
197
|
+
topic === TOPICS.IRIS_SUB_RESPONSE ||
|
|
198
|
+
topic === TOPICS.REALTIME_SUB ||
|
|
199
|
+
topic === TOPICS.MESSAGE_SYNC
|
|
200
|
+
) {
|
|
201
|
+
parsed = safeJson(body);
|
|
202
|
+
} else {
|
|
203
|
+
parsed = safeJson(payload);
|
|
204
|
+
}
|
|
205
|
+
this.emit("receive", { topic, payload: parsed });
|
|
206
|
+
if (topic === TOPICS.SEND_MESSAGE_RESPONSE) this.emit("send_response", parsed);
|
|
207
|
+
if (topic === TOPICS.IRIS_SUB_RESPONSE) this.emit("iris_sub_response", parsed);
|
|
208
|
+
if (topic === TOPICS.MESSAGE_SYNC) this._dispatchMessageSync(parsed);
|
|
209
|
+
if (topic === TOPICS.REALTIME_SUB) this._dispatchRealtimeSub(parsed);
|
|
210
|
+
return parsed;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// message-sync payloads are a list of { data: [ { path, op, value } ] } patches.
|
|
214
|
+
_dispatchMessageSync(payload) {
|
|
215
|
+
if (!Array.isArray(payload)) return this.emit("iris", payload);
|
|
216
|
+
for (const item of payload) {
|
|
217
|
+
if (!item || typeof item !== "object" || !Array.isArray(item.data)) {
|
|
218
|
+
this.emit("iris", item);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const meta = {};
|
|
222
|
+
for (const [k, v] of Object.entries(item)) if (k !== "data") meta[k] = v;
|
|
223
|
+
for (const patch of item.data) {
|
|
224
|
+
if (!patch || typeof patch !== "object") continue;
|
|
225
|
+
const path = patch.path;
|
|
226
|
+
if (!path || patch.value == null) {
|
|
227
|
+
this.emit("iris", { ...meta, ...patch });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const value = parseMaybeJson(patch.value);
|
|
231
|
+
const threadID = threadIdFromPath(path);
|
|
232
|
+
const wrapped = {
|
|
233
|
+
...meta,
|
|
234
|
+
message: {
|
|
235
|
+
path,
|
|
236
|
+
op: patch.op,
|
|
237
|
+
thread_id: threadID,
|
|
238
|
+
...(value && typeof value === "object" ? value : { value })
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
this.emit(path.startsWith("/direct_v2/threads/") ? "message" : "thread_update", wrapped);
|
|
242
|
+
this._dispatchDirectEvent({ path, op: patch.op, value, threadID });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_dispatchRealtimeSub(payload) {
|
|
248
|
+
this.emit("realtime_sub", payload);
|
|
249
|
+
if (!payload || typeof payload !== "object") return;
|
|
250
|
+
let message = payload.message;
|
|
251
|
+
if (typeof message === "string") message = safeJson(Buffer.from(message));
|
|
252
|
+
if (!message || typeof message !== "object") return;
|
|
253
|
+
if (message.topic !== "direct") return;
|
|
254
|
+
let direct = message.json || message.payload;
|
|
255
|
+
if (typeof direct === "string") direct = safeJson(Buffer.from(direct));
|
|
256
|
+
this._dispatchDirectPayload(direct);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_dispatchDirectPayload(payload) {
|
|
260
|
+
if (!payload || typeof payload !== "object") return this._dispatchDirectEvent({ value: payload });
|
|
261
|
+
if (!Array.isArray(payload.data)) return this._dispatchDirectEvent(payload);
|
|
262
|
+
const meta = {};
|
|
263
|
+
for (const [k, v] of Object.entries(payload)) if (k !== "data") meta[k] = v;
|
|
264
|
+
for (const item of payload.data) {
|
|
265
|
+
this._dispatchDirectEvent(item && typeof item === "object" ? { ...meta, ...item } : { ...meta, value: item });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_dispatchDirectEvent(event) {
|
|
270
|
+
const out = { ...event };
|
|
271
|
+
let value = out.value;
|
|
272
|
+
if (typeof value === "string") value = safeJson(Buffer.from(value));
|
|
273
|
+
out.value = value;
|
|
274
|
+
if (out.path && !out.threadID) out.threadID = threadIdFromPath(out.path);
|
|
275
|
+
this.emit("direct", out);
|
|
276
|
+
const kind = classifyDirectEvent(out);
|
|
277
|
+
if (kind) this.emit(kind, out);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
publishBytes(topic, payload) {
|
|
281
|
+
this._packetId += 1;
|
|
282
|
+
const packet = writePublishPacket(topic, payload, 1, this._packetId);
|
|
283
|
+
return this.transport.send(packet);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
publishJson(topic, data) {
|
|
287
|
+
return this.publishBytes(topic, compressPayload(Buffer.from(JSON.stringify(data))));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
irisSubscribe(seqId, snapshotAtMs, appVersion) {
|
|
291
|
+
return this.publishJson(TOPICS.IRIS_SUB, {
|
|
292
|
+
seq_id: seqId,
|
|
293
|
+
snapshot_at_ms: snapshotAtMs,
|
|
294
|
+
snapshot_app_version: appVersion || this.context.appVersion || APP_VERSION
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
graphQlSubscribe(subscriptions) {
|
|
299
|
+
return this.publishJson(TOPICS.REALTIME_SUB, { sub: [].concat(subscriptions) });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
skywalkerSubscribe(subscriptions) {
|
|
303
|
+
return this.publishJson(TOPICS.PUBSUB, { sub: [].concat(subscriptions) });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async ping() {
|
|
307
|
+
if (!this.connected) throw new Error("Realtime client is not connected");
|
|
308
|
+
await this.transport.send(writePingreqPacket());
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
disconnect() {
|
|
313
|
+
this._stopRequested = true;
|
|
314
|
+
this.connected = false;
|
|
315
|
+
if (this._pingTimer) clearInterval(this._pingTimer);
|
|
316
|
+
this._pingTimer = null;
|
|
317
|
+
try { this.transport.send(writeDisconnectPacket()); } catch (_) {}
|
|
318
|
+
this.transport.disconnect();
|
|
319
|
+
return this;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Extracts the thread id from a message-sync path such as
|
|
324
|
+
// "/direct_v2/threads/1234/items/..." or "/direct_v2/inbox/threads/1234".
|
|
325
|
+
function threadIdFromPath(path) {
|
|
326
|
+
const p = String(path || "");
|
|
327
|
+
let m = p.match(/^\/direct_v2\/threads\/([^/]+)/);
|
|
328
|
+
if (m) return m[1];
|
|
329
|
+
m = p.match(/^\/direct_v2\/inbox\/threads\/([^/]+)/);
|
|
330
|
+
if (m) return m[1];
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function classifyDirectEvent(event) {
|
|
335
|
+
const path = String(event.path || "").toLowerCase();
|
|
336
|
+
const action = String(event.action || "").toLowerCase();
|
|
337
|
+
const value = event.value;
|
|
338
|
+
const valueText = value && typeof value === "object" ? JSON.stringify(value).toLowerCase() : String(value || "").toLowerCase();
|
|
339
|
+
if (valueText.includes("activity_status") || path.includes("activity_indicator") || path.includes("typing")) return "typing";
|
|
340
|
+
if (path.includes("presence") || valueText.includes("is_active") || valueText.includes("last_active")) return "presence";
|
|
341
|
+
if (action === "mark_seen" || path.includes("/seen") || valueText.includes("seen")) return "seen";
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseMaybeJson(value) {
|
|
346
|
+
if (typeof value !== "string") return value;
|
|
347
|
+
const parsed = safeJson(Buffer.from(value));
|
|
348
|
+
return parsed === undefined ? value : parsed;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function safeJson(buffer) {
|
|
352
|
+
try {
|
|
353
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
354
|
+
} catch (_) {
|
|
355
|
+
return buffer;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function uint16(value) {
|
|
360
|
+
const buf = Buffer.alloc(2);
|
|
361
|
+
buf.writeUInt16BE(value & 0xffff, 0);
|
|
362
|
+
return buf;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
module.exports = {
|
|
366
|
+
RealtimeClient,
|
|
367
|
+
REALTIME_HOST,
|
|
368
|
+
IG_REALTIME_APP_ID,
|
|
369
|
+
REALTIME_SUBSCRIBE_TOPICS,
|
|
370
|
+
TOPICS,
|
|
371
|
+
ANDROID_UA,
|
|
372
|
+
APP_VERSION,
|
|
373
|
+
threadIdFromPath,
|
|
374
|
+
classifyDirectEvent,
|
|
375
|
+
newClientContext: () => crypto.randomUUID()
|
|
376
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* insta-chat - Unofficial Instagram Chat API
|
|
3
|
+
* Copyright (c) 2026 EryXenX (https://github.com/EryXenX)
|
|
4
|
+
* Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.
|
|
5
|
+
* Original author credit must be retained in all copies and derivatives.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// The realtime listener: it opens the MQTT connection, subscribes to the Direct
|
|
9
|
+
// message-sync stream and feeds decoded messages to a callback as they arrive,
|
|
10
|
+
// instead of the inbox poller's interval. It is push, so a bot replies as soon
|
|
11
|
+
// as Instagram delivers the message.
|
|
12
|
+
//
|
|
13
|
+
// Real-time is best-effort. Instagram can drop the socket, refuse the iris
|
|
14
|
+
// subscription, or the process can run on a host that blocks the broker, so the
|
|
15
|
+
// listener reports failure instead of pretending to be connected; listen() keeps
|
|
16
|
+
// the poller as its fallback.
|
|
17
|
+
|
|
18
|
+
"use strict";
|
|
19
|
+
|
|
20
|
+
const { RealtimeClient } = require("./client");
|
|
21
|
+
const { rawEventFromMessageSync } = require("./bridge");
|
|
22
|
+
|
|
23
|
+
class RealtimeListener {
|
|
24
|
+
constructor(context, options = {}) {
|
|
25
|
+
this.context = context || {};
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.connected = false;
|
|
28
|
+
this._client = null;
|
|
29
|
+
this._callback = null;
|
|
30
|
+
this._stopped = false;
|
|
31
|
+
this._subscribed = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_emitError(err) {
|
|
35
|
+
if (typeof this.options.onError === "function") this.options.onError(err);
|
|
36
|
+
else if (this._callback) this._callback(err, null);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_handleMessageSync(event) {
|
|
40
|
+
if (this._stopped || !this._callback) return;
|
|
41
|
+
const raw = rawEventFromMessageSync(event, this.context.userId);
|
|
42
|
+
if (raw) this._callback(null, raw);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_resolveCursor() {
|
|
46
|
+
const opts = this.options;
|
|
47
|
+
if (opts.seqId != null && opts.snapshotAtMs != null) {
|
|
48
|
+
return { seqId: opts.seqId, snapshotAtMs: opts.snapshotAtMs };
|
|
49
|
+
}
|
|
50
|
+
if (typeof opts.getRealtimeState === "function") {
|
|
51
|
+
return Promise.resolve(opts.getRealtimeState()).then((state) => state || null);
|
|
52
|
+
}
|
|
53
|
+
return Promise.resolve(null);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async start(callback) {
|
|
57
|
+
this._callback = callback;
|
|
58
|
+
this._client = new RealtimeClient(this.context, this.options);
|
|
59
|
+
this._client.on("message", (event) => this._handleMessageSync(event));
|
|
60
|
+
this._client.on("error", (err) => this._emitError(err));
|
|
61
|
+
this._client.on("disconnected", () => {
|
|
62
|
+
this.connected = false;
|
|
63
|
+
if (!this._stopped) this._emitError(new Error("Realtime MQTT connection closed"));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await this._client.connect();
|
|
67
|
+
this.connected = true;
|
|
68
|
+
|
|
69
|
+
const cursor = await this._resolveCursor();
|
|
70
|
+
if (cursor && cursor.seqId != null && cursor.snapshotAtMs != null) {
|
|
71
|
+
await this._client.irisSubscribe(Number(cursor.seqId) || 0, Number(cursor.snapshotAtMs));
|
|
72
|
+
this._subscribed = true;
|
|
73
|
+
} else if (typeof this.options.onReady === "function") {
|
|
74
|
+
this.options.onReady(this);
|
|
75
|
+
}
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
stop() {
|
|
80
|
+
this._stopped = true;
|
|
81
|
+
this.connected = false;
|
|
82
|
+
if (this._client) this._client.disconnect();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { RealtimeListener };
|