@eryxenx/fca

中文文档
1.3.0发布于 2026/11/14MITTypeScriptCommonJS近 30 天下载 6245

Facebook Chat API by EryXenX | Stable • Auto Re-login • Full E2EE Support — send messages, media, reactions & more in encrypted chats, hassle-free

原始使用文档
文档目录33 节
  1. ⚡ Why @eryxenx/fca?
  2. ✨ Features
  3. 📦 Installation
  4. 🚀 Quick Start
  5. Classic (GoatBot compatible)
  6. With E2EE (regular + encrypted threads)
  7. GoatBot login.js
  8. 🔐 E2EE — Encrypted Conversations
  9. Setup
  10. Listen (regular + E2EE combined)
  11. E2EE Methods
  12. Native media engine
  13. Receiving E2EE media (attachments & replies)
  14. 🛡️ Command Anti-Spam (Humanizer)
  15. What counts as a "command"
  16. Typing indicator
  17. Inspecting it
  18. 🛡️ sessionGuard
  19. 📡 sendBroadcast
  20. 🤖 MessengerBot
  21. 🎯 createFcaClient
  22. 📖 API Reference
  23. Sending Messages
  24. Message Actions
  25. Read Receipts & Typing
  26. Thread Management
  27. Thread Customization
  28. User Info
  29. Social
  30. Account & Config
  31. HTTP Utilities
  32. 📋 Login Options
  33. 📄 License

💬 @eryxenx/fca

Unofficial Facebook Messenger Bot API for Node.js NEXCA MQTT · Signal Protocol E2EE (mautrix-go powered) · sessionGuard · 90+ API Methods · Zero TypeScript

npm license node

Features • Installation • Quick Start • E2EE • sessionGuard • sendBroadcast • API Reference


⚡ Why @eryxenx/fca?

  • ✅ NEXCA MQTT — stable connection core, autoReconnect, jitter
  • ✅ Signal Protocol E2EE — Facebook real encrypted conversations support. Reliable media/reaction/unsend delivery is powered by a bundled mautrix-go / mautrix-meta native engine (© Tulir Asokan, MPL-2.0) — NEXCA's own hand-written E2EE encoder produced protocol-valid messages that Facebook accepted but Messenger clients failed to render for attachments/reactions/unsend, so this fork routes those through the native engine instead. Text messaging still uses the original engine (already reliable). The native engine is opt-in (FCA_E2EE_NATIVE=1) — see Native media engine.
  • ✅ sessionGuard — appstate corruption and silent logout protection, auto-backup
  • ✅ sendBroadcast — rate-limited multi-thread broadcast
  • ✅ Fixed MQTT subscribe race condition — no more "Connection refused: No subscription existed"
  • ✅ isActiveClient() guard — stale MQTT client events no longer processed
  • ✅ connectTimeout extended — no premature logout on slow networks
  • ✅ autoReconnect — auto-reconnect on connection drop
  • ✅ GoatBot compatible — all API signatures unchanged (threadID optional etc.)
  • ✅ 90+ API methods — sendMessage, editMessage, setMessageReaction, getThreadInfo and more

✨ Features

  • ✅ Full Messenger API — messages, reactions, attachments, stickers, polls, pins
  • ✅ NEXCA MQTT — stable connection, autoReconnect, jitter, isActiveClient guard
  • ✅ E2EE — Signal Protocol encrypted threads, auto-connects after login (listenE2EE, e2ee.*)
  • ✅ E2EE media engine — native mautrix-go engine (opt-in via FCA_E2EE_NATIVE=1) for reliable image/video/audio/document sending, reactions, and unsend in encrypted threads (falls back to the built-in JS engine automatically)
  • ✅ E2EE incoming media — attachments on encrypted messages (including replies) are auto-decrypted and served locally, so existing commands work unmodified
  • ✅ sessionGuard — appstate auto-save, corruption guard, .bak backup
  • ✅ Command anti-spam humanizer — human-like reply delay, per-thread cooldown, GoatBot no-prefix/onChat/onReply auto-detection
  • ✅ sendBroadcast — parallel/sequential multi-thread sending with rate limit
  • ✅ MessengerBot — Discord.js/Telegraf style (.command, .hears, .launch)
  • ✅ createFcaClient — namespaced facade (client.messages, client.threads etc.)
  • ✅ GoatBot / Mirai compatible — drop-in replacement

📦 Installation

npm install @eryxenx/fca

Node.js >= 20 required.

