@lazyneoaz/metachat 1.0.5 → 1.0.6
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/package.json +1 -1
- package/src/apis/getThreadList.js +7 -2
- package/src/apis/listenMqtt.js +25 -0
- package/src/apis/logout.js +60 -23
- package/src/engine/models/buildAPI.js +8 -0
- package/src/engine/models/loginHelper.js +12 -7
- package/src/utils/autoReLogin.js +33 -6
- package/src/utils/clients.js +10 -5
- package/src/utils/formatters/data/formatAttachment.js +49 -35
- package/src/utils/formatters/data/formatDelta.js +28 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyneoaz/metachat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"types": "src/types/index.d.ts",
|
|
6
6
|
"description": "Advanced Facebook Chat API client for building Messenger bots — real-time messaging, thread management, MQTT, and session stability.",
|
|
@@ -53,13 +53,18 @@ function formatThreadGraphQLResponse(messageThread) {
|
|
|
53
53
|
const lastR = messageThread.last_read_receipt;
|
|
54
54
|
const lastReadTimestamp = lastR?.nodes?.[0]?.timestamp_precise || null;
|
|
55
55
|
|
|
56
|
+
// Guard: all_participants or edges can be absent on restricted/deleted threads
|
|
57
|
+
const edges = (messageThread.all_participants && Array.isArray(messageThread.all_participants.edges))
|
|
58
|
+
? messageThread.all_participants.edges
|
|
59
|
+
: [];
|
|
60
|
+
|
|
56
61
|
return {
|
|
57
62
|
threadID: threadID,
|
|
58
63
|
threadName: messageThread.name,
|
|
59
|
-
participantIDs:
|
|
64
|
+
participantIDs: edges.map(
|
|
60
65
|
(d) => d.node.messaging_actor.id,
|
|
61
66
|
),
|
|
62
|
-
userInfo:
|
|
67
|
+
userInfo: edges.map((d) => {
|
|
63
68
|
const p = d.node.messaging_actor;
|
|
64
69
|
return {
|
|
65
70
|
id: p.id,
|
package/src/apis/listenMqtt.js
CHANGED
|
@@ -1007,7 +1007,32 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
1007
1007
|
? ctx._middleware.wrapCallback(baseCallback)
|
|
1008
1008
|
: baseCallback;
|
|
1009
1009
|
|
|
1010
|
+
// Replace ctx._emitter with the new MessageEmitter, but first migrate
|
|
1011
|
+
// any existing listeners so they are not silently orphaned.
|
|
1012
|
+
// Listeners added via api.on() before listenMqtt() was called live on the
|
|
1013
|
+
// old emitter; without migration they would never fire again.
|
|
1014
|
+
const prevEmitter = ctx._emitter;
|
|
1010
1015
|
ctx._emitter = msgEmitter;
|
|
1016
|
+
if (prevEmitter && prevEmitter !== msgEmitter && typeof prevEmitter.eventNames === 'function') {
|
|
1017
|
+
try {
|
|
1018
|
+
const LIFECYCLE_EVENTS = [
|
|
1019
|
+
'sessionExpired', 'checkpoint', 'relogin', 'ready',
|
|
1020
|
+
'account_inactive', 'checkpoint_282', 'checkpoint_956', 'error'
|
|
1021
|
+
];
|
|
1022
|
+
for (const event of LIFECYCLE_EVENTS) {
|
|
1023
|
+
const listeners = prevEmitter.rawListeners ? prevEmitter.rawListeners(event) : [];
|
|
1024
|
+
for (const listener of listeners) {
|
|
1025
|
+
msgEmitter.on(event, listener);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
} catch (_) {}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Reset reconnect-blocking flags — calling listenMqtt() always means
|
|
1032
|
+
// "start fresh". Without this, ctx._ending left behind by stopListening()
|
|
1033
|
+
// or emitAuthError() would permanently block scheduleReconnect().
|
|
1034
|
+
ctx._ending = false;
|
|
1035
|
+
ctx._cycling = false;
|
|
1011
1036
|
|
|
1012
1037
|
ctx._listeningActive = true;
|
|
1013
1038
|
ctx._lastListenCallback = callback || null;
|
package/src/apis/logout.js
CHANGED
|
@@ -11,50 +11,87 @@ const utils = require('../utils');
|
|
|
11
11
|
module.exports = function (defaultFuncs, api, ctx) {
|
|
12
12
|
/**
|
|
13
13
|
* Logs the current user out of Facebook.
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* Strategy:
|
|
16
|
+
* 1. Try to fetch the settings-menu endpoint and parse the logout form from
|
|
17
|
+
* the jsmods response (legacy path, may break if Facebook restructures).
|
|
18
|
+
* 2. If that fails for any reason, fall back to posting directly to
|
|
19
|
+
* /logout.php using the session token already in ctx.fb_dtsg. This is
|
|
20
|
+
* reliable as long as the session is still valid.
|
|
21
|
+
*
|
|
22
|
+
* @returns {Promise<void>}
|
|
15
23
|
*/
|
|
16
24
|
return async function logout() {
|
|
17
|
-
|
|
18
|
-
pmid: "0",
|
|
19
|
-
};
|
|
20
|
-
|
|
25
|
+
// ── Path 1: Parse logout form from settings menu ──────────────────────
|
|
21
26
|
try {
|
|
22
27
|
const resData = await defaultFuncs
|
|
23
28
|
.post(
|
|
24
29
|
"https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1",
|
|
25
30
|
ctx.jar,
|
|
26
|
-
|
|
31
|
+
{ pmid: "0" },
|
|
27
32
|
)
|
|
28
33
|
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
// Guard every access — Facebook restructures this response frequently.
|
|
36
|
+
const instances = resData?.jsmods?.instances;
|
|
37
|
+
const firstGroup = Array.isArray(instances) && instances[0] && instances[0][2] && instances[0][2][0];
|
|
38
|
+
const elem = firstGroup && Array.isArray(firstGroup) ? firstGroup.find(v => v.value === "logout") : null;
|
|
39
|
+
|
|
40
|
+
if (elem) {
|
|
41
|
+
const markup = resData?.jsmods?.markup;
|
|
42
|
+
const markupEntry = Array.isArray(markup) ? markup.find(v => v[0] === elem.markup?.__m) : null;
|
|
43
|
+
const html = markupEntry?.[1]?.__html;
|
|
44
|
+
|
|
45
|
+
if (html) {
|
|
46
|
+
const logoutForm = {
|
|
47
|
+
fb_dtsg: utils.getFrom(html, '"fb_dtsg" value="', '"') || ctx.fb_dtsg,
|
|
48
|
+
ref: utils.getFrom(html, '"ref" value="', '"'),
|
|
49
|
+
h: utils.getFrom(html, '"h" value="', '"'),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const logoutRes = await defaultFuncs
|
|
53
|
+
.post("https://www.facebook.com/logout.php", ctx.jar, logoutForm)
|
|
54
|
+
.then(utils.saveCookies(ctx.jar));
|
|
55
|
+
|
|
56
|
+
if (logoutRes.headers && logoutRes.headers.location) {
|
|
57
|
+
await defaultFuncs
|
|
58
|
+
.get(logoutRes.headers.location, ctx.jar)
|
|
59
|
+
.then(utils.saveCookies(ctx.jar));
|
|
60
|
+
ctx.loggedIn = false;
|
|
61
|
+
utils.log("logout", "Logged out successfully (path 1).");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// If no redirect location, fall through to path 2
|
|
65
|
+
}
|
|
33
66
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
} catch (_) {
|
|
68
|
+
// Settings-menu path failed — continue to fallback
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Path 2: Direct logout using ctx.fb_dtsg ───────────────────────────
|
|
72
|
+
// This path works as long as the session token in ctx is valid.
|
|
73
|
+
// It does NOT require parsing the settings-menu HTML.
|
|
74
|
+
try {
|
|
37
75
|
const logoutForm = {
|
|
38
|
-
fb_dtsg:
|
|
39
|
-
ref:
|
|
40
|
-
h:
|
|
76
|
+
fb_dtsg: ctx.fb_dtsg || "",
|
|
77
|
+
ref: "mb",
|
|
78
|
+
h: "",
|
|
41
79
|
};
|
|
42
80
|
|
|
43
81
|
const logoutRes = await defaultFuncs
|
|
44
82
|
.post("https://www.facebook.com/logout.php", ctx.jar, logoutForm)
|
|
45
83
|
.then(utils.saveCookies(ctx.jar));
|
|
46
84
|
|
|
47
|
-
if (
|
|
48
|
-
|
|
85
|
+
if (logoutRes.headers && logoutRes.headers.location) {
|
|
86
|
+
try {
|
|
87
|
+
await defaultFuncs
|
|
88
|
+
.get(logoutRes.headers.location, ctx.jar)
|
|
89
|
+
.then(utils.saveCookies(ctx.jar));
|
|
90
|
+
} catch (_) {}
|
|
49
91
|
}
|
|
50
92
|
|
|
51
|
-
await defaultFuncs
|
|
52
|
-
.get(logoutRes.headers.location, ctx.jar)
|
|
53
|
-
.then(utils.saveCookies(ctx.jar));
|
|
54
|
-
|
|
55
93
|
ctx.loggedIn = false;
|
|
56
|
-
utils.log("logout", "Logged out successfully.");
|
|
57
|
-
|
|
94
|
+
utils.log("logout", "Logged out successfully (path 2).");
|
|
58
95
|
} catch (err) {
|
|
59
96
|
utils.error("logout", err);
|
|
60
97
|
throw err;
|
|
@@ -84,6 +84,13 @@ async function buildAPI(html, jar, netData, globalOptions, fbLinkFunc, errorRetr
|
|
|
84
84
|
const emitter = new EventEmitter();
|
|
85
85
|
emitter.setMaxListeners(50);
|
|
86
86
|
|
|
87
|
+
// Pre-compute ttstamp so it is present from the very first request.
|
|
88
|
+
// tokenRefresh.js sets this field on every refresh — initialising it here
|
|
89
|
+
// keeps ctx consistent and prevents undefined reads before the first refresh.
|
|
90
|
+
const ttstamp = dtsgResult.fb_dtsg
|
|
91
|
+
? "2" + Array.from(dtsgResult.fb_dtsg).map(c => c.charCodeAt(0)).join("")
|
|
92
|
+
: "2";
|
|
93
|
+
|
|
87
94
|
const ctx = {
|
|
88
95
|
userID,
|
|
89
96
|
jar,
|
|
@@ -108,6 +115,7 @@ async function buildAPI(html, jar, netData, globalOptions, fbLinkFunc, errorRetr
|
|
|
108
115
|
cache: new SimpleCache(),
|
|
109
116
|
validator: globalValidator,
|
|
110
117
|
_emitter: emitter,
|
|
118
|
+
ttstamp,
|
|
111
119
|
...dtsgResult,
|
|
112
120
|
};
|
|
113
121
|
const defaultFuncs = utils.makeDefaults(html, userID, ctx);
|
|
@@ -317,13 +317,18 @@ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc,
|
|
|
317
317
|
|
|
318
318
|
// Expose EventEmitter interface on the API so consumers can subscribe to
|
|
319
319
|
// key lifecycle events: 'sessionExpired', 'checkpoint', 'relogin', 'ready'
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
320
|
+
//
|
|
321
|
+
// IMPORTANT: Use ctx._emitter dynamically (not a captured snapshot).
|
|
322
|
+
// listenMqtt() replaces ctx._emitter with a fresh MessageEmitter on every
|
|
323
|
+
// call. Capturing the initial emitter here would orphan any listeners added
|
|
324
|
+
// via api.on() after listenMqtt() runs — they would never receive events
|
|
325
|
+
// because the emitter they registered on is no longer the active one.
|
|
326
|
+
if (ctx._emitter) {
|
|
327
|
+
api.on = (event, listener) => ctx._emitter.on(event, listener);
|
|
328
|
+
api.once = (event, listener) => ctx._emitter.once(event, listener);
|
|
329
|
+
api.off = (event, listener) => ctx._emitter.removeListener(event, listener);
|
|
330
|
+
api.emit = (event, ...args) => ctx._emitter.emit(event, ...args);
|
|
331
|
+
api.removeAllListeners = (event) => ctx._emitter.removeAllListeners(event);
|
|
327
332
|
}
|
|
328
333
|
|
|
329
334
|
const { TokenRefreshManager } = require('../../utils/tokenRefresh');
|
package/src/utils/autoReLogin.js
CHANGED
|
@@ -121,9 +121,35 @@ class AutoReLoginManager {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
if (api) {
|
|
124
|
-
|
|
125
|
-
api.
|
|
126
|
-
|
|
124
|
+
// CRITICAL FIX: Update the live ctx object IN-PLACE instead of
|
|
125
|
+
// replacing api.ctx. Every API method closure (sendMessage,
|
|
126
|
+
// listenMqtt, etc.) captured the original ctx variable — replacing
|
|
127
|
+
// the reference leaves all of them with stale tokens forever.
|
|
128
|
+
// Updating in-place means all closures immediately see fresh values.
|
|
129
|
+
const liveCtx = api.ctx;
|
|
130
|
+
if (liveCtx && newApi.ctx) {
|
|
131
|
+
const sessionFields = [
|
|
132
|
+
'fb_dtsg', 'jazoest', 'lsd', 'ttstamp',
|
|
133
|
+
'mqttEndpoint', 'lastSeqId', 'appID', 'clientID',
|
|
134
|
+
'userAppID', 'mqttAppID', 'userID', 'region',
|
|
135
|
+
'access_token'
|
|
136
|
+
];
|
|
137
|
+
for (const field of sessionFields) {
|
|
138
|
+
if (newApi.ctx[field] !== undefined) {
|
|
139
|
+
liveCtx[field] = newApi.ctx[field];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Reset flags that block MQTT reconnection after re-login
|
|
143
|
+
liveCtx.loggedIn = true;
|
|
144
|
+
liveCtx._ending = false;
|
|
145
|
+
liveCtx._cycling = false;
|
|
146
|
+
liveCtx._mqttReauthing = false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Do NOT replace api.defaultFuncs — the existing closures already
|
|
150
|
+
// reference the live (now-updated) ctx, so they will pick up fresh
|
|
151
|
+
// tokens without needing new function objects.
|
|
152
|
+
|
|
127
153
|
if (api.tokenRefreshManager) {
|
|
128
154
|
api.tokenRefreshManager.resetFailureCount();
|
|
129
155
|
}
|
|
@@ -216,9 +242,10 @@ class AutoReLoginManager {
|
|
|
216
242
|
updateAppState(appState) {
|
|
217
243
|
if (!this.credentials) return;
|
|
218
244
|
if (!Array.isArray(appState) || appState.length === 0) return;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
245
|
+
// Always overwrite with the freshest cookies — the old condition was too
|
|
246
|
+
// restrictive and silently skipped updates when credentials.appState was
|
|
247
|
+
// already a valid object, leaving stale cookies in re-login credentials.
|
|
248
|
+
this.credentials.appState = appState;
|
|
222
249
|
}
|
|
223
250
|
|
|
224
251
|
disable() {
|
package/src/utils/clients.js
CHANGED
|
@@ -49,22 +49,26 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
49
49
|
|
|
50
50
|
await delay(retryTime);
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// Guard against undefined Content-Type header before splitting
|
|
53
|
+
const contentType = (data.request.headers && data.request.headers["content-type"]) || "";
|
|
54
|
+
if (contentType.split(";")[0].trim() === "multipart/form-data") {
|
|
53
55
|
const newData = await http.postFormData(
|
|
54
56
|
url,
|
|
55
57
|
ctx.jar,
|
|
56
58
|
data.request.formData,
|
|
57
59
|
data.request.qs,
|
|
58
|
-
ctx.globalOptions,
|
|
59
60
|
ctx
|
|
60
61
|
);
|
|
61
62
|
return await parseAndCheckLogin(ctx, http, retryCount)(newData);
|
|
62
63
|
} else {
|
|
64
|
+
// defaultFuncs.post signature: (url, jar, form, ctxx, customHeader)
|
|
65
|
+
// The 4th arg must be ctx (not ctx.globalOptions) — passing globalOptions
|
|
66
|
+
// here caused the retry to be treated as a raw network call without
|
|
67
|
+
// session context, missing auth headers and session inspection.
|
|
63
68
|
const newData = await http.post(
|
|
64
69
|
url,
|
|
65
70
|
ctx.jar,
|
|
66
71
|
data.request.form,
|
|
67
|
-
ctx.globalOptions,
|
|
68
72
|
ctx
|
|
69
73
|
);
|
|
70
74
|
return await parseAndCheckLogin(ctx, http, retryCount)(newData);
|
|
@@ -261,9 +265,10 @@ function saveCookies(jar) {
|
|
|
261
265
|
function getAccessFromBusiness(jar, Options) {
|
|
262
266
|
return async function (res) {
|
|
263
267
|
const html = res ? res.body : null;
|
|
264
|
-
|
|
268
|
+
// Use the same axios wrapper used everywhere else — "request" module does not exist
|
|
269
|
+
const { get } = require("./axios");
|
|
265
270
|
try {
|
|
266
|
-
const businessRes = await get("https://business.facebook.com/content_management", jar, null, Options,
|
|
271
|
+
const businessRes = await get("https://business.facebook.com/content_management", jar, null, Options, { noRef: true });
|
|
267
272
|
const token = /"accessToken":"([^.]+)","clientID":/g.exec(businessRes.body)[1];
|
|
268
273
|
return [html, token];
|
|
269
274
|
} catch (e) {
|
|
@@ -177,58 +177,72 @@ function _formatAttachment(attachment1, attachment2) {
|
|
|
177
177
|
attachment1: attachment1,
|
|
178
178
|
attachment2: attachment2
|
|
179
179
|
};
|
|
180
|
-
case "MessageImage":
|
|
180
|
+
case "MessageImage": {
|
|
181
|
+
// Guard nested objects — Facebook occasionally returns MessageImage with
|
|
182
|
+
// missing preview/thumbnail/large_preview/original_dimensions objects,
|
|
183
|
+
// which causes a crash on property access (e.g. blob.preview.uri).
|
|
184
|
+
const imgThumb = blob.thumbnail || {};
|
|
185
|
+
const imgPrev = blob.preview || {};
|
|
186
|
+
const imgLarge = blob.large_preview || {};
|
|
187
|
+
const imgDims = blob.original_dimensions || {};
|
|
181
188
|
return {
|
|
182
189
|
type: "photo",
|
|
183
190
|
ID: blob.legacy_attachment_id,
|
|
184
191
|
filename: blob.filename,
|
|
185
|
-
thumbnailUrl:
|
|
186
|
-
previewUrl:
|
|
187
|
-
previewWidth:
|
|
188
|
-
previewHeight:
|
|
189
|
-
largePreviewUrl:
|
|
190
|
-
largePreviewWidth:
|
|
191
|
-
largePreviewHeight:
|
|
192
|
-
url:
|
|
193
|
-
width:
|
|
194
|
-
height:
|
|
192
|
+
thumbnailUrl: imgThumb.uri || null,
|
|
193
|
+
previewUrl: imgPrev.uri || null,
|
|
194
|
+
previewWidth: imgPrev.width || 0,
|
|
195
|
+
previewHeight: imgPrev.height || 0,
|
|
196
|
+
largePreviewUrl: imgLarge.uri || null,
|
|
197
|
+
largePreviewWidth: imgLarge.width || 0,
|
|
198
|
+
largePreviewHeight: imgLarge.height || 0,
|
|
199
|
+
url: imgLarge.uri || null,
|
|
200
|
+
width: imgDims.x || 0,
|
|
201
|
+
height: imgDims.y || 0,
|
|
195
202
|
name: blob.filename
|
|
196
203
|
};
|
|
197
|
-
|
|
204
|
+
}
|
|
205
|
+
case "MessageAnimatedImage": {
|
|
206
|
+
const aniPrev = blob.preview_image || {};
|
|
207
|
+
const aniImg = blob.animated_image || {};
|
|
198
208
|
return {
|
|
199
209
|
type: "animated_image",
|
|
200
210
|
ID: blob.legacy_attachment_id,
|
|
201
211
|
filename: blob.filename,
|
|
202
|
-
previewUrl:
|
|
203
|
-
previewWidth:
|
|
204
|
-
previewHeight:
|
|
205
|
-
url:
|
|
206
|
-
width:
|
|
207
|
-
height:
|
|
208
|
-
thumbnailUrl:
|
|
212
|
+
previewUrl: aniPrev.uri || null,
|
|
213
|
+
previewWidth: aniPrev.width || 0,
|
|
214
|
+
previewHeight: aniPrev.height || 0,
|
|
215
|
+
url: aniImg.uri || null,
|
|
216
|
+
width: aniImg.width || 0,
|
|
217
|
+
height: aniImg.height || 0,
|
|
218
|
+
thumbnailUrl: aniPrev.uri || null,
|
|
209
219
|
name: blob.filename,
|
|
210
|
-
facebookUrl:
|
|
211
|
-
rawGifImage:
|
|
212
|
-
animatedGifUrl:
|
|
213
|
-
animatedGifPreviewUrl:
|
|
214
|
-
animatedWebpUrl:
|
|
215
|
-
animatedWebpPreviewUrl:
|
|
220
|
+
facebookUrl: aniImg.uri || null,
|
|
221
|
+
rawGifImage: aniImg.uri || null,
|
|
222
|
+
animatedGifUrl: aniImg.uri || null,
|
|
223
|
+
animatedGifPreviewUrl: aniPrev.uri || null,
|
|
224
|
+
animatedWebpUrl: aniImg.uri || null,
|
|
225
|
+
animatedWebpPreviewUrl: aniPrev.uri || null
|
|
216
226
|
};
|
|
217
|
-
|
|
227
|
+
}
|
|
228
|
+
case "MessageVideo": {
|
|
229
|
+
const vidImg = blob.large_image || {};
|
|
230
|
+
const vidDims = blob.original_dimensions || {};
|
|
218
231
|
return {
|
|
219
232
|
type: "video",
|
|
220
233
|
filename: blob.filename,
|
|
221
234
|
ID: blob.legacy_attachment_id,
|
|
222
|
-
previewUrl:
|
|
223
|
-
previewWidth:
|
|
224
|
-
previewHeight:
|
|
225
|
-
url: blob.playable_url,
|
|
226
|
-
width:
|
|
227
|
-
height:
|
|
228
|
-
duration: blob.playable_duration_in_ms,
|
|
229
|
-
videoType: blob.video_type.toLowerCase(),
|
|
230
|
-
thumbnailUrl:
|
|
235
|
+
previewUrl: vidImg.uri || null,
|
|
236
|
+
previewWidth: vidImg.width || 0,
|
|
237
|
+
previewHeight: vidImg.height || 0,
|
|
238
|
+
url: blob.playable_url || null,
|
|
239
|
+
width: vidDims.x || 0,
|
|
240
|
+
height: vidDims.y || 0,
|
|
241
|
+
duration: blob.playable_duration_in_ms || 0,
|
|
242
|
+
videoType: blob.video_type ? blob.video_type.toLowerCase() : "unknown",
|
|
243
|
+
thumbnailUrl: vidImg.uri || null
|
|
231
244
|
};
|
|
245
|
+
}
|
|
232
246
|
case "MessageFile":
|
|
233
247
|
return {
|
|
234
248
|
type: "file",
|
|
@@ -37,17 +37,24 @@ function formatDeltaMessage(m) {
|
|
|
37
37
|
isReply: true
|
|
38
38
|
} : null;
|
|
39
39
|
|
|
40
|
+
// Guard: actorFbId can be null on system/admin messages; threadKey can be
|
|
41
|
+
// missing on malformed deltas from Facebook — both crash with .toString().
|
|
42
|
+
const senderID = md.actorFbId != null ? formatID(md.actorFbId.toString()) : "0";
|
|
43
|
+
const threadKey = md.threadKey || {};
|
|
44
|
+
const threadRaw = threadKey.threadFbId || threadKey.otherUserFbId;
|
|
45
|
+
const threadID = threadRaw != null ? formatID(threadRaw.toString()) : "0";
|
|
46
|
+
|
|
40
47
|
return {
|
|
41
48
|
type: "message",
|
|
42
|
-
senderID
|
|
49
|
+
senderID,
|
|
43
50
|
body: m.delta.body || "",
|
|
44
|
-
threadID
|
|
51
|
+
threadID,
|
|
45
52
|
messageID: md.messageId,
|
|
46
53
|
offlineThreadingId: md.offlineThreadingId,
|
|
47
54
|
attachments: (m.delta.attachments || []).map(v => _formatAttachment(v)),
|
|
48
55
|
mentions: mentions,
|
|
49
56
|
timestamp: md.timestamp,
|
|
50
|
-
isGroup: !!
|
|
57
|
+
isGroup: !!threadKey.threadFbId,
|
|
51
58
|
participantIDs: m.delta.participants,
|
|
52
59
|
messageReply: messageReply
|
|
53
60
|
};
|
|
@@ -79,24 +86,35 @@ function formatDeltaEvent(m) {
|
|
|
79
86
|
logMessageData = m;
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
// Guard: messageMetadata or threadKey may be absent on some delta variants
|
|
90
|
+
const meta = m.messageMetadata || {};
|
|
91
|
+
const evtKey = meta.threadKey || {};
|
|
92
|
+
const evtThreadRaw = evtKey.threadFbId || evtKey.otherUserFbId;
|
|
93
|
+
const evtThreadID = evtThreadRaw != null ? formatID(evtThreadRaw.toString()) : "0";
|
|
94
|
+
const evtMessageID = meta.messageId != null ? meta.messageId.toString() : "";
|
|
95
|
+
|
|
82
96
|
return {
|
|
83
97
|
type: "event",
|
|
84
|
-
threadID:
|
|
85
|
-
messageID:
|
|
98
|
+
threadID: evtThreadID,
|
|
99
|
+
messageID: evtMessageID,
|
|
86
100
|
logMessageType,
|
|
87
101
|
logMessageData,
|
|
88
|
-
logMessageBody:
|
|
89
|
-
timestamp:
|
|
90
|
-
author:
|
|
102
|
+
logMessageBody: meta.adminText,
|
|
103
|
+
timestamp: meta.timestamp,
|
|
104
|
+
author: meta.actorFbId,
|
|
91
105
|
participantIDs: m.participants
|
|
92
106
|
};
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
function formatDeltaReadReceipt(delta) {
|
|
110
|
+
// Guard: threadKey or its sub-fields may be missing in some receipt variants
|
|
111
|
+
const tk = delta.threadKey || {};
|
|
112
|
+
const reader = (tk.otherUserFbId || delta.actorFbId);
|
|
113
|
+
const threadRaw = tk.otherUserFbId || tk.threadFbId;
|
|
96
114
|
return {
|
|
97
|
-
reader:
|
|
115
|
+
reader: reader != null ? reader.toString() : "0",
|
|
98
116
|
time: delta.actionTimestampMs,
|
|
99
|
-
threadID: formatID(
|
|
117
|
+
threadID: threadRaw != null ? formatID(threadRaw.toString()) : "0",
|
|
100
118
|
type: "read_receipt"
|
|
101
119
|
};
|
|
102
120
|
}
|