@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.
Maintainers
Readme
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/nexcaNode.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) andapi.listenE2EE(encrypted). E2EE reply events carryisE2EE: trueon both the event and themessageReplyobject.
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()); // trueKeep 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); // removeapi.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, .colorapi.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); // unmuteapi.changeArchivedStatus(threadID, archive, [callback])
api.changeArchivedStatus(threadID, true); // archive
api.changeArchivedStatus(threadID, false); // unarchiveThread 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); // demotePolls
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, .genderQuick 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 infoActions
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.bakbefore every overwrite
api.saveSession() // → boolean (force save now)
api.restoreSessionBackup() // → boolean (restore .bak if corrupted)
api.stopSessionGuard() // stop the timerE2EE 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 → FacebookThe 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