The E2EE media engine ships a precompiled native binary (src/api/socket/e2ee/native/build/) for Linux (.so) and Windows (.dll), loaded via the koffi FFI. npm install pulls in koffi and yumi-json-bigint automatically — no extra setup needed. Note the native engine only runs when FCA_E2EE_NATIVE=1 is set (see Native media engine); on platforms other than Linux/Windows x64 (e.g. macOS, ARM) the binary fails to load and E2EE media/reactions/unsend automatically fall back to the built-in JS engine (text messaging is unaffected either way).

The E2EE socket's User-Agent defaults to desktop Windows Chrome 139 to match the rest of the client; override with FCA_E2EE_UA if you run with a different UA.


🚀 Quick Start

Classic (GoatBot compatible)

const login = require("@eryxenx/fca");

login({ appState: require("./account.json") }, { listenEvents: true }, (err, api) => {
  if (err) throw err;

  api.sessionGuard("./account.json", {
    interval: 3 * 60 * 1000,
    debounce: 30 * 1000
  });

  api.listenMqtt((err, event) => {
    if (err) throw err;
    if (event.type === "message") api.sendMessage(event.body, event.threadID);
  });
});

With E2EE (regular + encrypted threads)

connectE2EE() runs automatically right after login — you never call it yourself. Calling it again in your own code opens a second, racing connection on the same bridge. Just check api.e2ee.isConnected() if you need to know the status.

login({ appState: require("./account.json") }, { listenEvents: true }, (err, api) => {
  if (err) throw err;

  api.sessionGuard("./account.json");
  // E2EE is already connecting in the background here — do not call connectE2EE().

  api.listenE2EE((err, event) => {
    if (err) throw err;
    if (event.type === "message") {
      if (event.isE2EE) {
        api.e2ee.sendMessage(event.threadID, "Got your encrypted message!");
      } else {
        api.sendMessage("Got it!", event.threadID);
      }
    }
  });
});

GoatBot login.js

const login = require("@eryxenx/fca");

login({ appState }, options, (err, api) => {
  if (err) return;

  api.sessionGuard(path.join(process.cwd(), "account.txt"), {
    interval: 3 * 60 * 1000,
    debounce: 30 * 1000
  });

  // Nothing else needed for E2EE — it auto-connects inside @eryxenx/fca
  // right after login. Do NOT call api.connectE2EE() here.

  api.listenMqtt(callback);
});

This is the only setup needed — nothing else in login.js (or anywhere in GoatBot) has to change for E2EE. Once login succeeds, api.e2ee is already connecting in the background, and every existing GoatBot call — api.sendMessage(...) (with or without attachment), api.setMessageReaction(...), api.unsendMessage(...) — automatically detects when a thread is an encrypted DM and routes through the E2EE engine internally. Commands don't need to know or care whether a thread is encrypted; they call the same functions either way.

Adding this to your own login.js

Your bot's actual login.js will look different from the snippet above (dashboard setup, database sync, custom logging, etc.) — you're not replacing the file, just adding/checking two things inside your existing login callback:

StepWhat to do
1. sessionGuardAdd api.sessionGuard(accountPath, { interval, debounce }) once, right after login succeeds, if it's not already there.
2. E2EENothing to add — it's automatic. If your login.js already has an explicit await api.connectE2EE() call, delete it — it's redundant with the automatic connection and the two can race.

To check whether E2EE is already wired into your bot, search for e2ee.isConnected in your login.js — if it's there, setup is already done.


🔐 E2EE — Encrypted Conversations

Uses Facebook's real Signal Protocol infrastructure — same as the official Messenger app.

Setup

E2EE connects automatically after login (creating .nexca/e2ee_device.json on first run) — there's nothing to call manually. Just check status once connected:

console.log(api.e2ee.isConnected()); // true (may need a moment right after login)

Listen (regular + E2EE combined)

api.listenE2EE((err, event) => {
  if (event.type === "message") {
    if (event.isE2EE) {
      api.e2ee.sendMessage(event.threadID, "Encrypted reply!");
    } else {
      api.sendMessage("Normal reply!", event.threadID);
    }
  }
});

E2EE Methods

await api.e2ee.sendMessage(threadID, "Hello!");
await api.e2ee.sendMessage(threadID, { body: "Photo!", attachment: fs.createReadStream("photo.jpg") });
await api.e2ee.sendReaction(threadID, messageID, "❤️");
await api.e2ee.sendTyping(threadID, true);
await api.e2ee.unsendMessage(messageID, threadID);
await api.e2ee.editMessage(threadID, messageID, "Updated!");
api.e2ee.isConnected();
await api.e2ee.disconnect();

Native media engine

