@lazyneoaz/metachat 5.0.0 → 5.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/dist/index.mjs CHANGED
@@ -25415,7 +25415,11 @@ function looksLikeCheckpoint(res) {
25415
25415
  return true;
25416
25416
  }
25417
25417
  const body = typeof res?.body === "string" ? res.body : "";
25418
- return /"error":1357004|account has been (?:disabled|suspended)|confirm your identity|security check/i.test(body.slice(0, 400));
25418
+ if (!body) return false;
25419
+ if (/"isNotCritical"\s*:\s*1/.test(body.slice(0, 400))) return false;
25420
+ return /account has been (?:disabled|suspended)|confirm your identity|security check|we (?:suspended|disabled) your account/i.test(
25421
+ body.slice(0, 400)
25422
+ );
25419
25423
  }
25420
25424
  async function requestWithRetry(requestFunction, retries = 3, baseDelay = 1e3, ctx) {
25421
25425
  const emit2 = (event, payload) => {
@@ -75519,6 +75523,7 @@ __export(utils_exports2, {
75519
75523
  BASE_COOLDOWN_MS: () => BASE_COOLDOWN_MS,
75520
75524
  BRAND: () => BRAND,
75521
75525
  CHROME_MAJORS: () => CHROME_MAJORS,
75526
+ CORE_REACTIONS: () => CORE_REACTIONS,
75522
75527
  ConfigurationError: () => ConfigurationError,
75523
75528
  DESKTOP_PLATFORMS: () => DESKTOP_PLATFORMS,
75524
75529
  FCAError: () => FCAError,
@@ -75530,6 +75535,7 @@ __export(utils_exports2, {
75530
75535
  NotFoundError: () => NotFoundError,
75531
75536
  PING_GRACE_MS: () => PING_GRACE_MS,
75532
75537
  PermissionError: () => PermissionError,
75538
+ REACTION_ALIASES: () => REACTION_ALIASES,
75533
75539
  RateLimitError: () => RateLimitError,
75534
75540
  RealtimeHealth: () => RealtimeHealth,
75535
75541
  RecoverySupervisor: () => RecoverySupervisor,
@@ -75611,6 +75617,8 @@ __export(utils_exports2, {
75611
75617
  isAuthFailure: () => isAuthFailure,
75612
75618
  isBreakerOpen: () => isBreakerOpen,
75613
75619
  isLoggedOutDocument: () => isLoggedOutDocument,
75620
+ isNonCriticalError: () => isNonCriticalError,
75621
+ isReactionEmoji: () => isReactionEmoji,
75614
75622
  isReadableStream: () => isReadableStream2,
75615
75623
  isStaleTokenError: () => isStaleTokenError,
75616
75624
  json: () => json2,
@@ -75621,6 +75629,7 @@ __export(utils_exports2, {
75621
75629
  meta: () => meta,
75622
75630
  missingSessionCookies: () => missingSessionCookies,
75623
75631
  normalizeBrowserProfile: () => normalizeBrowserProfile,
75632
+ normalizeReaction: () => normalizeReaction,
75624
75633
  noteAction: () => noteAction,
75625
75634
  noteAuthFailure: () => noteAuthFailure,
75626
75635
  noteCheckpoint: () => noteCheckpoint,
@@ -75642,6 +75651,7 @@ __export(utils_exports2, {
75642
75651
  resetBreaker: () => resetBreaker,
75643
75652
  resolveProfile: () => resolveProfile,
75644
75653
  retryDelay: () => retryDelay,
75654
+ retryOnNonCritical: () => retryOnNonCritical,
75645
75655
  safetySnapshot: () => safetySnapshot,
75646
75656
  saveCookies: () => saveCookies,
75647
75657
  scheduleSessionKeepAlive: () => scheduleSessionKeepAlive,
@@ -90718,6 +90728,30 @@ function buildWsTaskContent(ctx, tasks, versionId) {
90718
90728
  var import_form_data3 = __toESM(require_form_data2());
90719
90729
  init_constants();
90720
90730
  init_user_agents();
90731
+
90732
+ // src/utils/transient.ts
90733
+ var NON_CRITICAL_CODES = /* @__PURE__ */ new Set([1357004, 1357001, 1357031]);
90734
+ function isNonCriticalError(value) {
90735
+ if (!value || typeof value !== "object") return false;
90736
+ const body = value.body && typeof value.body === "object" ? value.body : value;
90737
+ if (body.isNotCritical === 1 || body.is_not_critical === 1) return true;
90738
+ const num = Number(body.error ?? body.errorCode ?? body.code);
90739
+ if (NON_CRITICAL_CODES.has(num)) return true;
90740
+ const text3 = String(body.errorDescription || body.errorSummary || body.message || "");
90741
+ return /re-?open(ing)? your browser|try again(?: later)?|temporarily unavailable/i.test(text3);
90742
+ }
90743
+ async function retryOnNonCritical(operation, refresh) {
90744
+ try {
90745
+ const first2 = await operation();
90746
+ if (!isNonCriticalError(first2)) return first2;
90747
+ } catch (error2) {
90748
+ if (!isNonCriticalError(error2)) throw error2;
90749
+ }
90750
+ await refresh();
90751
+ return operation();
90752
+ }
90753
+
90754
+ // src/utils/attachments.ts
90721
90755
  function cleanJsonResponse(value) {
90722
90756
  if (typeof value !== "string") return value;
90723
90757
  const normalized = value.replace(/^for\s*\(;;\);\s*/i, "");
@@ -90810,18 +90844,76 @@ function buildUploadQuery(ctx) {
90810
90844
  return qs2;
90811
90845
  }
90812
90846
  var EXTENSION_BY_MIME = {
90847
+ // video
90813
90848
  "video/mp4": "mp4",
90849
+ "video/quicktime": "mov",
90850
+ "video/webm": "webm",
90851
+ "video/x-matroska": "mkv",
90852
+ "video/x-msvideo": "avi",
90853
+ "video/mpeg": "mpeg",
90854
+ "video/3gpp": "3gp",
90855
+ // audio
90814
90856
  "audio/mp4": "m4a",
90857
+ "audio/x-m4a": "m4a",
90815
90858
  "audio/mpeg": "mp3",
90816
90859
  "audio/mp3": "mp3",
90817
90860
  "audio/aac": "aac",
90818
90861
  "audio/ogg": "ogg",
90862
+ "audio/opus": "opus",
90819
90863
  "audio/wav": "wav",
90864
+ "audio/x-wav": "wav",
90865
+ "audio/webm": "weba",
90866
+ "audio/flac": "flac",
90867
+ "audio/x-flac": "flac",
90868
+ "audio/3gpp": "3gp",
90869
+ // image
90820
90870
  "image/jpeg": "jpg",
90871
+ "image/jpg": "jpg",
90821
90872
  "image/png": "png",
90822
90873
  "image/gif": "gif",
90823
- "image/webp": "webp"
90874
+ "image/webp": "webp",
90875
+ "image/bmp": "bmp",
90876
+ "image/tiff": "tiff",
90877
+ "image/svg+xml": "svg",
90878
+ "image/heic": "heic",
90879
+ "image/heif": "heif",
90880
+ "image/avif": "avif",
90881
+ // documents / archives
90882
+ "application/pdf": "pdf",
90883
+ "application/zip": "zip",
90884
+ "application/x-zip-compressed": "zip",
90885
+ "application/x-rar-compressed": "rar",
90886
+ "application/vnd.rar": "rar",
90887
+ "application/x-7z-compressed": "7z",
90888
+ "application/gzip": "gz",
90889
+ "application/x-tar": "tar",
90890
+ "text/plain": "txt",
90891
+ "text/csv": "csv",
90892
+ "text/html": "html",
90893
+ "text/calendar": "ics",
90894
+ "application/json": "json",
90895
+ "application/xml": "xml",
90896
+ "text/xml": "xml",
90897
+ "application/msword": "doc",
90898
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
90899
+ "application/vnd.ms-excel": "xls",
90900
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
90901
+ "application/vnd.ms-powerpoint": "ppt",
90902
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
90903
+ "application/vnd.android.package-archive": "apk",
90904
+ "application/x-msdownload": "exe",
90905
+ "application/octet-stream": "bin"
90824
90906
  };
90907
+ function extensionForMime(mime) {
90908
+ if (!mime) return "";
90909
+ const normalized = String(mime).toLowerCase().split(";")[0].trim();
90910
+ if (EXTENSION_BY_MIME[normalized]) return EXTENSION_BY_MIME[normalized];
90911
+ const subtype = normalized.split("/")[1] || "";
90912
+ if (!subtype) return "";
90913
+ if (/\+([a-z0-9]+)$/.test(subtype)) return subtype.replace(/.*\+/, "");
90914
+ const cleaned = subtype.replace(/^vnd\./, "").replace(/^x-/, "").replace(/[-.]/g, "");
90915
+ return /^[a-z0-9]{1,10}$/.test(cleaned) ? cleaned : "";
90916
+ }
90825
90917
  function sniffMime(buffer) {
90826
90918
  if (buffer.length < 12) return "";
90827
90919
  const b = buffer;
@@ -90846,11 +90938,11 @@ function sniffMime(buffer) {
90846
90938
  }
90847
90939
  function normalizeFilename(filename, mime) {
90848
90940
  const base = filename && String(filename).trim() ? String(filename).trim() : `file-${Date.now()}`;
90849
- const ext = EXTENSION_BY_MIME[mime] || "";
90850
- const match = /\.[a-z0-9]{1,5}$/i.exec(base);
90941
+ const ext = extensionForMime(mime);
90942
+ const match = /\.[a-z0-9]{1,6}$/i.exec(base);
90851
90943
  const currentExt = match ? match[0].slice(1).toLowerCase() : "";
90852
90944
  if (ext && currentExt !== ext) {
90853
- const audioOverride = (ext === "m4a" || ext === "mp3" || ext === "aac" || ext === "ogg" || ext === "wav") && (currentExt === "mp4" || currentExt === "m4v" || currentExt === "");
90945
+ const audioOverride = (ext === "m4a" || ext === "mp3" || ext === "aac" || ext === "ogg" || ext === "wav" || ext === "opus" || ext === "flac") && (currentExt === "mp4" || currentExt === "m4v" || currentExt === "");
90854
90946
  if (audioOverride || !currentExt) {
90855
90947
  return currentExt ? `${base.slice(0, base.length - currentExt.length - 1)}.${ext}` : `${base}.${ext}`;
90856
90948
  }
@@ -90860,16 +90952,45 @@ function normalizeFilename(filename, mime) {
90860
90952
  }
90861
90953
  async function uploadMercuryAttachment(ctx, defaultFuncs, attachment) {
90862
90954
  const normalized = await normalizeAttachmentInput(attachment, ctx);
90955
+ const buffer = await readAll(normalized.stream);
90863
90956
  const url2 = "https://www.facebook.com/ajax/mercury/upload.php";
90957
+ let lastError;
90958
+ for (let attempt = 0; attempt < 2; attempt++) {
90959
+ if (attempt > 0) await refreshUploadTokens(ctx, defaultFuncs);
90960
+ await ensureUploadTokens(ctx, defaultFuncs);
90961
+ const formData = new import_form_data3.default();
90962
+ formData.append("farr", Readable.from(buffer), {
90963
+ filename: normalized.filename,
90964
+ contentType: normalized.contentType || void 0
90965
+ });
90966
+ let data2;
90967
+ try {
90968
+ const res = await defaultFuncs.postMultipart(url2, ctx.jar, formData, buildUploadQuery(ctx));
90969
+ data2 = cleanJsonResponse(res && res.body !== void 0 ? res.body : res);
90970
+ } catch (error2) {
90971
+ lastError = error2;
90972
+ if (attempt === 0 && isNonCriticalError(error2)) continue;
90973
+ throw error2;
90974
+ }
90975
+ if (extractAttachmentIds(data2).length) return finalizeUpload(data2);
90976
+ lastError = buildNoMetadataError(data2);
90977
+ if (attempt === 0 && isNonCriticalError(lastError)) continue;
90978
+ throw lastError;
90979
+ }
90980
+ throw lastError || buildNoMetadataError(null);
90981
+ }
90982
+ function buildNoMetadataError(data2) {
90983
+ const err = new Error("UploadFb returned no metadata/ids");
90984
+ err.code = "NO_METADATA";
90985
+ err.body = data2;
90986
+ return err;
90987
+ }
90988
+ async function refreshUploadTokens(ctx, defaultFuncs) {
90989
+ ctx.lsd = void 0;
90990
+ ctx.master = ctx.master || {};
90991
+ ctx.master.__rev = void 0;
90992
+ if (typeof ctx.__cometTokens === "object") ctx.__cometTokens = void 0;
90864
90993
  await ensureUploadTokens(ctx, defaultFuncs);
90865
- const formData = new import_form_data3.default();
90866
- formData.append("farr", normalized.stream, {
90867
- filename: normalized.filename,
90868
- contentType: normalized.contentType || void 0
90869
- });
90870
- const res = await defaultFuncs.postMultipart(url2, ctx.jar, formData, buildUploadQuery(ctx));
90871
- const data2 = cleanJsonResponse(res && res.body !== void 0 ? res.body : res);
90872
- return finalizeUpload(data2);
90873
90994
  }
90874
90995
  async function normalizeAttachmentInput(attachment, ctx) {
90875
90996
  if (Buffer.isBuffer(attachment)) {
@@ -90976,6 +91097,68 @@ async function uploadGroupImage(ctx, defaultFuncs, image) {
90976
91097
  return cleanJsonResponse(res && res.body !== void 0 ? res.body : res);
90977
91098
  }
90978
91099
 
91100
+ // src/utils/reactions.ts
91101
+ var CORE_REACTIONS = [
91102
+ "\u{1F60D}",
91103
+ "\u{1F606}",
91104
+ "\u{1F62E}",
91105
+ "\u{1F622}",
91106
+ "\u{1F620}",
91107
+ "\u{1F44D}",
91108
+ "\u{1F44E}",
91109
+ "\u2764",
91110
+ "\u{1F497}",
91111
+ ""
91112
+ ];
91113
+ var REACTION_ALIASES = {
91114
+ ":heart_eyes:": "\u{1F60D}",
91115
+ ":love:": "\u{1F60D}",
91116
+ ":laughing:": "\u{1F606}",
91117
+ ":haha:": "\u{1F606}",
91118
+ ":open_mouth:": "\u{1F62E}",
91119
+ ":wow:": "\u{1F62E}",
91120
+ ":cry:": "\u{1F622}",
91121
+ ":sad:": "\u{1F622}",
91122
+ ":angry:": "\u{1F620}",
91123
+ ":thumbsup:": "\u{1F44D}",
91124
+ ":like:": "\u{1F44D}",
91125
+ ":thumbsdown:": "\u{1F44E}",
91126
+ ":dislike:": "\u{1F44E}",
91127
+ ":heart:": "\u2764",
91128
+ ":glowingheart:": "\u{1F497}"
91129
+ };
91130
+ function isReactionEmoji(value) {
91131
+ if (value == null) return false;
91132
+ const text3 = String(value).trim();
91133
+ if (!text3) return false;
91134
+ if (CORE_REACTIONS.indexOf(text3) !== -1) return true;
91135
+ let clusters;
91136
+ try {
91137
+ const seg = new Intl.Segmenter(void 0, { granularity: "grapheme" });
91138
+ clusters = Array.from(seg.segment(text3), (s) => s.segment);
91139
+ } catch {
91140
+ clusters = Array.from(text3);
91141
+ }
91142
+ if (clusters.length !== 1) return false;
91143
+ const only = clusters[0];
91144
+ if (!new RegExp("\\p{Extended_Pictographic}", "u").test(only) && !/\uFE0F|\u20E3/u.test(only)) return false;
91145
+ if (!/[\p{Extended_Pictographic}\uFE0F\u20E3]/u.test(only)) return false;
91146
+ if (/[\u0000-\u001F\u007F]/.test(only)) return false;
91147
+ return true;
91148
+ }
91149
+ function normalizeReaction(value, options = {}) {
91150
+ const raw = value == null ? "" : String(value).trim();
91151
+ if (raw === "") {
91152
+ if (options.allowRemove) return "";
91153
+ throw new Error("Reaction is not a valid emoji.");
91154
+ }
91155
+ const resolved = REACTION_ALIASES[raw] !== void 0 ? REACTION_ALIASES[raw] : raw;
91156
+ if (!options.forceCustom && !isReactionEmoji(resolved)) {
91157
+ throw new Error(`Reaction is not a valid emoji: ${JSON.stringify(raw)}`);
91158
+ }
91159
+ return resolved;
91160
+ }
91161
+
90979
91162
  // src/utils/lsd.ts
90980
91163
  var TOKEN_TTL_MS = 10 * 60 * 1e3;
90981
91164
  var STALE_TOKEN_ERRORS = /* @__PURE__ */ new Set([
@@ -91770,10 +91953,10 @@ function startRecovery(ctx, runtime, options = {}) {
91770
91953
 
91771
91954
  // src/utils/realtime-health.ts
91772
91955
  init_pacing();
91773
- var SILENT_WINDOW_MS = 6 * 60 * 1e3;
91774
- var PING_GRACE_MS = 45 * 1e3;
91956
+ var SILENT_WINDOW_MS = 30 * 60 * 1e3;
91957
+ var PING_GRACE_MS = 60 * 1e3;
91775
91958
  var RealtimeHealth = class {
91776
- constructor(now = () => Date.now()) {
91959
+ constructor(now = () => Date.now(), options = {}) {
91777
91960
  this.now = now;
91778
91961
  this.frames = 0;
91779
91962
  this.lastFrameAt = 0;
@@ -91782,17 +91965,23 @@ var RealtimeHealth = class {
91782
91965
  this.forcedReconnects = 0;
91783
91966
  this.lastPingAt = 0;
91784
91967
  this.startedAt = Date.now();
91968
+ /** Consecutive pings sent without any resulting frame. */
91969
+ this.silentPings = 0;
91785
91970
  this.startedAt = this.now();
91971
+ this.silentWindowMs = options.silentWindowMs ?? SILENT_WINDOW_MS;
91972
+ this.pingGraceMs = options.pingGraceMs ?? PING_GRACE_MS;
91786
91973
  }
91787
91974
  /** Record any inbound frame (message, pong, anything from the broker). */
91788
91975
  noteFrame() {
91789
91976
  this.frames += 1;
91790
91977
  this.lastFrameAt = this.now();
91978
+ this.silentPings = 0;
91791
91979
  }
91792
91980
  /** Record a successful connect. */
91793
91981
  noteConnected() {
91794
91982
  this.connectedAt = this.now();
91795
91983
  this.lastFrameAt = this.now();
91984
+ this.silentPings = 0;
91796
91985
  }
91797
91986
  /** Record that the transport reports itself disconnected. */
91798
91987
  noteDisconnected() {
@@ -91801,6 +91990,7 @@ var RealtimeHealth = class {
91801
91990
  /** Record an active ping being sent. */
91802
91991
  notePing() {
91803
91992
  this.lastPingAt = this.now();
91993
+ this.silentPings += 1;
91804
91994
  }
91805
91995
  sinceLastFrame() {
91806
91996
  const base = this.lastFrameAt || this.connectedAt || this.startedAt;
@@ -91810,8 +92000,8 @@ var RealtimeHealth = class {
91810
92000
  * Decide what to do about the current socket.
91811
92001
  *
91812
92002
  * - `disconnected` — the transport already knows it is down; reconnect.
91813
- * - `dead` — connected on paper, silent past the window, and a ping raised no
91814
- * traffic within the grace period; reconnect.
92003
+ * - `dead` — connected on paper, silent well past the window, and repeated
92004
+ * pings raised no traffic at all; reconnect.
91815
92005
  * - `suspect` — silent past the window but a ping was only just sent; wait.
91816
92006
  * - `quiet` — silent but inside the window; normal.
91817
92007
  * - `healthy` — recent traffic.
@@ -91819,9 +92009,10 @@ var RealtimeHealth = class {
91819
92009
  verdict(connected) {
91820
92010
  if (!connected) return "disconnected";
91821
92011
  const silent = this.sinceLastFrame();
91822
- if (silent < SILENT_WINDOW_MS / 2) return "healthy";
91823
- if (silent < SILENT_WINDOW_MS) return "quiet";
91824
- if (this.lastPingAt && this.now() - this.lastPingAt < PING_GRACE_MS) return "suspect";
92012
+ if (silent < this.silentWindowMs / 2) return "healthy";
92013
+ if (silent < this.silentWindowMs) return "quiet";
92014
+ if (this.lastPingAt && this.now() - this.lastPingAt < this.pingGraceMs) return "suspect";
92015
+ if (this.silentPings < 3) return "suspect";
91825
92016
  return "dead";
91826
92017
  }
91827
92018
  /** A snapshot for diagnostics. */
@@ -91843,8 +92034,11 @@ var RealtimeHealth = class {
91843
92034
  }
91844
92035
  };
91845
92036
  function startRealtimeSupervisor(options) {
91846
- const health = new RealtimeHealth();
91847
- const interval = options.intervalMs ?? 3 * 60 * 1e3;
92037
+ const health = options.health || new RealtimeHealth(void 0, {
92038
+ silentWindowMs: options.silentWindowMs,
92039
+ pingGraceMs: options.pingGraceMs
92040
+ });
92041
+ const interval = options.intervalMs ?? 10 * 60 * 1e3;
91848
92042
  let stopped = false;
91849
92043
  let timer = null;
91850
92044
  const evaluate = () => {
@@ -93561,7 +93755,8 @@ var allowedProperties = {
93561
93755
  emojiSize: true,
93562
93756
  body: true,
93563
93757
  mentions: true,
93564
- location: true
93758
+ location: true,
93759
+ edit: true
93565
93760
  };
93566
93761
  var sendMessage_default = (defaultFuncs, api, ctx) => {
93567
93762
  async function uploadAttachment(attachments) {
@@ -93683,6 +93878,18 @@ var sendMessage_default = (defaultFuncs, api, ctx) => {
93683
93878
  if (msgType === "String") {
93684
93879
  msg = { body: msg };
93685
93880
  }
93881
+ if (Array.isArray(msg.edit) && msg.edit.length > 0) {
93882
+ let body = String(msg.body || "");
93883
+ const edits = [...msg.edit].sort((a, b) => Number(b?.[1] ?? 0) - Number(a?.[1] ?? 0));
93884
+ for (const entry of edits) {
93885
+ const text3 = Array.isArray(entry) ? String(entry[0] ?? "") : String(entry ?? "");
93886
+ const rawOffset = Array.isArray(entry) ? Number(entry[1]) : NaN;
93887
+ const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.min(body.length, rawOffset)) : body.length;
93888
+ body = body.slice(0, offset) + text3 + body.slice(offset);
93889
+ }
93890
+ msg.body = body;
93891
+ delete msg.edit;
93892
+ }
93686
93893
  const finish = (err, data2) => {
93687
93894
  settle2(err, data2);
93688
93895
  return data2;
@@ -93956,45 +94163,6 @@ function sendTypingIndicator_default(defaultFuncs, api, ctx) {
93956
94163
  }
93957
94164
 
93958
94165
  // src/deltas/apis/messaging/setMessageReaction.ts
93959
- var VALID_REACTIONS = [
93960
- "\u{1F60D}",
93961
- // :heart_eyes:
93962
- "\u{1F606}",
93963
- // :laughing:
93964
- "\u{1F62E}",
93965
- // :open_mouth:
93966
- "\u{1F622}",
93967
- // :cry:
93968
- "\u{1F620}",
93969
- // :angry:
93970
- "\u{1F44D}",
93971
- // :thumbsup:
93972
- "\u{1F44E}",
93973
- // :thumbsdown:
93974
- "\u2764",
93975
- // :heart:
93976
- "\u{1F497}",
93977
- // :glowingheart:
93978
- ""
93979
- // remove
93980
- ];
93981
- var ALIASES = {
93982
- ":heart_eyes:": "\u{1F60D}",
93983
- ":love:": "\u{1F60D}",
93984
- ":laughing:": "\u{1F606}",
93985
- ":haha:": "\u{1F606}",
93986
- ":open_mouth:": "\u{1F62E}",
93987
- ":wow:": "\u{1F62E}",
93988
- ":cry:": "\u{1F622}",
93989
- ":sad:": "\u{1F622}",
93990
- ":angry:": "\u{1F620}",
93991
- ":thumbsup:": "\u{1F44D}",
93992
- ":like:": "\u{1F44D}",
93993
- ":thumbsdown:": "\u{1F44E}",
93994
- ":dislike:": "\u{1F44E}",
93995
- ":heart:": "\u2764",
93996
- ":glowingheart:": "\u{1F497}"
93997
- };
93998
94166
  function setMessageReaction_default(defaultFuncs, _api, ctx) {
93999
94167
  return async function setMessageReaction(reaction, messageID, callback, forceCustomReaction) {
94000
94168
  const cb = typeof callback === "function" ? callback : void 0;
@@ -94011,11 +94179,14 @@ function setMessageReaction_default(defaultFuncs, _api, ctx) {
94011
94179
  if (!messageID) {
94012
94180
  throw { error: "setMessageReaction: messageID is required." };
94013
94181
  }
94014
- let value = reaction == null ? "" : String(reaction);
94015
- if (ALIASES[value] !== void 0) {
94016
- value = ALIASES[value];
94017
- } else if (value !== "" && VALID_REACTIONS.indexOf(value) === -1 && !forceCustomReaction) {
94018
- throw { error: "Reaction is not a valid emoji." };
94182
+ let value;
94183
+ try {
94184
+ value = normalizeReaction(reaction, {
94185
+ allowRemove: true,
94186
+ forceCustom: !!forceCustomReaction
94187
+ });
94188
+ } catch (validationError) {
94189
+ throw { error: validationError?.message || "Reaction is not a valid emoji." };
94019
94190
  }
94020
94191
  const variables = {
94021
94192
  data: {
@@ -94807,33 +94978,35 @@ function searchMusic_default(defaultFuncs, _api, ctx) {
94807
94978
  }
94808
94979
  const page = collectTracks(parsed);
94809
94980
  const tracks = page.tracks.slice(0, count);
94810
- if (tracks.length === 0) {
94981
+ if (tracks.length) {
94982
+ const result = tracks.slice();
94983
+ result.tracks = tracks;
94984
+ result.endCursor = page.endCursor;
94985
+ result.hasNextPage = page.hasNextPage;
94986
+ result.query = text3;
94987
+ return result;
94988
+ }
94989
+ for (let retry = 0; retry < 3; retry++) {
94811
94990
  invalidateCometTokens(ctx);
94991
+ await sleep(600 + retry * 900);
94812
94992
  const retried = await attempt(true);
94813
- if (!isStaleTokenError(retried) && !retried.error && !(retried.errors || []).length) {
94814
- const retryPage = collectTracks(retried);
94815
- const retryTracks = retryPage.tracks.slice(0, count);
94816
- if (retryTracks.length) {
94817
- const result2 = retryTracks.slice();
94818
- result2.tracks = retryTracks;
94819
- result2.endCursor = retryPage.endCursor;
94820
- result2.hasNextPage = retryPage.hasNextPage;
94821
- result2.query = text3;
94822
- return result2;
94823
- }
94993
+ if (isStaleTokenError(retried) || retried?.error || (retried?.errors || []).length) continue;
94994
+ const retryPage = collectTracks(retried);
94995
+ const retryTracks = retryPage.tracks.slice(0, count);
94996
+ if (retryTracks.length) {
94997
+ const result = retryTracks.slice();
94998
+ result.tracks = retryTracks;
94999
+ result.endCursor = retryPage.endCursor;
95000
+ result.hasNextPage = retryPage.hasNextPage;
95001
+ result.query = text3;
95002
+ return result;
94824
95003
  }
94825
- throw {
94826
- error: `No music results found for "${text3}". The Facebook music catalog may be unavailable for this account or region.`,
94827
- code: "NO_RESULTS",
94828
- query: text3
94829
- };
94830
95004
  }
94831
- const result = tracks.slice();
94832
- result.tracks = tracks;
94833
- result.endCursor = page.endCursor;
94834
- result.hasNextPage = page.hasNextPage;
94835
- result.query = text3;
94836
- return result;
95005
+ throw {
95006
+ error: `No music results found for "${text3}". The Facebook music catalog may be unavailable for this account or region.`,
95007
+ code: "NO_RESULTS",
95008
+ query: text3
95009
+ };
94837
95010
  }
94838
95011
  return function searchMusic(query = "", options = {}, callback) {
94839
95012
  const selectedOptions = typeof options === "function" ? {} : options || {};
@@ -98990,15 +99163,22 @@ function story_default(defaultFuncs, api, ctx) {
98990
99163
  }
98991
99164
  async function sendStoryReply(storyIdOrUrl, message, isReaction) {
98992
99165
  try {
98993
- const allowedReactions = ["\u2764\uFE0F", "\u{1F44D}", "\u{1F917}", "\u{1F606}", "\u{1F621}", "\u{1F622}", "\u{1F62E}"];
98994
99166
  if (!storyIdOrUrl) throw new Error("Story ID or URL is required.");
98995
99167
  if (!message) throw new Error("A message or reaction is required.");
99168
+ let reactionValue = message;
99169
+ if (isReaction) {
99170
+ try {
99171
+ reactionValue = normalizeReaction(message);
99172
+ } catch (validationError) {
99173
+ throw new Error(validationError?.message || "Reaction is not a valid emoji.");
99174
+ }
99175
+ }
98996
99176
  let storyID = getStoryIDFromURL(storyIdOrUrl);
98997
99177
  if (!storyID) storyID = storyIdOrUrl;
98998
99178
  const variables = {
98999
99179
  input: {
99000
99180
  attribution_id_v2: "StoriesCometSuspenseRoot.react,comet.stories.viewer,via_cold_start",
99001
- message,
99181
+ message: reactionValue,
99002
99182
  story_id: storyID,
99003
99183
  story_reply_type: isReaction ? "LIGHT_WEIGHT" : "TEXT",
99004
99184
  actor_id: ctx.userID,
@@ -99006,12 +99186,9 @@ function story_default(defaultFuncs, api, ctx) {
99006
99186
  }
99007
99187
  };
99008
99188
  if (isReaction) {
99009
- if (!allowedReactions.includes(message)) {
99010
- throw new Error(`Invalid reaction. Please use one of: ${allowedReactions.join(" ")}`);
99011
- }
99012
99189
  variables.input.lightweight_reaction_actions = {
99013
99190
  offsets: [0],
99014
- reaction: message
99191
+ reaction: reactionValue
99015
99192
  };
99016
99193
  }
99017
99194
  const form = {
@@ -99420,14 +99597,32 @@ function getThreadInfo_default(defaultFuncs, api, ctx) {
99420
99597
  throw new Error("getThreadInfo: malformed response from GraphQL.");
99421
99598
  }
99422
99599
  const threadInfos = {};
99423
- for (let i2 = Math.min(resData.length - 2, threadIDs.length - 1); i2 >= 0; i2--) {
99600
+ let matched = 0;
99601
+ for (let i2 = 0; i2 < resData.length; i2++) {
99424
99602
  const res = resData[i2];
99425
- if (!res || res.error_results || !res[Object.keys(res)[0]]?.data) continue;
99426
- const threadInfo = formatThreadGraphQLResponse(res[Object.keys(res)[0]].data);
99603
+ if (!res || typeof res !== "object" || res.error_results) continue;
99604
+ const keys = Object.keys(res);
99605
+ const queryKey = keys.find((k) => /^o\d+$/.test(k));
99606
+ let index3;
99607
+ if (queryKey) {
99608
+ index3 = Number(queryKey.slice(1));
99609
+ } else if (typeof res.__ar === "number" || res.error !== void 0) {
99610
+ continue;
99611
+ } else {
99612
+ index3 = i2;
99613
+ }
99614
+ if (index3 < 0 || index3 >= threadIDs.length) continue;
99615
+ const data2 = res[queryKey || keys[0]];
99616
+ if (!data2 || !data2.data) continue;
99617
+ const threadInfo = formatThreadGraphQLResponse(data2.data);
99427
99618
  if (threadInfo) {
99428
- threadInfos[threadInfo.threadID || threadIDs[threadIDs.length - 1 - i2]] = threadInfo;
99619
+ threadInfos[threadInfo.threadID || threadIDs[index3]] = threadInfo;
99620
+ matched++;
99429
99621
  }
99430
99622
  }
99623
+ if (matched === 0) {
99624
+ throw new Error("getThreadInfo: malformed response from GraphQL.");
99625
+ }
99431
99626
  return Array.isArray(threadID) ? threadInfos : Object.values(threadInfos)[0] || null;
99432
99627
  } catch (err) {
99433
99628
  error("getThreadInfo", err);
@@ -99966,7 +100161,10 @@ var getUserInfo_default = (defaultFuncs, api, ctx) => {
99966
100161
  form[`ids[${i2}]`] = v;
99967
100162
  });
99968
100163
  const getGenderString = (code) => code === 1 ? "male" : code === 2 ? "female" : "no specific gender";
99969
- defaultFuncs.post("https://www.facebook.com/chat/user_info/", ctx.jar, form).then((resData) => parseAndCheckLogin(ctx, defaultFuncs)(resData)).then((resData) => {
100164
+ const performFetch = () => defaultFuncs.post("https://www.facebook.com/chat/user_info/", ctx.jar, form).then((resData) => parseAndCheckLogin(ctx, defaultFuncs)(resData));
100165
+ retryOnNonCritical(performFetch, () => {
100166
+ invalidateCometTokens?.(ctx);
100167
+ }).then((resData) => {
99970
100168
  if (resData?.error && resData?.error !== 3252001) throw resData;
99971
100169
  const retObj = {};
99972
100170
  const profiles = resData?.payload?.profiles;
@@ -101092,6 +101290,9 @@ var listenMqtt_default = (defaultFuncs, api, ctx) => {
101092
101290
  }
101093
101291
  lifecycle.healthSupervisor = startRealtimeSupervisor({
101094
101292
  isConnected: () => generation === lifecycle.generation && client2.connected,
101293
+ // Share the per-connection tracker so this supervisor sees
101294
+ // every inbound frame the listener records.
101295
+ health,
101095
101296
  reconnect: (reason) => {
101096
101297
  if (generation !== lifecycle.generation) return;
101097
101298
  if (lifecycle.stopped || ctx.loggedIn === false) return;
@@ -101474,6 +101675,7 @@ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc,
101474
101675
  }
101475
101676
  if (listenMqtt_default) {
101476
101677
  api["listenMqtt"] = listenMqtt_default(defaultFuncs, api, ctx);
101678
+ api["listen"] = api["listenMqtt"];
101477
101679
  }
101478
101680
  };
101479
101681
  api.getCurrentUserID = () => ctx.userID;
@@ -101798,4 +102000,4 @@ lodash/lodash.js:
101798
102000
  *)
101799
102001
  */
101800
102002
 
101801
- export { PING_GRACE_MS, RealtimeHealth, RecoverySupervisor, SILENT_WINDOW_MS, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, collectSessionCookies, index_default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isLoggedOutDocument, isStaleTokenError, login, missingSessionCookies, normalizeBrowserProfile, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, public_exports as publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, safetySnapshot, sleep, startLiveness, startRealtimeSupervisor, startRecovery, tripBreaker, warmUpSession, withCometTokens, withSessionRetry };
102003
+ export { CORE_REACTIONS, PING_GRACE_MS, REACTION_ALIASES, RealtimeHealth, RecoverySupervisor, SILENT_WINDOW_MS, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, cleanJsonResponse, collectSessionCookies, index_default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isLoggedOutDocument, isNonCriticalError, isReactionEmoji, isStaleTokenError, login, missingSessionCookies, normalizeBrowserProfile, normalizeReaction, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, public_exports as publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, retryOnNonCritical, safetySnapshot, sleep, startLiveness, startRealtimeSupervisor, startRecovery, tripBreaker, uploadGroupImage, uploadMercuryAttachment, warmUpSession, withCometTokens, withSessionRetry };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyneoaz/metachat",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "A modern TypeScript Facebook Chat API client for building reliable Messenger bots, with built-in anti-detection protection, automatic cookie refresh, music search, and AI theme support.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -57,7 +57,7 @@
57
57
  "prepublishOnly": "npm run build",
58
58
  "lint": "npm run typecheck",
59
59
  "format": "prettier --write \"src/**/*.ts\" \"index.ts\"",
60
- "test": "npm run build && node test/smoke.test.cjs && node test/smoke.test.mjs && node test/music-search.test.cjs && node test/mqtt-lifecycle.test.cjs && node test/session-reliability.test.cjs && node test/anti-detection.test.cjs && node test/account-safety.test.cjs && node test/liveness.test.cjs && node test/recovery.test.cjs && node test/cookie-acquisition.test.cjs && node test/realtime-health.test.cjs",
60
+ "test": "npm run build && node test/smoke.test.cjs && node test/smoke.test.mjs && node test/music-search.test.cjs && node test/mqtt-lifecycle.test.cjs && node test/session-reliability.test.cjs && node test/anti-detection.test.cjs && node test/account-safety.test.cjs && node test/liveness.test.cjs && node test/recovery.test.cjs && node test/cookie-acquisition.test.cjs && node test/realtime-health.test.cjs && node test/attachment-resilience.test.cjs && node test/reaction-validation.test.cjs && node test/api-surface-fixes.test.cjs && node test/threadinfo-regression.test.cjs",
61
61
  "audit": "npm audit",
62
62
  "audit:fix": "npm audit fix"
63
63
  },