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

@dekuzxc/nexca

v1.4.7

Published

NEXCA - Next Extended Chat API. Feature-complete Facebook Messenger bot API with E2EE (Signal Protocol), MQTT + HTTP fallback, 90+ API methods, and colorful logs.

Readme

npm version license node E2EE transport methods

NEXCA is a feature-complete Facebook Messenger bot library for Node.js.
MQTT-first transport · Signal Protocol E2EE · 90+ API methods · Zero TypeScript


Table of Contents


Installation

npm install @dekuzxc/nexca

Node.js ≥ 18 required. No TypeScript, no build step.


Quick Start

const login = require("@dekuzxc/nexca");
const fs    = require("fs");

const appState = JSON.parse(fs.readFileSync("./appstate.json", "utf8"));

login({ appState }, { listenEvents: true }, async (err, api) => {
    if (err) throw err;

    console.log("Logged in as:", api.getCurrentUserID());
    // nexcaConfig.json is auto-created here ↑

    api.listen((err, event) => {
        if (err) throw err;
        if (event.type === "message" && event.body === "!ping") {
            api.sendMessage("Pong! 🏓", event.threadID);
        }
    });
});

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 after delivery | | autoReconnect | boolean | true | Auto-reconnect MQTT on disconnect | | online | boolean | false | Appear as online to others | | emitReady | boolean | false | Emit a ready event when MQTT is connected | | pageID | string | — | Act as a Facebook Page | | userAgent | string | Safari UA | Override the HTTP User-Agent | | proxy | string | — | HTTP proxy URL (http://127.0.0.1:8080) |


nexcaConfig.json

NEXCA automatically creates nexcaConfig.json in your project directory every time login() succeeds. No setup needed — it just appears.

{
  "botUID":    "61589208980818",
  "botName":  "My Bot Account",
  "region":   "EAG",
  "version":  "1.1.0",
  "lastLogin": "2026-05-21T12:00:00.000Z",
  "logs":      false
}

Set "logs": true to enable colorful per-message console logs showing the sender UID, message body, DM/group indicator, and timestamp. Toggle it live without restarting — the bot re-reads the file on every message.

Read it from your bot code at any time:

const config = require("./nexcaConfig.json");

console.log(config.botName, config.region);
api.sendMessage("Running as: " + config.botName, threadID);

Add your own fields — NEXCA preserves custom keys on every login update:

const fs = require("fs");
const config = JSON.parse(fs.readFileSync("./nexcaConfig.json", "utf8"));

// Add your own fields
config.prefix  = "!";
config.adminID = "100000000000001";

fs.writeFileSync("./nexcaConfig.json", JSON.stringify(config, null, 2));

Event Types

All events arrive in the listen / listenE2EE callback as callback(err, event).

| event.type | When it fires | |---|---| | message | New text or media message | | message_reply | A reply to an existing message — includes event.messageReply | | message_reaction | Reaction added or removed | | message_unsend | A message was unsent by sender | | event | Thread event (rename, add/remove member, etc.) | | typ | Typing indicator (listenTyping: true) | | presence | User online/offline status (updatePresence: true) | | change_thread_image | Group image changed | | ready | MQTT ready (emitReady: true) |

Message Object

{
    type:         "message",
    senderID:     "100000000000001",
    threadID:     "100000000000002",
    messageID:    "mid.$abc...",
    body:         "Hello world",
    args:         ["Hello", "world"],
    attachments:  [],
    mentions:     {},              // { "uid": "@Name" }
    timestamp:    1716000000000,
    isGroup:      false,
    isE2EE:       false            // true when from listenE2EE
}

Reply Object (message_reply)

When event.type === "message_reply", the event carries a messageReply field describing the original message being replied to:

{
    type:         "message_reply",
    senderID:     "100000000000001",   // who sent the reply
    threadID:     "100000000000002",
    messageID:    "mid.$reply...",
    body:         "This is the reply text",
    args:         ["This", "is", ...],
    attachments:  [],
    mentions:     {},
    timestamp:    1716000000001,
    isGroup:      true,
    isE2EE:       false,

    // ── The original message being replied to ──
    messageReply: {
        messageID: "mid.$original...",
        senderID:  "100000000000003",  // who sent the original
        threadID:  "100000000000002",
        body:      "Original message text",
        args:      ["Original", "message", "text"],
        attachments: [],
        mentions:  {},
        isGroup:   true,
        timestamp: 1716000000000
    }
}

Reply handler pattern (same as stfca):

api.listen(async (err, event) => {
    if (event.type === "message_reply" && event.messageReply) {
        const repliedMsgID = event.messageReply.messageID;

        if (replyHandlers.has(repliedMsgID)) {
            const handler = replyHandlers.get(repliedMsgID);
            replyHandlers.delete(repliedMsgID);
            await handler(event);
        }
    }
});

Works identically in both api.listen (MQTT) and api.listenE2EE (encrypted). E2EE reply events carry isE2EE: true on both the event and the messageReply object.

Thread Events (event type)

When event.type === "event", the logMessageType field identifies what happened. These values match the stfca convention:

| event.logMessageType | Description | |---|---| | log:subscribe | Members added to a group — logMessageData.addedParticipants | | log:unsubscribe | Member left or was removed — logMessageData.leftParticipantFbId | | log:thread-name | Group name changed — logMessageData.name | | log:thread-color | Chat theme/color changed | | log:thread-icon | Thread emoji changed | | log:user-nickname | Nickname changed | | log:thread-admins | Admin status changed | | log:thread-poll | Poll created or updated | | log:thread-pinned | Message pinned/unpinned | | log:thread-call | Call log entry | | log:thread-approval-mode | Join approval mode changed | | log:link-status | Joinable link reset |

Join/leave handler example:

api.listen((err, event) => {
    if (event.type !== "event") return;

    if (event.logMessageType === "log:subscribe") {
        const added = (event.logMessageData.addedParticipants || [])
            .map(p => p.userFbId || p);
        api.sendMessage(`Welcome to the group!`, event.threadID);
    }

    if (event.logMessageType === "log:unsubscribe") {
        const who = event.logMessageData.leftParticipantFbId;
        api.sendMessage(`Goodbye ${who}!`, event.threadID);
    }
});

Attachment Shapes

{ type: "photo",   ID, url, width, height, filename }
{ type: "video",   ID, url, duration, width, height }
{ type: "audio",   ID, url, duration, filename }
{ type: "file",    ID, url, size, mimeType, filename }
{ type: "sticker", stickerID, url, width, height }

API Reference


Login

login(loginData, [options], [callback])Promise<api>

// appState (recommended)
const api = await login({ appState: require("./appstate.json") });

// Email + password
login({ email: "[email protected]", password: "secret" }, callback);

// With options
login({ appState }, { listenEvents: true, autoReconnect: true }, callback);

Listening

api.listen(callback)MessageEmitter

Start listening for all events via MQTT.

const emitter = api.listen((err, event) => {
    if (err) return console.error(err);

    if (event.type === "message")
        console.log(event.senderID, "→", event.body);

    if (event.type === "message_reaction")
        console.log(event.senderID, "reacted", event.reaction);

    if (event.type === "typ")
        console.log(event.from, event.isTyping ? "typing..." : "stopped");
});

// Stop listening
await emitter.stopListeningAsync();

api.listenE2EE(callback)MessageEmitter

Combined listener: receives both regular MQTT messages and decrypted E2EE messages in the same callback. Call api.connectE2EE() first.

await api.connectE2EE(); // auto-creates .nexca/e2ee_device.json

api.listenE2EE((err, event) => {
    if (err) return console.error(err);

    if (event.type === "message") {
        if (event.isE2EE) {
            // Must reply via E2EE for encrypted threads
            api.e2ee.sendMessage(event.threadID, "Got your encrypted message!");
        } else {
            api.sendMessage("Got it!", event.threadID);
        }
    }
});

Note: For E2EE DM replies, always use api.e2ee.sendMessage() — the regular MQTT path does not reach E2EE threads.


E2EE — Encrypted Conversations

NEXCA connects to Facebook's real E2EE infrastructure using the Signal Protocol — the same Noise WebSocket path used by the official Messenger mobile app.

api.connectE2EE([deviceStorePath])Promise

Initialize E2EE. Automatically creates the device store directory if it doesn't exist.

// Default path: .nexca/e2ee_device.json in process.cwd()
// Directory is auto-created — no manual setup needed
await api.connectE2EE();

// Custom path
await api.connectE2EE("./my_data/e2ee.json");

console.log("E2EE connected:", api.e2ee.isConnected()); // true

Keep the device store file safe. Deleting it forces device re-registration with Facebook.

api.e2ee.sendMessage(threadId, msg, [replyToMessageId])Promise

msg can be a plain string or a message object with body and/or attachment (readable stream or array of streams). Attachments on E2EE threads are automatically encrypted and sent via the Noise WebSocket. Attachments on non-E2EE threads fall back to NEXCA's own sendMessage.

// Text only
await api.e2ee.sendMessage("100000000000001", "Hello, encrypted!");

// Reply
await api.e2ee.sendMessage("100000000000001", "Reply!", "mid.$replyID");

// Image
const fs = require("fs");
await api.e2ee.sendMessage("100000000000001", {
    body: "Check this out!",
    attachment: fs.createReadStream("photo.jpg")
});

// Audio / Video / File — same pattern, type detected automatically from file extension
await api.e2ee.sendMessage("100000000000001", { attachment: fs.createReadStream("voice.ogg") });
await api.e2ee.sendMessage("100000000000001", { attachment: fs.createReadStream("clip.mp4") });

// Multiple attachments
await api.e2ee.sendMessage("100000000000001", {
    attachment: [fs.createReadStream("a.jpg"), fs.createReadStream("b.jpg")]
});

api.e2ee.sendReaction(threadId, messageId, reaction)Promise

await api.e2ee.sendReaction("100000000000001", "mid.$abc123", "❤️");

api.e2ee.sendTyping(threadId, isTyping)Promise

await api.e2ee.sendTyping("100000000000001", true);
await api.e2ee.sendTyping("100000000000001", false);

api.e2ee.unsendMessage(messageId, threadId)Promise

await api.e2ee.unsendMessage("mid.$abc123", "100000000000001");

api.e2ee.editMessage(threadId, messageId, text)Promise

await api.e2ee.editMessage("100000000000001", "mid.$abc123", "Updated!");

api.e2ee.onMessage(callback)

api.e2ee.onMessage((err, event) => {
    // event.isE2EE is always true
    console.log("[E2EE]", event.senderID, ":", event.body);
});

api.e2ee.isConnected()boolean

if (api.e2ee.isConnected()) await api.e2ee.sendMessage(threadID, "hi");

api.e2ee.disconnect()Promise

await api.e2ee.disconnect();

Sending Messages

api.sendMessage(msg, threadID, [callback], [replyToMessageID])Promise

MQTT-first, HTTP fallback.

// Text
await api.sendMessage("Hello!", threadID);

// Reply
api.sendMessage("Noted!", threadID, callback, replyToMessageID);

// Mention — tag must include the @ prefix; fromIndex is the position of @ in body
api.sendMessage({
    body: "Hey @John!",
    mentions: [{ id: "100000000000001", tag: "@John", fromIndex: 4 }]
}, threadID);

// Multiple mentions
api.sendMessage({
    body: "@Alice and @Bob check this out",
    mentions: [
        { id: "111111111111", tag: "@Alice", fromIndex: 0 },
        { id: "222222222222", tag: "@Bob",   fromIndex: 11 }
    ]
}, threadID);

// Attachment from file stream
api.sendMessage({ body: "File!", attachment: fs.createReadStream("./photo.jpg") }, threadID);

// Attachment from a remote URL (stream via request)
const request = require("request");
api.sendMessage({ attachment: request("https://example.com/img.jpg") }, threadID);

// Sticker / Emoji / Location
api.sendMessage({ sticker: "369239263222822" }, threadID);
api.sendMessage({ emoji: "❤️", emojiSize: "large" }, threadID);
api.sendMessage({ location: { latitude: 14.5995, longitude: 120.9842, current: true } }, threadID);

api.sendSticker(stickerID, threadID, [replyTo], [callback])

api.sendSticker("369239263222822", threadID);

api.sendEmoji(emoji, [emojiSize], threadID, [callback])

api.sendEmoji("🔥", "large", threadID);
// emojiSize: "small" | "medium" | "large"

api.sendGif(gifSrc, threadID, [callback])

api.sendGif("https://media.giphy.com/media/xyz/giphy.gif", threadID);

api.sendLocation(lat, lng, threadID, [isCurrent], [callback])

api.sendLocation(14.5995, 120.9842, threadID);

api.sendImage(path, threadID, [caption], [callback])

api.sendImage("./photo.jpg", threadID, "Look at this!");

api.sendVideo(path, threadID, [caption], [callback])

api.sendVideo("./video.mp4", threadID, "Watch 👀");

api.sendAudio(path, threadID, [callback])

api.sendAudio("./voice.ogg", threadID);

api.sendFile(path, threadID, [caption], [callback])

api.sendFile("./doc.pdf", threadID, "Here's the doc");

api.shareLink(url, threadID, [message], [callback])

api.shareLink("https://github.com", threadID, "Check this out!");

api.shareContact(text, userID, threadID, [callback])

api.shareContact("Meet my friend!", "100000000000001", threadID);

api.forwardAttachment(attachmentID, userOrUsers, [callback])

api.forwardAttachment("attach_fbid", ["uid1", "uid2"]);

Other send methods

| Method | Description | |---|---| | api.sendMessageMqtt(msg, threadID) | MQTT-only, no HTTP fallback | | api.OldMessage(msg, threadID, cb, replyTo, isSingleUser) | HTTP-only (legacy) | | api.sendMessageDM(msg, threadID) | HTTP DM shorthand |


Message Actions

api.editMessage(text, messageID, [callback])

Uses MQTT (label 742) for instant delivery; falls back to HTTP if MQTT is not yet connected.

// Promise
await api.editMessage("Updated text", "mid.$abc123");

// Callback
api.editMessage("Updated text", "mid.$abc123", (err) => {
    if (err) return console.error(err);
    console.log("Message edited!");
});

api.unsendMessage(messageID, [callback])

api.unsendMessage("mid.$abc123", callback);

api.deleteMessage(messageIDs, [callback])

api.deleteMessage(["mid.$abc123", "mid.$def456"], callback);

api.setMessageReaction(reaction, messageID, threadID, [callback])

api.setMessageReaction("😍", messageID, threadID);
api.setMessageReaction("", messageID, threadID); // remove

api.getMessage(threadID, messageID, [callback])

const msg = await api.getMessage(threadID, "mid.$abc123");
console.log(msg.senderID, ":", msg.body);

Attachments & Media

api.uploadAttachment(attachments, [callback])Promise

const [meta] = await api.uploadAttachment([fs.createReadStream("./photo.jpg")]);
console.log("File ID:", meta.attach_fbid);

Tip — stream a URL as attachment:

const request = require("request");
api.sendMessage({ attachment: request("https://example.com/img.jpg") }, threadID);

api.uploadImageToImgbb(image, [expiration], [callback])Promise

// URL, Buffer, or base64
const result = await api.uploadImageToImgbb("https://example.com/img.jpg");
console.log("URL:", result.data.url);

api.resolvePhotoUrl(photoID, [callback])

api.resolvePhotoUrl("photo_id", (err, url) => console.log(url));

Thread Management

api.getThreadInfo(threadID, [callback])Promise

const info = await api.getThreadInfo(threadID);
// info.threadName, .participantIDs, .isGroup, .unreadCount, .adminIDs, .emoji, .color

api.getThreadList(limit, [timestamp], [tags], [callback])Promise

const threads = await api.getThreadList(10, null, ["INBOX"]);
// tags: "INBOX" | "PENDING" | "ARCHIVED"

api.getThreadHistory(threadID, amount, [timestamp], [callback])Promise

const messages = await api.getThreadHistory(threadID, 20, null);

api.createGroup(message, participantIDs, [callback])

const { threadID } = await api.createGroup("Hey!", ["uid1", "uid2"]);

api.deleteThread(threadID, [callback])

api.deleteThread(threadID);

api.muteThread(threadID, muteSeconds, [callback])

api.muteThread(threadID, 3600); // mute 1 hour
api.muteThread(threadID, 0);    // unmute

api.changeArchivedStatus(threadID, archive, [callback])

api.changeArchivedStatus(threadID, true);  // archive
api.changeArchivedStatus(threadID, false); // unarchive

Thread Customization

api.setTitle("New Name", threadID);
api.changeThreadColor("#0084FF", threadID);
api.changeThreadEmoji("🔥", threadID);
api.changeNickname("The Boss", threadID, "100000000000001");
api.changeGroupImage(fs.createReadStream("./group.jpg"), threadID);
api.changeThreadTheme("197931430960496", threadID);

Thread Members

api.addUserToGroup("100000000000001", threadID);
api.removeUserFromGroup("100000000000001", threadID);
api.changeAdminStatus(threadID, "100000000000001", true);  // promote
api.changeAdminStatus(threadID, "100000000000001", false); // demote

Polls

api.createPoll(title, threadID, [options], [callback])

api.createPoll("Favorite language?", threadID, { JS: false, Python: false });

api.setPollVote(pollID, optionIDs, [newOptions], [callback])

api.setPollVote(pollID, ["option_id"], [], callback);

Pins

api.pinMessage("mid.$abc123", threadID);   // MQTT label 430 — pin_msg_v2_
api.unpinMessage("mid.$abc123", threadID); // MQTT label 431 — unpin_msg_v2_

Read / Delivery Receipts

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

// Typing indicator
api.sendTypingIndicator(threadID, true);
setTimeout(() => api.sendTypingIndicator(threadID, false), 3000);

User Info & Friends

api.getUserInfo(id, [callback])Promise

const users = await api.getUserInfo(["uid1", "uid2"]);
// users[uid].name, .thumbSrc, .gender

Quick reference

api.getCurrentUserID()                        // → string
api.getUserID("John Doe", callback)           // search by name
api.getUID("https://facebook.com/zuck", cb)   // → "4"
api.getFriendsList(callback)                  // full friends list
api.searchFriends("Maria", callback)
api.getAvatarUser("uid", callback)            // returns graph.facebook.com picture URL
api.getAvatarUser("uid", "square", callback)  // type: square | large | normal | small
api.getProfileInfo("uid", callback)           // GraphQL node
api.getPublicData("uid", callback)            // public scrape (name, vanity, uid)
api.getRepInfo(callback)                      // account rep info

Actions

api.sendFriendRequest("uid");                 // GraphQL FriendingCometFriendRequestSendMutation
api.handleFriendRequest("uid", true);         // accept via /requests/friends/ajax/
api.handleFriendRequest("uid", false);        // decline
api.handleMessageRequest(threadID, true);
api.changeBlockedStatus("uid", true);         // block
api.changeBlockedStatus("uid", false);        // unblock
api.followUser("uid");
api.unfollowUser("uid");
api.unfriend("uid");
api.setActiveStatus(true);                    // appear online
api.logout();

Social / Posting

// Reactions: "like" | "love" | "haha" | "wow" | "sad" | "angry" | "care" | "none"
api.reactToPost("postID", "love");
api.reactToComment("commentID", "haha");
api.postComment("postID", "Great post! 🔥");
api.deleteComment("commentID");
api.sharePost("postID", "Check this!");
api.getPostInfo("postID", callback);
api.getStoryReactions("feedbackID", callback);

Stickers

api.getStickers("thumbs up", null, (err, stickers) => {
    if (stickers.length) api.sendSticker(stickers[0].id, threadID);
});

api.getStickerPacks((err, packs) => {
    packs.forEach(p => console.log(p.id, p.name));
});

HTTP Utilities

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

const dtsg = await api.getFreshDtsg();

Misc / Config

api.setOptions({ listenTyping: true, proxy: "http://127.0.0.1:8080" });
api.getAppState()        // → cookie array (save to reuse session)
api.getCurrentUserID()   // → string
api.getCtx()             // → internal ctx object (region, fb_dtsg, etc.)

// Add a custom method to the api object
api.addExternalModule("myFunc", (defaultFuncs, api, ctx) => {
    return function (text, threadID) {
        return api.sendMessage("[BOT] " + text, threadID);
    };
});
api.myFunc("Hello!", threadID);

Broadcast

api.sendBroadcast(msg, threadIDs, [options], [callback])Promise

Send to multiple threads with rate limiting and per-thread delivery tracking.

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

console.log(result.sent.length + "/" + result.total + " delivered");

Session Guard

Protects your appstate from corruption and silent logouts. Highly recommended.

// Call right after login
api.sessionGuard("./appstate.json");

// Custom timing
api.sessionGuard("./appstate.json", {
    interval: 3 * 60 * 1000,  // save every 3 min
    debounce: 60 * 1000        // cooldown after sendMessage
});

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 appstate.json.bak before every overwrite
api.saveSession()            // → boolean  (force save now)
api.restoreSessionBackup()   // → boolean  (restore .bak if corrupted)
api.stopSessionGuard()       // stop the timer

E2EE Architecture

NEXCA uses Facebook's real E2EE infrastructure — identical to the official Messenger mobile app:

Signal Protocol stack
  ├── @signalapp/libsignal-client  — Double Ratchet + X3DH
  ├── Noise_XX_25519_AESGCM_SHA256 — WebSocket handshake
  ├── WA-binary + Protobuf          — message frame encoding
  └── ICDC device registration      — registers bot as E2EE device

Connection flow
  api.connectE2EE()
    ├─ 1. Auto-create device store directory
    ├─ 2. Bootstrap auth (re-use existing cookie session)
    ├─ 3. Register device keys with Facebook (first run only)
    └─ 4. Open Noise WebSocket → ready

Message flow
  Incoming  →  Noise WS  →  listenE2EE callback  (event.isE2EE = true)
  Outgoing  →  api.e2ee.sendMessage()  →  Noise WS  →  Facebook

The E2EE engine is vendored inside the package. Your bot continues to work even if the upstream npm dependency is removed or broken.


Made with ❤️ by Deku  ·  MIT License