Sending image/video/audio/document attachments, reactions, and unsend in E2EE threads can be routed through a precompiled mautrix-go / mautrix-meta binary (src/api/socket/e2ee/native/) via the koffi FFI, instead of this project's own hand-written Signal Protocol encoder. This project's own encoder builds protocol-valid messages that Facebook's servers accept, but real Messenger clients silently fail to render — the native engine avoids that entirely.

⚠️ The native engine is DISABLED by default. Both the vendored JS engine and the native engine register with Facebook as the same E2EE device (same device id), and Facebook's server keeps only one live E2EE socket per device — so when both run at once they kick each other off in a loop (a fresh handshake every few seconds, which looks like automated behavior to Facebook). The vendored JS engine is therefore given exclusive use of the socket unless you opt in:

  • Set FCA_E2EE_NATIVE=1 in the environment to enable the native engine. With it on you get E2EE group text receive/send, native media sends, native reactions/unsend/edit/markRead — but you also accept the device-kick socket churn described above (the JS engine reconnects with exponential backoff, so it stays usable, just noisy).

  • With it off (default): E2EE DM text + DM media work through the vendored engine. E2EE group send/receive, and E2EE markRead, are unavailable; reactions/unsend/edit in E2EE DMs fall back to the vendored engine.

  • Text messages always use @eryxenx/fca's own E2EE engine (already reliable, no native dependency).

  • If the native engine fails to load or a send fails (unsupported platform, binary mismatch, etc.), @eryxenx/fca automatically falls back to its own engine and logs [native-media] ... falling back to legacy vendor engine.

  • See src/api/socket/e2ee/native/NOTICE.md for the required MPL-2.0 attribution to the upstream mautrix-go/mautrix-meta project (© Tulir Asokan and contributors) — do not remove it if you redistribute this fork.

Receiving E2EE media (attachments & replies)

