npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@eryxenx/fca

v1.1.7

Published

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

Readme

💬 @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

FeaturesInstallationQuick StartE2EEsessionGuardsendBroadcastAPI 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:

| Step | What to do | |---|---| | 1. sessionGuard | Add api.sessionGuard(accountPath, { interval, debounce }) once, right after login succeeds, if it's not already there. | | 2. E2EE | Nothing 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 commandsconfig.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

| Option | Type | Default | Description | |--------|------|---------|-------------| | selfListen | boolean | false | Receive your own sent messages | | listenEvents | boolean | true | Receive thread/group events | | listenTyping | boolean | false | Receive typing indicator events | | updatePresence | boolean | false | Receive online/offline presence events | | autoMarkDelivery | boolean | false | Auto-mark incoming messages as delivered | | autoMarkRead | boolean | false | Auto-mark threads as read | | autoReconnect | boolean | true | Auto-reconnect MQTT on disconnect | | online | boolean | false | Appear as online to others | | emitReady | boolean | false | Emit ready event when MQTT connected | | proxy | string | — | HTTP proxy URL | | userAgent | string | Safari UA | Override 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