@lazyneoaz/metachat 5.0.1 → 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.cjs CHANGED
@@ -75558,6 +75558,7 @@ __export(utils_exports2, {
75558
75558
  BASE_COOLDOWN_MS: () => BASE_COOLDOWN_MS,
75559
75559
  BRAND: () => BRAND,
75560
75560
  CHROME_MAJORS: () => CHROME_MAJORS,
75561
+ CORE_REACTIONS: () => CORE_REACTIONS,
75561
75562
  ConfigurationError: () => ConfigurationError,
75562
75563
  DESKTOP_PLATFORMS: () => DESKTOP_PLATFORMS,
75563
75564
  FCAError: () => FCAError,
@@ -75569,6 +75570,7 @@ __export(utils_exports2, {
75569
75570
  NotFoundError: () => NotFoundError,
75570
75571
  PING_GRACE_MS: () => PING_GRACE_MS,
75571
75572
  PermissionError: () => PermissionError,
75573
+ REACTION_ALIASES: () => REACTION_ALIASES,
75572
75574
  RateLimitError: () => RateLimitError,
75573
75575
  RealtimeHealth: () => RealtimeHealth,
75574
75576
  RecoverySupervisor: () => RecoverySupervisor,
@@ -75650,6 +75652,8 @@ __export(utils_exports2, {
75650
75652
  isAuthFailure: () => isAuthFailure,
75651
75653
  isBreakerOpen: () => isBreakerOpen,
75652
75654
  isLoggedOutDocument: () => isLoggedOutDocument,
75655
+ isNonCriticalError: () => isNonCriticalError,
75656
+ isReactionEmoji: () => isReactionEmoji,
75653
75657
  isReadableStream: () => isReadableStream2,
75654
75658
  isStaleTokenError: () => isStaleTokenError,
75655
75659
  json: () => json2,
@@ -75660,6 +75664,7 @@ __export(utils_exports2, {
75660
75664
  meta: () => meta,
75661
75665
  missingSessionCookies: () => missingSessionCookies,
75662
75666
  normalizeBrowserProfile: () => normalizeBrowserProfile,
75667
+ normalizeReaction: () => normalizeReaction,
75663
75668
  noteAction: () => noteAction,
75664
75669
  noteAuthFailure: () => noteAuthFailure,
75665
75670
  noteCheckpoint: () => noteCheckpoint,
@@ -75681,6 +75686,7 @@ __export(utils_exports2, {
75681
75686
  resetBreaker: () => resetBreaker,
75682
75687
  resolveProfile: () => resolveProfile,
75683
75688
  retryDelay: () => retryDelay,
75689
+ retryOnNonCritical: () => retryOnNonCritical,
75684
75690
  safetySnapshot: () => safetySnapshot,
75685
75691
  saveCookies: () => saveCookies,
75686
75692
  scheduleSessionKeepAlive: () => scheduleSessionKeepAlive,
@@ -90757,6 +90763,30 @@ function buildWsTaskContent(ctx, tasks, versionId) {
90757
90763
  var import_form_data3 = __toESM(require_form_data2());
90758
90764
  init_constants();
90759
90765
  init_user_agents();
90766
+
90767
+ // src/utils/transient.ts
90768
+ var NON_CRITICAL_CODES = /* @__PURE__ */ new Set([1357004, 1357001, 1357031]);
90769
+ function isNonCriticalError(value) {
90770
+ if (!value || typeof value !== "object") return false;
90771
+ const body = value.body && typeof value.body === "object" ? value.body : value;
90772
+ if (body.isNotCritical === 1 || body.is_not_critical === 1) return true;
90773
+ const num = Number(body.error ?? body.errorCode ?? body.code);
90774
+ if (NON_CRITICAL_CODES.has(num)) return true;
90775
+ const text3 = String(body.errorDescription || body.errorSummary || body.message || "");
90776
+ return /re-?open(ing)? your browser|try again(?: later)?|temporarily unavailable/i.test(text3);
90777
+ }
90778
+ async function retryOnNonCritical(operation, refresh) {
90779
+ try {
90780
+ const first2 = await operation();
90781
+ if (!isNonCriticalError(first2)) return first2;
90782
+ } catch (error2) {
90783
+ if (!isNonCriticalError(error2)) throw error2;
90784
+ }
90785
+ await refresh();
90786
+ return operation();
90787
+ }
90788
+
90789
+ // src/utils/attachments.ts
90760
90790
  function cleanJsonResponse(value) {
90761
90791
  if (typeof value !== "string") return value;
90762
90792
  const normalized = value.replace(/^for\s*\(;;\);\s*/i, "");
@@ -90849,18 +90879,76 @@ function buildUploadQuery(ctx) {
90849
90879
  return qs2;
90850
90880
  }
90851
90881
  var EXTENSION_BY_MIME = {
90882
+ // video
90852
90883
  "video/mp4": "mp4",
90884
+ "video/quicktime": "mov",
90885
+ "video/webm": "webm",
90886
+ "video/x-matroska": "mkv",
90887
+ "video/x-msvideo": "avi",
90888
+ "video/mpeg": "mpeg",
90889
+ "video/3gpp": "3gp",
90890
+ // audio
90853
90891
  "audio/mp4": "m4a",
90892
+ "audio/x-m4a": "m4a",
90854
90893
  "audio/mpeg": "mp3",
90855
90894
  "audio/mp3": "mp3",
90856
90895
  "audio/aac": "aac",
90857
90896
  "audio/ogg": "ogg",
90897
+ "audio/opus": "opus",
90858
90898
  "audio/wav": "wav",
90899
+ "audio/x-wav": "wav",
90900
+ "audio/webm": "weba",
90901
+ "audio/flac": "flac",
90902
+ "audio/x-flac": "flac",
90903
+ "audio/3gpp": "3gp",
90904
+ // image
90859
90905
  "image/jpeg": "jpg",
90906
+ "image/jpg": "jpg",
90860
90907
  "image/png": "png",
90861
90908
  "image/gif": "gif",
90862
- "image/webp": "webp"
90909
+ "image/webp": "webp",
90910
+ "image/bmp": "bmp",
90911
+ "image/tiff": "tiff",
90912
+ "image/svg+xml": "svg",
90913
+ "image/heic": "heic",
90914
+ "image/heif": "heif",
90915
+ "image/avif": "avif",
90916
+ // documents / archives
90917
+ "application/pdf": "pdf",
90918
+ "application/zip": "zip",
90919
+ "application/x-zip-compressed": "zip",
90920
+ "application/x-rar-compressed": "rar",
90921
+ "application/vnd.rar": "rar",
90922
+ "application/x-7z-compressed": "7z",
90923
+ "application/gzip": "gz",
90924
+ "application/x-tar": "tar",
90925
+ "text/plain": "txt",
90926
+ "text/csv": "csv",
90927
+ "text/html": "html",
90928
+ "text/calendar": "ics",
90929
+ "application/json": "json",
90930
+ "application/xml": "xml",
90931
+ "text/xml": "xml",
90932
+ "application/msword": "doc",
90933
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
90934
+ "application/vnd.ms-excel": "xls",
90935
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
90936
+ "application/vnd.ms-powerpoint": "ppt",
90937
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
90938
+ "application/vnd.android.package-archive": "apk",
90939
+ "application/x-msdownload": "exe",
90940
+ "application/octet-stream": "bin"
90863
90941
  };
90942
+ function extensionForMime(mime) {
90943
+ if (!mime) return "";
90944
+ const normalized = String(mime).toLowerCase().split(";")[0].trim();
90945
+ if (EXTENSION_BY_MIME[normalized]) return EXTENSION_BY_MIME[normalized];
90946
+ const subtype = normalized.split("/")[1] || "";
90947
+ if (!subtype) return "";
90948
+ if (/\+([a-z0-9]+)$/.test(subtype)) return subtype.replace(/.*\+/, "");
90949
+ const cleaned = subtype.replace(/^vnd\./, "").replace(/^x-/, "").replace(/[-.]/g, "");
90950
+ return /^[a-z0-9]{1,10}$/.test(cleaned) ? cleaned : "";
90951
+ }
90864
90952
  function sniffMime(buffer) {
90865
90953
  if (buffer.length < 12) return "";
90866
90954
  const b = buffer;
@@ -90885,11 +90973,11 @@ function sniffMime(buffer) {
90885
90973
  }
90886
90974
  function normalizeFilename(filename, mime) {
90887
90975
  const base = filename && String(filename).trim() ? String(filename).trim() : `file-${Date.now()}`;
90888
- const ext = EXTENSION_BY_MIME[mime] || "";
90889
- const match = /\.[a-z0-9]{1,5}$/i.exec(base);
90976
+ const ext = extensionForMime(mime);
90977
+ const match = /\.[a-z0-9]{1,6}$/i.exec(base);
90890
90978
  const currentExt = match ? match[0].slice(1).toLowerCase() : "";
90891
90979
  if (ext && currentExt !== ext) {
90892
- const audioOverride = (ext === "m4a" || ext === "mp3" || ext === "aac" || ext === "ogg" || ext === "wav") && (currentExt === "mp4" || currentExt === "m4v" || currentExt === "");
90980
+ const audioOverride = (ext === "m4a" || ext === "mp3" || ext === "aac" || ext === "ogg" || ext === "wav" || ext === "opus" || ext === "flac") && (currentExt === "mp4" || currentExt === "m4v" || currentExt === "");
90893
90981
  if (audioOverride || !currentExt) {
90894
90982
  return currentExt ? `${base.slice(0, base.length - currentExt.length - 1)}.${ext}` : `${base}.${ext}`;
90895
90983
  }
@@ -90916,12 +91004,12 @@ async function uploadMercuryAttachment(ctx, defaultFuncs, attachment) {
90916
91004
  data2 = cleanJsonResponse(res && res.body !== void 0 ? res.body : res);
90917
91005
  } catch (error2) {
90918
91006
  lastError = error2;
90919
- if (attempt === 0 && isTransientUploadError(error2)) continue;
91007
+ if (attempt === 0 && isNonCriticalError(error2)) continue;
90920
91008
  throw error2;
90921
91009
  }
90922
91010
  if (extractAttachmentIds(data2).length) return finalizeUpload(data2);
90923
91011
  lastError = buildNoMetadataError(data2);
90924
- if (attempt === 0 && isTransientUploadError(lastError)) continue;
91012
+ if (attempt === 0 && isNonCriticalError(lastError)) continue;
90925
91013
  throw lastError;
90926
91014
  }
90927
91015
  throw lastError || buildNoMetadataError(null);
@@ -90932,14 +91020,6 @@ function buildNoMetadataError(data2) {
90932
91020
  err.body = data2;
90933
91021
  return err;
90934
91022
  }
90935
- function isTransientUploadError(value) {
90936
- const body = value && (value.body || value);
90937
- if (!body || typeof body !== "object") return false;
90938
- const num = Number(body.error ?? body.errorCode);
90939
- if (num === 1357004 || num === 1357001) return true;
90940
- const text3 = String(body.errorDescription || body.errorSummary || value?.message || "");
90941
- return /re-?open(ing)? your browser|try again|temporarily unavailable/i.test(text3);
90942
- }
90943
91023
  async function refreshUploadTokens(ctx, defaultFuncs) {
90944
91024
  ctx.lsd = void 0;
90945
91025
  ctx.master = ctx.master || {};
@@ -91052,6 +91132,68 @@ async function uploadGroupImage(ctx, defaultFuncs, image) {
91052
91132
  return cleanJsonResponse(res && res.body !== void 0 ? res.body : res);
91053
91133
  }
91054
91134
 
91135
+ // src/utils/reactions.ts
91136
+ var CORE_REACTIONS = [
91137
+ "\u{1F60D}",
91138
+ "\u{1F606}",
91139
+ "\u{1F62E}",
91140
+ "\u{1F622}",
91141
+ "\u{1F620}",
91142
+ "\u{1F44D}",
91143
+ "\u{1F44E}",
91144
+ "\u2764",
91145
+ "\u{1F497}",
91146
+ ""
91147
+ ];
91148
+ var REACTION_ALIASES = {
91149
+ ":heart_eyes:": "\u{1F60D}",
91150
+ ":love:": "\u{1F60D}",
91151
+ ":laughing:": "\u{1F606}",
91152
+ ":haha:": "\u{1F606}",
91153
+ ":open_mouth:": "\u{1F62E}",
91154
+ ":wow:": "\u{1F62E}",
91155
+ ":cry:": "\u{1F622}",
91156
+ ":sad:": "\u{1F622}",
91157
+ ":angry:": "\u{1F620}",
91158
+ ":thumbsup:": "\u{1F44D}",
91159
+ ":like:": "\u{1F44D}",
91160
+ ":thumbsdown:": "\u{1F44E}",
91161
+ ":dislike:": "\u{1F44E}",
91162
+ ":heart:": "\u2764",
91163
+ ":glowingheart:": "\u{1F497}"
91164
+ };
91165
+ function isReactionEmoji(value) {
91166
+ if (value == null) return false;
91167
+ const text3 = String(value).trim();
91168
+ if (!text3) return false;
91169
+ if (CORE_REACTIONS.indexOf(text3) !== -1) return true;
91170
+ let clusters;
91171
+ try {
91172
+ const seg = new Intl.Segmenter(void 0, { granularity: "grapheme" });
91173
+ clusters = Array.from(seg.segment(text3), (s) => s.segment);
91174
+ } catch {
91175
+ clusters = Array.from(text3);
91176
+ }
91177
+ if (clusters.length !== 1) return false;
91178
+ const only = clusters[0];
91179
+ if (!new RegExp("\\p{Extended_Pictographic}", "u").test(only) && !/\uFE0F|\u20E3/u.test(only)) return false;
91180
+ if (!/[\p{Extended_Pictographic}\uFE0F\u20E3]/u.test(only)) return false;
91181
+ if (/[\u0000-\u001F\u007F]/.test(only)) return false;
91182
+ return true;
91183
+ }
91184
+ function normalizeReaction(value, options = {}) {
91185
+ const raw = value == null ? "" : String(value).trim();
91186
+ if (raw === "") {
91187
+ if (options.allowRemove) return "";
91188
+ throw new Error("Reaction is not a valid emoji.");
91189
+ }
91190
+ const resolved = REACTION_ALIASES[raw] !== void 0 ? REACTION_ALIASES[raw] : raw;
91191
+ if (!options.forceCustom && !isReactionEmoji(resolved)) {
91192
+ throw new Error(`Reaction is not a valid emoji: ${JSON.stringify(raw)}`);
91193
+ }
91194
+ return resolved;
91195
+ }
91196
+
91055
91197
  // src/utils/lsd.ts
91056
91198
  var TOKEN_TTL_MS = 10 * 60 * 1e3;
91057
91199
  var STALE_TOKEN_ERRORS = /* @__PURE__ */ new Set([
@@ -93648,7 +93790,8 @@ var allowedProperties = {
93648
93790
  emojiSize: true,
93649
93791
  body: true,
93650
93792
  mentions: true,
93651
- location: true
93793
+ location: true,
93794
+ edit: true
93652
93795
  };
93653
93796
  var sendMessage_default = (defaultFuncs, api, ctx) => {
93654
93797
  async function uploadAttachment(attachments) {
@@ -93770,6 +93913,18 @@ var sendMessage_default = (defaultFuncs, api, ctx) => {
93770
93913
  if (msgType === "String") {
93771
93914
  msg = { body: msg };
93772
93915
  }
93916
+ if (Array.isArray(msg.edit) && msg.edit.length > 0) {
93917
+ let body = String(msg.body || "");
93918
+ const edits = [...msg.edit].sort((a, b) => Number(b?.[1] ?? 0) - Number(a?.[1] ?? 0));
93919
+ for (const entry of edits) {
93920
+ const text3 = Array.isArray(entry) ? String(entry[0] ?? "") : String(entry ?? "");
93921
+ const rawOffset = Array.isArray(entry) ? Number(entry[1]) : NaN;
93922
+ const offset = Number.isFinite(rawOffset) ? Math.max(0, Math.min(body.length, rawOffset)) : body.length;
93923
+ body = body.slice(0, offset) + text3 + body.slice(offset);
93924
+ }
93925
+ msg.body = body;
93926
+ delete msg.edit;
93927
+ }
93773
93928
  const finish = (err, data2) => {
93774
93929
  settle2(err, data2);
93775
93930
  return data2;
@@ -94043,45 +94198,6 @@ function sendTypingIndicator_default(defaultFuncs, api, ctx) {
94043
94198
  }
94044
94199
 
94045
94200
  // src/deltas/apis/messaging/setMessageReaction.ts
94046
- var VALID_REACTIONS = [
94047
- "\u{1F60D}",
94048
- // :heart_eyes:
94049
- "\u{1F606}",
94050
- // :laughing:
94051
- "\u{1F62E}",
94052
- // :open_mouth:
94053
- "\u{1F622}",
94054
- // :cry:
94055
- "\u{1F620}",
94056
- // :angry:
94057
- "\u{1F44D}",
94058
- // :thumbsup:
94059
- "\u{1F44E}",
94060
- // :thumbsdown:
94061
- "\u2764",
94062
- // :heart:
94063
- "\u{1F497}",
94064
- // :glowingheart:
94065
- ""
94066
- // remove
94067
- ];
94068
- var ALIASES = {
94069
- ":heart_eyes:": "\u{1F60D}",
94070
- ":love:": "\u{1F60D}",
94071
- ":laughing:": "\u{1F606}",
94072
- ":haha:": "\u{1F606}",
94073
- ":open_mouth:": "\u{1F62E}",
94074
- ":wow:": "\u{1F62E}",
94075
- ":cry:": "\u{1F622}",
94076
- ":sad:": "\u{1F622}",
94077
- ":angry:": "\u{1F620}",
94078
- ":thumbsup:": "\u{1F44D}",
94079
- ":like:": "\u{1F44D}",
94080
- ":thumbsdown:": "\u{1F44E}",
94081
- ":dislike:": "\u{1F44E}",
94082
- ":heart:": "\u2764",
94083
- ":glowingheart:": "\u{1F497}"
94084
- };
94085
94201
  function setMessageReaction_default(defaultFuncs, _api, ctx) {
94086
94202
  return async function setMessageReaction(reaction, messageID, callback, forceCustomReaction) {
94087
94203
  const cb = typeof callback === "function" ? callback : void 0;
@@ -94098,11 +94214,14 @@ function setMessageReaction_default(defaultFuncs, _api, ctx) {
94098
94214
  if (!messageID) {
94099
94215
  throw { error: "setMessageReaction: messageID is required." };
94100
94216
  }
94101
- let value = reaction == null ? "" : String(reaction);
94102
- if (ALIASES[value] !== void 0) {
94103
- value = ALIASES[value];
94104
- } else if (value !== "" && VALID_REACTIONS.indexOf(value) === -1 && !forceCustomReaction) {
94105
- throw { error: "Reaction is not a valid emoji." };
94217
+ let value;
94218
+ try {
94219
+ value = normalizeReaction(reaction, {
94220
+ allowRemove: true,
94221
+ forceCustom: !!forceCustomReaction
94222
+ });
94223
+ } catch (validationError) {
94224
+ throw { error: validationError?.message || "Reaction is not a valid emoji." };
94106
94225
  }
94107
94226
  const variables = {
94108
94227
  data: {
@@ -94894,33 +95013,35 @@ function searchMusic_default(defaultFuncs, _api, ctx) {
94894
95013
  }
94895
95014
  const page = collectTracks(parsed);
94896
95015
  const tracks = page.tracks.slice(0, count);
94897
- if (tracks.length === 0) {
95016
+ if (tracks.length) {
95017
+ const result = tracks.slice();
95018
+ result.tracks = tracks;
95019
+ result.endCursor = page.endCursor;
95020
+ result.hasNextPage = page.hasNextPage;
95021
+ result.query = text3;
95022
+ return result;
95023
+ }
95024
+ for (let retry = 0; retry < 3; retry++) {
94898
95025
  invalidateCometTokens(ctx);
95026
+ await sleep(600 + retry * 900);
94899
95027
  const retried = await attempt(true);
94900
- if (!isStaleTokenError(retried) && !retried.error && !(retried.errors || []).length) {
94901
- const retryPage = collectTracks(retried);
94902
- const retryTracks = retryPage.tracks.slice(0, count);
94903
- if (retryTracks.length) {
94904
- const result2 = retryTracks.slice();
94905
- result2.tracks = retryTracks;
94906
- result2.endCursor = retryPage.endCursor;
94907
- result2.hasNextPage = retryPage.hasNextPage;
94908
- result2.query = text3;
94909
- return result2;
94910
- }
95028
+ if (isStaleTokenError(retried) || retried?.error || (retried?.errors || []).length) continue;
95029
+ const retryPage = collectTracks(retried);
95030
+ const retryTracks = retryPage.tracks.slice(0, count);
95031
+ if (retryTracks.length) {
95032
+ const result = retryTracks.slice();
95033
+ result.tracks = retryTracks;
95034
+ result.endCursor = retryPage.endCursor;
95035
+ result.hasNextPage = retryPage.hasNextPage;
95036
+ result.query = text3;
95037
+ return result;
94911
95038
  }
94912
- throw {
94913
- error: `No music results found for "${text3}". The Facebook music catalog may be unavailable for this account or region.`,
94914
- code: "NO_RESULTS",
94915
- query: text3
94916
- };
94917
95039
  }
94918
- const result = tracks.slice();
94919
- result.tracks = tracks;
94920
- result.endCursor = page.endCursor;
94921
- result.hasNextPage = page.hasNextPage;
94922
- result.query = text3;
94923
- return result;
95040
+ throw {
95041
+ error: `No music results found for "${text3}". The Facebook music catalog may be unavailable for this account or region.`,
95042
+ code: "NO_RESULTS",
95043
+ query: text3
95044
+ };
94924
95045
  }
94925
95046
  return function searchMusic(query = "", options = {}, callback) {
94926
95047
  const selectedOptions = typeof options === "function" ? {} : options || {};
@@ -99077,15 +99198,22 @@ function story_default(defaultFuncs, api, ctx) {
99077
99198
  }
99078
99199
  async function sendStoryReply(storyIdOrUrl, message, isReaction) {
99079
99200
  try {
99080
- const allowedReactions = ["\u2764\uFE0F", "\u{1F44D}", "\u{1F917}", "\u{1F606}", "\u{1F621}", "\u{1F622}", "\u{1F62E}"];
99081
99201
  if (!storyIdOrUrl) throw new Error("Story ID or URL is required.");
99082
99202
  if (!message) throw new Error("A message or reaction is required.");
99203
+ let reactionValue = message;
99204
+ if (isReaction) {
99205
+ try {
99206
+ reactionValue = normalizeReaction(message);
99207
+ } catch (validationError) {
99208
+ throw new Error(validationError?.message || "Reaction is not a valid emoji.");
99209
+ }
99210
+ }
99083
99211
  let storyID = getStoryIDFromURL(storyIdOrUrl);
99084
99212
  if (!storyID) storyID = storyIdOrUrl;
99085
99213
  const variables = {
99086
99214
  input: {
99087
99215
  attribution_id_v2: "StoriesCometSuspenseRoot.react,comet.stories.viewer,via_cold_start",
99088
- message,
99216
+ message: reactionValue,
99089
99217
  story_id: storyID,
99090
99218
  story_reply_type: isReaction ? "LIGHT_WEIGHT" : "TEXT",
99091
99219
  actor_id: ctx.userID,
@@ -99093,12 +99221,9 @@ function story_default(defaultFuncs, api, ctx) {
99093
99221
  }
99094
99222
  };
99095
99223
  if (isReaction) {
99096
- if (!allowedReactions.includes(message)) {
99097
- throw new Error(`Invalid reaction. Please use one of: ${allowedReactions.join(" ")}`);
99098
- }
99099
99224
  variables.input.lightweight_reaction_actions = {
99100
99225
  offsets: [0],
99101
- reaction: message
99226
+ reaction: reactionValue
99102
99227
  };
99103
99228
  }
99104
99229
  const form = {
@@ -99507,14 +99632,32 @@ function getThreadInfo_default(defaultFuncs, api, ctx) {
99507
99632
  throw new Error("getThreadInfo: malformed response from GraphQL.");
99508
99633
  }
99509
99634
  const threadInfos = {};
99510
- for (let i2 = Math.min(resData.length - 2, threadIDs.length - 1); i2 >= 0; i2--) {
99635
+ let matched = 0;
99636
+ for (let i2 = 0; i2 < resData.length; i2++) {
99511
99637
  const res = resData[i2];
99512
- if (!res || res.error_results || !res[Object.keys(res)[0]]?.data) continue;
99513
- const threadInfo = formatThreadGraphQLResponse(res[Object.keys(res)[0]].data);
99638
+ if (!res || typeof res !== "object" || res.error_results) continue;
99639
+ const keys = Object.keys(res);
99640
+ const queryKey = keys.find((k) => /^o\d+$/.test(k));
99641
+ let index3;
99642
+ if (queryKey) {
99643
+ index3 = Number(queryKey.slice(1));
99644
+ } else if (typeof res.__ar === "number" || res.error !== void 0) {
99645
+ continue;
99646
+ } else {
99647
+ index3 = i2;
99648
+ }
99649
+ if (index3 < 0 || index3 >= threadIDs.length) continue;
99650
+ const data2 = res[queryKey || keys[0]];
99651
+ if (!data2 || !data2.data) continue;
99652
+ const threadInfo = formatThreadGraphQLResponse(data2.data);
99514
99653
  if (threadInfo) {
99515
- threadInfos[threadInfo.threadID || threadIDs[threadIDs.length - 1 - i2]] = threadInfo;
99654
+ threadInfos[threadInfo.threadID || threadIDs[index3]] = threadInfo;
99655
+ matched++;
99516
99656
  }
99517
99657
  }
99658
+ if (matched === 0) {
99659
+ throw new Error("getThreadInfo: malformed response from GraphQL.");
99660
+ }
99518
99661
  return Array.isArray(threadID) ? threadInfos : Object.values(threadInfos)[0] || null;
99519
99662
  } catch (err) {
99520
99663
  error("getThreadInfo", err);
@@ -100053,7 +100196,10 @@ var getUserInfo_default = (defaultFuncs, api, ctx) => {
100053
100196
  form[`ids[${i2}]`] = v;
100054
100197
  });
100055
100198
  const getGenderString = (code) => code === 1 ? "male" : code === 2 ? "female" : "no specific gender";
100056
- defaultFuncs.post("https://www.facebook.com/chat/user_info/", ctx.jar, form).then((resData) => parseAndCheckLogin(ctx, defaultFuncs)(resData)).then((resData) => {
100199
+ const performFetch = () => defaultFuncs.post("https://www.facebook.com/chat/user_info/", ctx.jar, form).then((resData) => parseAndCheckLogin(ctx, defaultFuncs)(resData));
100200
+ retryOnNonCritical(performFetch, () => {
100201
+ invalidateCometTokens?.(ctx);
100202
+ }).then((resData) => {
100057
100203
  if (resData?.error && resData?.error !== 3252001) throw resData;
100058
100204
  const retObj = {};
100059
100205
  const profiles = resData?.payload?.profiles;
@@ -101564,6 +101710,7 @@ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc,
101564
101710
  }
101565
101711
  if (listenMqtt_default) {
101566
101712
  api["listenMqtt"] = listenMqtt_default(defaultFuncs, api, ctx);
101713
+ api["listen"] = api["listenMqtt"];
101567
101714
  }
101568
101715
  };
101569
101716
  api.getCurrentUserID = () => ctx.userID;
@@ -101888,7 +102035,9 @@ lodash/lodash.js:
101888
102035
  *)
101889
102036
  */
101890
102037
 
102038
+ exports.CORE_REACTIONS = CORE_REACTIONS;
101891
102039
  exports.PING_GRACE_MS = PING_GRACE_MS;
102040
+ exports.REACTION_ALIASES = REACTION_ALIASES;
101892
102041
  exports.RealtimeHealth = RealtimeHealth;
101893
102042
  exports.RecoverySupervisor = RecoverySupervisor;
101894
102043
  exports.SILENT_WINDOW_MS = SILENT_WINDOW_MS;
@@ -101914,10 +102063,13 @@ exports.invalidateCometTokens = invalidateCometTokens;
101914
102063
  exports.isAuthFailure = isAuthFailure;
101915
102064
  exports.isBreakerOpen = isBreakerOpen;
101916
102065
  exports.isLoggedOutDocument = isLoggedOutDocument;
102066
+ exports.isNonCriticalError = isNonCriticalError;
102067
+ exports.isReactionEmoji = isReactionEmoji;
101917
102068
  exports.isStaleTokenError = isStaleTokenError;
101918
102069
  exports.login = login;
101919
102070
  exports.missingSessionCookies = missingSessionCookies;
101920
102071
  exports.normalizeBrowserProfile = normalizeBrowserProfile;
102072
+ exports.normalizeReaction = normalizeReaction;
101921
102073
  exports.noteAction = noteAction;
101922
102074
  exports.noteAuthFailure = noteAuthFailure;
101923
102075
  exports.noteCheckpoint = noteCheckpoint;
@@ -101931,6 +102083,7 @@ exports.reserveRequest = reserveRequest;
101931
102083
  exports.resetBreaker = resetBreaker;
101932
102084
  exports.resolveProfile = resolveProfile;
101933
102085
  exports.retryDelay = retryDelay;
102086
+ exports.retryOnNonCritical = retryOnNonCritical;
101934
102087
  exports.safetySnapshot = safetySnapshot;
101935
102088
  exports.sleep = sleep;
101936
102089
  exports.startLiveness = startLiveness;
package/dist/index.d.mts CHANGED
@@ -1261,6 +1261,25 @@ declare function uploadMercuryAttachment(ctx: any, defaultFuncs: any, attachment
1261
1261
  */
1262
1262
  declare function uploadGroupImage(ctx: any, defaultFuncs: any, image: any): Promise<any>;
1263
1263
 
1264
+ /**
1265
+ * Shared reaction validation for every surface that sends a reaction.
1266
+ * Facebook accepts any single emoji; a whitelist silently rejects valid picks.
1267
+ */
1268
+ declare const CORE_REACTIONS: string[];
1269
+ declare const REACTION_ALIASES: Record<string, string>;
1270
+ declare function isReactionEmoji(value: unknown): boolean;
1271
+ declare function normalizeReaction(value: unknown, options?: {
1272
+ allowRemove?: boolean;
1273
+ forceCustom?: boolean;
1274
+ }): string;
1275
+
1276
+ /**
1277
+ * Facebook "non-critical" errors (e.g. 1357004) are retryable and must not be
1278
+ * treated as auth failures or checkpoints.
1279
+ */
1280
+ declare function isNonCriticalError(value: any): boolean;
1281
+ declare function retryOnNonCritical<T = any>(operation: () => Promise<T>, refresh: () => void | Promise<void>): Promise<T>;
1282
+
1264
1283
  type ApiContext = ApiContext$1;
1265
1284
  type ApiInternal = Api;
1266
1285
  type DefaultFuncs = DefaultFuncs$1;
@@ -1271,4 +1290,4 @@ declare const _default: typeof login$1 & {
1271
1290
  default: typeof login$1;
1272
1291
  };
1273
1292
 
1274
- export { type API, type AddedStickerPackInfo, type AiTheme, type AnyAttachment, type ApiContext, type ApiInternal, type Attachment, type AudioAttachment, type BrowserProfile, type Callback, type CommentMessage, type CommentResult, type Coordinates, type DefaultFuncs, type EmojiEvent, type Event, type FileAttachment, type GroupNameEvent, type ListenEvent, type LivenessHandle, type LivenessState, type LoginCredentials, type LoginOptions, type Mention, type Message, type MessageID, type MessageObject, type MessageReply, type MusicTag, type MusicTrack, type NicknameEvent, PING_GRACE_MS, type PhotoAttachment, type Reaction, RealtimeHealth, type RealtimeHealthState, type RecoveryEvent, type RecoveryOptions, type RecoveryRuntime, type RecoveryState, RecoverySupervisor, SILENT_WINDOW_MS, type SearchMusicOptions, type SearchMusicResult, type ShareAttachment, type ShareResult, type StickerAttachment, type StickerInfo, type StickerPackInfo, type ThreadID, type ThreadInfo, type ThreadThemeEvent, type TypingIndicator, type UnsendMessageEvent, type UserID, type UserInfo, type VideoAttachment, type WarmUpResult, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, cleanJsonResponse, collectSessionCookies, _default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isLoggedOutDocument, isStaleTokenError, login$1 as login, missingSessionCookies, normalizeBrowserProfile, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, safetySnapshot, sleep, startLiveness, startRealtimeSupervisor, startRecovery, tripBreaker, uploadGroupImage, uploadMercuryAttachment, warmUpSession, withCometTokens, withSessionRetry };
1293
+ export { type API, type AddedStickerPackInfo, type AiTheme, type AnyAttachment, type ApiContext, type ApiInternal, type Attachment, type AudioAttachment, type BrowserProfile, CORE_REACTIONS, type Callback, type CommentMessage, type CommentResult, type Coordinates, type DefaultFuncs, type EmojiEvent, type Event, type FileAttachment, type GroupNameEvent, type ListenEvent, type LivenessHandle, type LivenessState, type LoginCredentials, type LoginOptions, type Mention, type Message, type MessageID, type MessageObject, type MessageReply, type MusicTag, type MusicTrack, type NicknameEvent, PING_GRACE_MS, type PhotoAttachment, REACTION_ALIASES, type Reaction, RealtimeHealth, type RealtimeHealthState, type RecoveryEvent, type RecoveryOptions, type RecoveryRuntime, type RecoveryState, RecoverySupervisor, SILENT_WINDOW_MS, type SearchMusicOptions, type SearchMusicResult, type ShareAttachment, type ShareResult, type StickerAttachment, type StickerInfo, type StickerPackInfo, type ThreadID, type ThreadInfo, type ThreadThemeEvent, type TypingIndicator, type UnsendMessageEvent, type UserID, type UserInfo, type VideoAttachment, type WarmUpResult, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, cleanJsonResponse, collectSessionCookies, _default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isLoggedOutDocument, isNonCriticalError, isReactionEmoji, isStaleTokenError, login$1 as login, missingSessionCookies, normalizeBrowserProfile, normalizeReaction, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, retryOnNonCritical, safetySnapshot, sleep, startLiveness, startRealtimeSupervisor, startRecovery, tripBreaker, uploadGroupImage, uploadMercuryAttachment, warmUpSession, withCometTokens, withSessionRetry };
package/dist/index.d.ts CHANGED
@@ -1261,6 +1261,25 @@ declare function uploadMercuryAttachment(ctx: any, defaultFuncs: any, attachment
1261
1261
  */
1262
1262
  declare function uploadGroupImage(ctx: any, defaultFuncs: any, image: any): Promise<any>;
1263
1263
 
1264
+ /**
1265
+ * Shared reaction validation for every surface that sends a reaction.
1266
+ * Facebook accepts any single emoji; a whitelist silently rejects valid picks.
1267
+ */
1268
+ declare const CORE_REACTIONS: string[];
1269
+ declare const REACTION_ALIASES: Record<string, string>;
1270
+ declare function isReactionEmoji(value: unknown): boolean;
1271
+ declare function normalizeReaction(value: unknown, options?: {
1272
+ allowRemove?: boolean;
1273
+ forceCustom?: boolean;
1274
+ }): string;
1275
+
1276
+ /**
1277
+ * Facebook "non-critical" errors (e.g. 1357004) are retryable and must not be
1278
+ * treated as auth failures or checkpoints.
1279
+ */
1280
+ declare function isNonCriticalError(value: any): boolean;
1281
+ declare function retryOnNonCritical<T = any>(operation: () => Promise<T>, refresh: () => void | Promise<void>): Promise<T>;
1282
+
1264
1283
  type ApiContext = ApiContext$1;
1265
1284
  type ApiInternal = Api;
1266
1285
  type DefaultFuncs = DefaultFuncs$1;
@@ -1271,4 +1290,4 @@ declare const _default: typeof login$1 & {
1271
1290
  default: typeof login$1;
1272
1291
  };
1273
1292
 
1274
- export { type API, type AddedStickerPackInfo, type AiTheme, type AnyAttachment, type ApiContext, type ApiInternal, type Attachment, type AudioAttachment, type BrowserProfile, type Callback, type CommentMessage, type CommentResult, type Coordinates, type DefaultFuncs, type EmojiEvent, type Event, type FileAttachment, type GroupNameEvent, type ListenEvent, type LivenessHandle, type LivenessState, type LoginCredentials, type LoginOptions, type Mention, type Message, type MessageID, type MessageObject, type MessageReply, type MusicTag, type MusicTrack, type NicknameEvent, PING_GRACE_MS, type PhotoAttachment, type Reaction, RealtimeHealth, type RealtimeHealthState, type RecoveryEvent, type RecoveryOptions, type RecoveryRuntime, type RecoveryState, RecoverySupervisor, SILENT_WINDOW_MS, type SearchMusicOptions, type SearchMusicResult, type ShareAttachment, type ShareResult, type StickerAttachment, type StickerInfo, type StickerPackInfo, type ThreadID, type ThreadInfo, type ThreadThemeEvent, type TypingIndicator, type UnsendMessageEvent, type UserID, type UserInfo, type VideoAttachment, type WarmUpResult, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, cleanJsonResponse, collectSessionCookies, _default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isLoggedOutDocument, isStaleTokenError, login$1 as login, missingSessionCookies, normalizeBrowserProfile, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, safetySnapshot, sleep, startLiveness, startRealtimeSupervisor, startRecovery, tripBreaker, uploadGroupImage, uploadMercuryAttachment, warmUpSession, withCometTokens, withSessionRetry };
1293
+ export { type API, type AddedStickerPackInfo, type AiTheme, type AnyAttachment, type ApiContext, type ApiInternal, type Attachment, type AudioAttachment, type BrowserProfile, CORE_REACTIONS, type Callback, type CommentMessage, type CommentResult, type Coordinates, type DefaultFuncs, type EmojiEvent, type Event, type FileAttachment, type GroupNameEvent, type ListenEvent, type LivenessHandle, type LivenessState, type LoginCredentials, type LoginOptions, type Mention, type Message, type MessageID, type MessageObject, type MessageReply, type MusicTag, type MusicTrack, type NicknameEvent, PING_GRACE_MS, type PhotoAttachment, REACTION_ALIASES, type Reaction, RealtimeHealth, type RealtimeHealthState, type RecoveryEvent, type RecoveryOptions, type RecoveryRuntime, type RecoveryState, RecoverySupervisor, SILENT_WINDOW_MS, type SearchMusicOptions, type SearchMusicResult, type ShareAttachment, type ShareResult, type StickerAttachment, type StickerInfo, type StickerPackInfo, type ThreadID, type ThreadInfo, type ThreadThemeEvent, type TypingIndicator, type UnsendMessageEvent, type UserID, type UserInfo, type VideoAttachment, type WarmUpResult, acquireCookies, applySetCookie, awaitProactiveSlot, canActProactively, cleanJsonResponse, collectSessionCookies, _default as default, defaultBrowserProfile, ensureCometTokens, extractCookies, extractCookiesFromBody, extractCookiesFromJar, extractCookiesFromSetCookie, extractCurrentUser, getFingerprintParams, getHeaders, getWebSocketHeaders, hasAuthenticatedSession, humanDelay, invalidateCometTokens, isAuthFailure, isBreakerOpen, isLoggedOutDocument, isNonCriticalError, isReactionEmoji, isStaleTokenError, login$1 as login, missingSessionCookies, normalizeBrowserProfile, normalizeReaction, noteAction, noteAuthFailure, noteCheckpoint, noteRateLimit, paceRequest, publicTypes, randomUserAgent, refreshSessionCookies, repairSession, reserveRequest, resetBreaker, resolveProfile, retryDelay, retryOnNonCritical, safetySnapshot, sleep, startLiveness, startRealtimeSupervisor, startRecovery, tripBreaker, uploadGroupImage, uploadMercuryAttachment, warmUpSession, withCometTokens, withSessionRetry };
package/dist/index.mjs CHANGED
@@ -75523,6 +75523,7 @@ __export(utils_exports2, {
75523
75523
  BASE_COOLDOWN_MS: () => BASE_COOLDOWN_MS,
75524
75524
  BRAND: () => BRAND,
75525
75525
  CHROME_MAJORS: () => CHROME_MAJORS,
75526
+ CORE_REACTIONS: () => CORE_REACTIONS,
75526
75527
  ConfigurationError: () => ConfigurationError,
75527
75528
  DESKTOP_PLATFORMS: () => DESKTOP_PLATFORMS,
75528
75529
  FCAError: () => FCAError,
@@ -75534,6 +75535,7 @@ __export(utils_exports2, {
75534
75535
  NotFoundError: () => NotFoundError,
75535
75536
  PING_GRACE_MS: () => PING_GRACE_MS,
75536
75537
  PermissionError: () => PermissionError,
75538
+ REACTION_ALIASES: () => REACTION_ALIASES,
75537
75539
  RateLimitError: () => RateLimitError,
75538
75540
  RealtimeHealth: () => RealtimeHealth,
75539
75541
  RecoverySupervisor: () => RecoverySupervisor,
@@ -75615,6 +75617,8 @@ __export(utils_exports2, {
75615
75617
  isAuthFailure: () => isAuthFailure,
75616
75618
  isBreakerOpen: () => isBreakerOpen,
75617
75619
  isLoggedOutDocument: () => isLoggedOutDocument,
75620
+ isNonCriticalError: () => isNonCriticalError,
75621
+ isReactionEmoji: () => isReactionEmoji,
75618
75622
  isReadableStream: () => isReadableStream2,
75619
75623
  isStaleTokenError: () => isStaleTokenError,
75620
75624
  json: () => json2,
@@ -75625,6 +75629,7 @@ __export(utils_exports2, {
75625
75629
  meta: () => meta,
75626
75630
  missingSessionCookies: () => missingSessionCookies,
75627
75631
  normalizeBrowserProfile: () => normalizeBrowserProfile,
75632
+ normalizeReaction: () => normalizeReaction,
75628
75633
  noteAction: () => noteAction,
75629
75634
  noteAuthFailure: () => noteAuthFailure,
75630
75635
  noteCheckpoint: () => noteCheckpoint,
@@ -75646,6 +75651,7 @@ __export(utils_exports2, {
75646
75651
  resetBreaker: () => resetBreaker,
75647
75652
  resolveProfile: () => resolveProfile,
75648
75653
  retryDelay: () => retryDelay,
75654
+ retryOnNonCritical: () => retryOnNonCritical,
75649
75655
  safetySnapshot: () => safetySnapshot,
75650
75656
  saveCookies: () => saveCookies,
75651
75657
  scheduleSessionKeepAlive: () => scheduleSessionKeepAlive,
@@ -90722,6 +90728,30 @@ function buildWsTaskContent(ctx, tasks, versionId) {
90722
90728
  var import_form_data3 = __toESM(require_form_data2());
90723
90729
  init_constants();
90724
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
90725
90755
  function cleanJsonResponse(value) {
90726
90756
  if (typeof value !== "string") return value;
90727
90757
  const normalized = value.replace(/^for\s*\(;;\);\s*/i, "");
@@ -90814,18 +90844,76 @@ function buildUploadQuery(ctx) {
90814
90844
  return qs2;
90815
90845
  }
90816
90846
  var EXTENSION_BY_MIME = {
90847
+ // video
90817
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
90818
90856
  "audio/mp4": "m4a",
90857
+ "audio/x-m4a": "m4a",
90819
90858
  "audio/mpeg": "mp3",
90820
90859
  "audio/mp3": "mp3",
90821
90860
  "audio/aac": "aac",
90822
90861
  "audio/ogg": "ogg",
90862
+ "audio/opus": "opus",
90823
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
90824
90870
  "image/jpeg": "jpg",
90871
+ "image/jpg": "jpg",
90825
90872
  "image/png": "png",
90826
90873
  "image/gif": "gif",
90827
- "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"
90828
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
+ }
90829
90917
  function sniffMime(buffer) {
90830
90918
  if (buffer.length < 12) return "";
90831
90919
  const b = buffer;
@@ -90850,11 +90938,11 @@ function sniffMime(buffer) {
90850
90938
  }
90851
90939
  function normalizeFilename(filename, mime) {
90852
90940
  const base = filename && String(filename).trim() ? String(filename).trim() : `file-${Date.now()}`;
90853
- const ext = EXTENSION_BY_MIME[mime] || "";
90854
- 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);
90855
90943
  const currentExt = match ? match[0].slice(1).toLowerCase() : "";
90856
90944
  if (ext && currentExt !== ext) {
90857
- 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 === "");
90858
90946
  if (audioOverride || !currentExt) {
90859
90947
  return currentExt ? `${base.slice(0, base.length - currentExt.length - 1)}.${ext}` : `${base}.${ext}`;
90860
90948
  }
@@ -90881,12 +90969,12 @@ async function uploadMercuryAttachment(ctx, defaultFuncs, attachment) {
90881
90969
  data2 = cleanJsonResponse(res && res.body !== void 0 ? res.body : res);
90882
90970
  } catch (error2) {
90883
90971
  lastError = error2;
90884
- if (attempt === 0 && isTransientUploadError(error2)) continue;
90972
+ if (attempt === 0 && isNonCriticalError(error2)) continue;
90885
90973
  throw error2;
90886
90974
  }
90887
90975
  if (extractAttachmentIds(data2).length) return finalizeUpload(data2);
90888
90976
  lastError = buildNoMetadataError(data2);
90889
- if (attempt === 0 && isTransientUploadError(lastError)) continue;
90977
+ if (attempt === 0 && isNonCriticalError(lastError)) continue;
90890
90978
  throw lastError;
90891
90979
  }
90892
90980
  throw lastError || buildNoMetadataError(null);
@@ -90897,14 +90985,6 @@ function buildNoMetadataError(data2) {
90897
90985
  err.body = data2;
90898
90986
  return err;
90899
90987
  }
90900
- function isTransientUploadError(value) {
90901
- const body = value && (value.body || value);
90902
- if (!body || typeof body !== "object") return false;
90903
- const num = Number(body.error ?? body.errorCode);
90904
- if (num === 1357004 || num === 1357001) return true;
90905
- const text3 = String(body.errorDescription || body.errorSummary || value?.message || "");
90906
- return /re-?open(ing)? your browser|try again|temporarily unavailable/i.test(text3);
90907
- }
90908
90988
  async function refreshUploadTokens(ctx, defaultFuncs) {
90909
90989
  ctx.lsd = void 0;
90910
90990
  ctx.master = ctx.master || {};
@@ -91017,6 +91097,68 @@ async function uploadGroupImage(ctx, defaultFuncs, image) {
91017
91097
  return cleanJsonResponse(res && res.body !== void 0 ? res.body : res);
91018
91098
  }
91019
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
+
91020
91162
  // src/utils/lsd.ts
91021
91163
  var TOKEN_TTL_MS = 10 * 60 * 1e3;
91022
91164
  var STALE_TOKEN_ERRORS = /* @__PURE__ */ new Set([
@@ -93613,7 +93755,8 @@ var allowedProperties = {
93613
93755
  emojiSize: true,
93614
93756
  body: true,
93615
93757
  mentions: true,
93616
- location: true
93758
+ location: true,
93759
+ edit: true
93617
93760
  };
93618
93761
  var sendMessage_default = (defaultFuncs, api, ctx) => {
93619
93762
  async function uploadAttachment(attachments) {
@@ -93735,6 +93878,18 @@ var sendMessage_default = (defaultFuncs, api, ctx) => {
93735
93878
  if (msgType === "String") {
93736
93879
  msg = { body: msg };
93737
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
+ }
93738
93893
  const finish = (err, data2) => {
93739
93894
  settle2(err, data2);
93740
93895
  return data2;
@@ -94008,45 +94163,6 @@ function sendTypingIndicator_default(defaultFuncs, api, ctx) {
94008
94163
  }
94009
94164
 
94010
94165
  // src/deltas/apis/messaging/setMessageReaction.ts
94011
- var VALID_REACTIONS = [
94012
- "\u{1F60D}",
94013
- // :heart_eyes:
94014
- "\u{1F606}",
94015
- // :laughing:
94016
- "\u{1F62E}",
94017
- // :open_mouth:
94018
- "\u{1F622}",
94019
- // :cry:
94020
- "\u{1F620}",
94021
- // :angry:
94022
- "\u{1F44D}",
94023
- // :thumbsup:
94024
- "\u{1F44E}",
94025
- // :thumbsdown:
94026
- "\u2764",
94027
- // :heart:
94028
- "\u{1F497}",
94029
- // :glowingheart:
94030
- ""
94031
- // remove
94032
- ];
94033
- var ALIASES = {
94034
- ":heart_eyes:": "\u{1F60D}",
94035
- ":love:": "\u{1F60D}",
94036
- ":laughing:": "\u{1F606}",
94037
- ":haha:": "\u{1F606}",
94038
- ":open_mouth:": "\u{1F62E}",
94039
- ":wow:": "\u{1F62E}",
94040
- ":cry:": "\u{1F622}",
94041
- ":sad:": "\u{1F622}",
94042
- ":angry:": "\u{1F620}",
94043
- ":thumbsup:": "\u{1F44D}",
94044
- ":like:": "\u{1F44D}",
94045
- ":thumbsdown:": "\u{1F44E}",
94046
- ":dislike:": "\u{1F44E}",
94047
- ":heart:": "\u2764",
94048
- ":glowingheart:": "\u{1F497}"
94049
- };
94050
94166
  function setMessageReaction_default(defaultFuncs, _api, ctx) {
94051
94167
  return async function setMessageReaction(reaction, messageID, callback, forceCustomReaction) {
94052
94168
  const cb = typeof callback === "function" ? callback : void 0;
@@ -94063,11 +94179,14 @@ function setMessageReaction_default(defaultFuncs, _api, ctx) {
94063
94179
  if (!messageID) {
94064
94180
  throw { error: "setMessageReaction: messageID is required." };
94065
94181
  }
94066
- let value = reaction == null ? "" : String(reaction);
94067
- if (ALIASES[value] !== void 0) {
94068
- value = ALIASES[value];
94069
- } else if (value !== "" && VALID_REACTIONS.indexOf(value) === -1 && !forceCustomReaction) {
94070
- 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." };
94071
94190
  }
94072
94191
  const variables = {
94073
94192
  data: {
@@ -94859,33 +94978,35 @@ function searchMusic_default(defaultFuncs, _api, ctx) {
94859
94978
  }
94860
94979
  const page = collectTracks(parsed);
94861
94980
  const tracks = page.tracks.slice(0, count);
94862
- 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++) {
94863
94990
  invalidateCometTokens(ctx);
94991
+ await sleep(600 + retry * 900);
94864
94992
  const retried = await attempt(true);
94865
- if (!isStaleTokenError(retried) && !retried.error && !(retried.errors || []).length) {
94866
- const retryPage = collectTracks(retried);
94867
- const retryTracks = retryPage.tracks.slice(0, count);
94868
- if (retryTracks.length) {
94869
- const result2 = retryTracks.slice();
94870
- result2.tracks = retryTracks;
94871
- result2.endCursor = retryPage.endCursor;
94872
- result2.hasNextPage = retryPage.hasNextPage;
94873
- result2.query = text3;
94874
- return result2;
94875
- }
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;
94876
95003
  }
94877
- throw {
94878
- error: `No music results found for "${text3}". The Facebook music catalog may be unavailable for this account or region.`,
94879
- code: "NO_RESULTS",
94880
- query: text3
94881
- };
94882
95004
  }
94883
- const result = tracks.slice();
94884
- result.tracks = tracks;
94885
- result.endCursor = page.endCursor;
94886
- result.hasNextPage = page.hasNextPage;
94887
- result.query = text3;
94888
- 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
+ };
94889
95010
  }
94890
95011
  return function searchMusic(query = "", options = {}, callback) {
94891
95012
  const selectedOptions = typeof options === "function" ? {} : options || {};
@@ -99042,15 +99163,22 @@ function story_default(defaultFuncs, api, ctx) {
99042
99163
  }
99043
99164
  async function sendStoryReply(storyIdOrUrl, message, isReaction) {
99044
99165
  try {
99045
- const allowedReactions = ["\u2764\uFE0F", "\u{1F44D}", "\u{1F917}", "\u{1F606}", "\u{1F621}", "\u{1F622}", "\u{1F62E}"];
99046
99166
  if (!storyIdOrUrl) throw new Error("Story ID or URL is required.");
99047
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
+ }
99048
99176
  let storyID = getStoryIDFromURL(storyIdOrUrl);
99049
99177
  if (!storyID) storyID = storyIdOrUrl;
99050
99178
  const variables = {
99051
99179
  input: {
99052
99180
  attribution_id_v2: "StoriesCometSuspenseRoot.react,comet.stories.viewer,via_cold_start",
99053
- message,
99181
+ message: reactionValue,
99054
99182
  story_id: storyID,
99055
99183
  story_reply_type: isReaction ? "LIGHT_WEIGHT" : "TEXT",
99056
99184
  actor_id: ctx.userID,
@@ -99058,12 +99186,9 @@ function story_default(defaultFuncs, api, ctx) {
99058
99186
  }
99059
99187
  };
99060
99188
  if (isReaction) {
99061
- if (!allowedReactions.includes(message)) {
99062
- throw new Error(`Invalid reaction. Please use one of: ${allowedReactions.join(" ")}`);
99063
- }
99064
99189
  variables.input.lightweight_reaction_actions = {
99065
99190
  offsets: [0],
99066
- reaction: message
99191
+ reaction: reactionValue
99067
99192
  };
99068
99193
  }
99069
99194
  const form = {
@@ -99472,14 +99597,32 @@ function getThreadInfo_default(defaultFuncs, api, ctx) {
99472
99597
  throw new Error("getThreadInfo: malformed response from GraphQL.");
99473
99598
  }
99474
99599
  const threadInfos = {};
99475
- 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++) {
99476
99602
  const res = resData[i2];
99477
- if (!res || res.error_results || !res[Object.keys(res)[0]]?.data) continue;
99478
- 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);
99479
99618
  if (threadInfo) {
99480
- threadInfos[threadInfo.threadID || threadIDs[threadIDs.length - 1 - i2]] = threadInfo;
99619
+ threadInfos[threadInfo.threadID || threadIDs[index3]] = threadInfo;
99620
+ matched++;
99481
99621
  }
99482
99622
  }
99623
+ if (matched === 0) {
99624
+ throw new Error("getThreadInfo: malformed response from GraphQL.");
99625
+ }
99483
99626
  return Array.isArray(threadID) ? threadInfos : Object.values(threadInfos)[0] || null;
99484
99627
  } catch (err) {
99485
99628
  error("getThreadInfo", err);
@@ -100018,7 +100161,10 @@ var getUserInfo_default = (defaultFuncs, api, ctx) => {
100018
100161
  form[`ids[${i2}]`] = v;
100019
100162
  });
100020
100163
  const getGenderString = (code) => code === 1 ? "male" : code === 2 ? "female" : "no specific gender";
100021
- 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) => {
100022
100168
  if (resData?.error && resData?.error !== 3252001) throw resData;
100023
100169
  const retObj = {};
100024
100170
  const profiles = resData?.payload?.profiles;
@@ -101529,6 +101675,7 @@ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc,
101529
101675
  }
101530
101676
  if (listenMqtt_default) {
101531
101677
  api["listenMqtt"] = listenMqtt_default(defaultFuncs, api, ctx);
101678
+ api["listen"] = api["listenMqtt"];
101532
101679
  }
101533
101680
  };
101534
101681
  api.getCurrentUserID = () => ctx.userID;
@@ -101853,4 +102000,4 @@ lodash/lodash.js:
101853
102000
  *)
101854
102001
  */
101855
102002
 
101856
- export { PING_GRACE_MS, 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, 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, uploadGroupImage, uploadMercuryAttachment, 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.1",
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 && node test/attachment-resilience.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
  },