Incoming encrypted image/video/audio/document messages are automatically decrypted and served over a short-lived local URL (http://127.0.0.1:<port>/<token>, 15 min TTL), and populated into event.attachments / event.messageReply.attachments — same shape as normal (non-E2EE) attachments. Reply-based commands (e.g. "reply to an image with /imgur") work the same in E2EE threads as in normal ones, no command code changes needed.

if (event.messageReply && event.messageReply.attachments.length) {
  const url = event.messageReply.attachments[0].url; // works for E2EE too
}

If media resolution fails, check the console for [media-resolve] / [media-decode] errors — this usually means the CDN download host (FB_E2EE_MEDIA_DOWNLOAD_HOST env var, default rupload.facebook.com) needs adjusting for your account/region.


🛡️ Command Anti-Spam (Humanizer)

Delays and rate-limits command replies so the bot reacts like a person typing, not a script firing instantly — and drops commands sent faster than a human could plausibly send them (spam bursts).

Installed automatically on login (enabled by default). No setup needed:

login({ appState }, {}, (err, api) => {
  api.listenMqtt((err, event) => { /* ... */ });
});

Configure it via the humanize login option, or api.setOptions({ humanize }):

login({ appState }, {
  humanize: {
    enabled: true,
    reactDelayMs: [2000, 3000], // random delay before a command is let through
    cooldownMs: 5500,           // min gap between two accepted commands (per thread)
    resetMs: 10000,             // idle gap before cooldown state resets
    maxPerMinute: 20,           // global accepted-commands cap
    commands: ["/", "!", "."],  // prefixes that count as "this is a command"
    perThread: true             // cooldown tracked per thread vs. globally
  }
}, cb);

api.setOptions({ humanize: { enabled: false } }) turns it off entirely.

What counts as a "command"

By default, only messages starting with one of the commands prefixes are treated as commands — everything else (plain chat) passes through untouched, with no delay and no typing indicator.

If global.GoatBot is present (i.e. running under GoatBot-Pro), the humanizer automatically also treats these as commands, with zero config needed:

  • No-prefix admin commands — config.noPrefix.enable: true + sender in config.adminBot, first word matches a real command/alias.
  • onChat triggers — a message whose full text exactly matches a command name registered in GoatBot.onChat (e.g. typing prefix with no /, which GoatBot's prefix.js reacts to).
  • onReply flows — a reply to a message tracked in GoatBot.onReply (e.g. the numeric "1", "2", "3" replies used by GoatBot's setting.js menu system).

This means the whole multi-step setting command flow — not just the initial /setting — gets the same delay/cooldown/typing treatment as a normal command.

For anything outside these built-ins, give it a custom matcher:

const h = api.getHumanizer();
h.setMatcher((event) => {
  // your own logic — return true to treat this event as a command
  return event.body === "some no-prefix trigger";
});

matcher can also be passed directly in the humanize config (humanize: { matcher: fn }). A message counts as a command if it matches the prefix list or a built-in GoatBot check or the matcher.

Typing indicator

When a command is accepted, api.sendTypingIndicator(threadID, true) is called immediately, held for the exact accepted delay, then turned off right before the event is forwarded — so the typing indicator and the reply delay are the same window, not two stacked waits. Disabled automatically if global.GoatBot.config.enableTypingIndicator is false.

Inspecting it

const h = api.getHumanizer();
h.stats();     // { enabled, global: {accepted, dropped, ...}, threads: [...] }
h.reset();     // clear all cooldown state

Note: this operates on incoming command volume — separate from outgoing send pacing (see sendBroadcast below, and any queueing you add yourself around api.sendMessage).


🛡️ sessionGuard

Protects your appstate from corruption and silent logouts.

api.sessionGuard("./account.json");

// Custom timing
api.sessionGuard("./account.json", {
  interval: 3 * 60 * 1000,
  debounce: 30 * 1000
});

What it does:

  • Auto-saves appstate every N minutes
  • Saves after every successful sendMessage (debounced)
  • Corruption guard — never overwrites a larger appstate with a smaller one
  • Auto-backup — writes .bak before every overwrite
api.saveSession();           // force save now
api.restoreSessionBackup();  // restore from .bak
api.stopSessionGuard();      // stop the timer

📡 sendBroadcast

Rate-limited multi-thread broadcast.

const result = await api.sendBroadcast(
  "Hello everyone!",
  ["THREAD_1", "THREAD_2", "THREAD_3"],
  {
    delay: 2000,
    parallel: 2,
    onEach: (err, info, id) => {
      console.log(err ? "Failed: " + id : "Sent: " + id);
    }
  }
);
console.log(result.sent.length + "/" + result.total + " delivered");

🤖 MessengerBot

Discord.js/Telegraf style high-level bot class.

const { createMessengerBot } = require("@eryxenx/fca");

const bot = await createMessengerBot(
  { appState: require("./account.json") },
  { commandPrefix: "/", stopOnSignals: true }
);

bot.command("ping", async ctx => await ctx.replyAsync("pong 🏓"));
bot.hears(/hello/i, async ctx => await ctx.replyAsync("Hi! 👋"));
bot.on("messageCreate", event => console.log(event.body));

await bot.launch({ stopOnSignals: true });

🎯 createFcaClient

Namespaced facade grouping all API methods by domain.

const { createFcaClient } = require("@eryxenx/fca");
const client = createFcaClient(api);

await client.messages.send("Hello!", threadID);
await client.messages.react("❤️", messageID, threadID);
await client.threads.getInfo(threadID);
await client.users.getInfo(userID);
await client.account.refreshDtsg();

📖 API Reference

Sending Messages

api.sendMessage("Hello!", threadID);
api.sendMessage({ body: "Photo!", attachment: fs.createReadStream("photo.jpg") }, threadID);
api.sendMessage({ body: "Hey @John", mentions: [{ id: "uid", tag: "@John", fromIndex: 4 }] }, threadID);
api.sendMessage({ sticker: "369239263222822" }, threadID);
api.sendMessage({ location: { latitude: 23.8, longitude: 90.4, current: true } }, threadID);
api.sendBroadcast("msg", ["tid1", "tid2"], { delay: 2000 });
api.sendGif("https://media.giphy.com/xyz.gif", threadID);
api.sendLocation(23.8, 90.4, threadID);
api.sendImage("./photo.jpg", threadID, "caption");
api.sendVideo("./video.mp4", threadID);
api.sendAudio("./voice.ogg", threadID);
api.sendFile("./doc.pdf", threadID);
api.shareLink("https://github.com", threadID, "Check this!");
api.shareContact("Meet my friend!", userID, threadID);

Message Actions

api.editMessage("Updated text", messageID);
api.unsendMessage(messageID);
api.deleteMessage([messageID]);
api.setMessageReaction("😍", messageID, threadID);  // threadID optional
api.setMessageReaction("", messageID);               // remove reaction
api.getMessage(threadID, messageID);
api.forwardAttachment(attachmentID, [userID]);
api.uploadAttachment([fs.createReadStream("photo.jpg")]);

Read Receipts & Typing

api.markAsRead(threadID);
api.markAsReadAll();
api.markAsDelivered(threadID, messageID);
api.markAsSeen();
api.sendTypingIndicator(threadID, true);

Thread Management

api.getThreadInfo(threadID);
api.getThreadList(10, null, ["INBOX"]);
api.getThreadHistory(threadID, 20);
api.createGroup("Hey!", ["uid1", "uid2"]);
api.deleteThread(threadID);
api.muteThread(threadID, 3600);
api.changeArchivedStatus(threadID, true);
api.handleMessageRequest(threadID, true);
api.searchForThread("query");

Thread Customization

api.setTitle("New Name", threadID);
api.changeThreadColor("#0084FF", threadID);
api.changeThreadEmoji("🔥", threadID);
api.changeNickname("The Boss", threadID, userID);
api.changeGroupImage(fs.createReadStream("group.jpg"), threadID);
api.changeAdminStatus(threadID, userID, true);
api.addUserToGroup(userID, threadID);
api.removeUserFromGroup(userID, threadID);
api.createPoll("Question?", threadID, { "Yes": false, "No": false });
api.pinMessage(messageID, threadID);
api.unpinMessage(messageID, threadID);

User Info

api.getUserInfo(userID);
api.getUserID("John Doe", callback);
api.getUID("https://facebook.com/zuck");
api.getFriendsList();
api.getAvatarUser(userID);
api.getProfileInfo(userID);
api.getPublicData(userID);
api.sendFriendRequest(userID);
api.handleFriendRequest(userID, true);
api.changeBlockedStatus(userID, true);
api.followUser(userID);
api.unfollowUser(userID);
api.unfriend(userID);

Social

api.reactToPost(postID, "love");
api.reactToComment(commentID, "haha");
api.postComment(postID, "Great post!");
api.sharePost(postID, "Check this!");

Account & Config

api.getCurrentUserID();
api.getAppState();
api.setOptions({ listenTyping: true });
api.logout();
api.refreshFb_dtsg();
api.addExternalModule("myFunc", (defaultFuncs, api, ctx) => {
  return function(text, threadID) {
    return api.sendMessage("[BOT] " + text, threadID);
  };
});

HTTP Utilities

api.httpGet(url, params, callback);
api.httpPost(url, form, callback);
api.httpPostFormData(url, form, callback);
api.uploadImageToImgbb(imageUrl);

📋 Login Options

OptionTypeDefaultDescription
selfListenbooleanfalseReceive your own sent messages
listenEventsbooleantrueReceive thread/group events
listenTypingbooleanfalseReceive typing indicator events
updatePresencebooleanfalseReceive online/offline presence events
autoMarkDeliverybooleanfalseAuto-mark incoming messages as delivered
autoMarkReadbooleanfalseAuto-mark threads as read
autoReconnectbooleantrueAuto-reconnect MQTT on disconnect
onlinebooleanfalseAppear as online to others
emitReadybooleanfalseEmit ready event when MQTT connected
proxystring—HTTP proxy URL
userAgentstringSafari UAOverride HTTP User-Agent

📄 License

MIT License

@eryxenx/fca by EryXenX (Mohammad Akash)

The E2EE native media engine (src/api/socket/e2ee/native/) bundles a precompiled binary built from mautrix-go / mautrix-meta, © Tulir Asokan and contributors, licensed under MPL-2.0 (not MIT). See src/api/socket/e2ee/native/NOTICE.md for full attribution — this notice must be preserved in any redistribution of this fork. NEXCA MQTT core by Deku — MIT License

Unauthorized copying or redistribution without credit is prohibited.


Made with ❤️ by EryXenX

版本与发布记录

@eryxenx/fca 共发布 23 个版本,首次发布于 2026/08/21;过去 12 个月发布了 23 个版本。

版本发布日期距今
1.3.0latest2026/11/14今天
1.2.02026/10/2321 天前
1.1.102026/10/2321 天前
1.1.92026/09/151 个月前
1.1.82026/09/102 个月前
1.1.72026/08/312 个月前

关于 @eryxenx/fca 的常见问题

@eryxenx/fca 是什么?

Facebook Chat API by EryXenX | Stable • Auto Re-login • Full E2EE Support — send messages, media, reactions & more in encrypted chats, hassle-free

如何安装 @eryxenx/fca?

在项目目录运行 npm install @eryxenx/fca,或使用 pnpm add / yarn add / bun add。

@eryxenx/fca 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@eryxenx/fca 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@eryxenx/fca 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=12.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@eryxenx/fca 可以商用吗?

@eryxenx/fca 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@eryxenx/fca 的下载量有多少?

根据 npm 官方下载统计,@eryxenx/fca 在 2026/10/14 – 2026/11/12 这 30 天内被下载 6,245 次,日均约 208 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@eryxenx/fca 还在维护吗?

最新版本 1.3.0 发布于 2026/11/14(今天);至今共发布 23 个版本,其中过去 12 个月发布了 23 个;首次发布于 2026/08/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。