levix-bot 2.0.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.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/SETUP.md +242 -0
  4. package/app.cjs +337 -0
  5. package/bin/levix.js +7 -0
  6. package/deploy/install.sh +182 -0
  7. package/deploy/levix.service +56 -0
  8. package/deploy/nginx/levix.leviro.net.conf +79 -0
  9. package/package.json +78 -0
  10. package/public/brand/apple-touch-icon.png +0 -0
  11. package/public/brand/banner.jpg +0 -0
  12. package/public/brand/banner.webp +0 -0
  13. package/public/brand/icon-16.png +0 -0
  14. package/public/brand/icon-192.png +0 -0
  15. package/public/brand/icon-32.png +0 -0
  16. package/public/brand/icon-512.png +0 -0
  17. package/public/brand/icon-512.webp +0 -0
  18. package/public/brand/mark.png +0 -0
  19. package/public/brand/mark.svg +16 -0
  20. package/public/brand/mark.webp +0 -0
  21. package/public/brand/wordmark.png +0 -0
  22. package/public/brand/wordmark.webp +0 -0
  23. package/public/dashboard.css +881 -0
  24. package/public/dashboard.js +1057 -0
  25. package/public/logo.svg +16 -0
  26. package/public/qrcode.min.js +1 -0
  27. package/public/socket.io.min.js +7 -0
  28. package/scheduler.cjs +197 -0
  29. package/src/auth/auth-storage.cjs +74 -0
  30. package/src/auth/use-database-auth-state.js +130 -0
  31. package/src/bootstrap/core.js +74 -0
  32. package/src/bootstrap/events.cjs +43 -0
  33. package/src/bootstrap/panel.js +97 -0
  34. package/src/cli.js +153 -0
  35. package/src/commands/autoschedule.cjs +84 -0
  36. package/src/commands/blacklist.cjs +116 -0
  37. package/src/commands/block.cjs +70 -0
  38. package/src/commands/calc.cjs +387 -0
  39. package/src/commands/debt.cjs +175 -0
  40. package/src/commands/deleteschedule.cjs +36 -0
  41. package/src/commands/gemini.cjs +727 -0
  42. package/src/commands/group/add.cjs +136 -0
  43. package/src/commands/group/all.cjs +43 -0
  44. package/src/commands/group/antiSpam.cjs +96 -0
  45. package/src/commands/group/antilink.cjs +167 -0
  46. package/src/commands/group/approveall.cjs +54 -0
  47. package/src/commands/group/clearwarns.cjs +41 -0
  48. package/src/commands/group/deletenote.cjs +33 -0
  49. package/src/commands/group/demote.cjs +62 -0
  50. package/src/commands/group/kick.cjs +85 -0
  51. package/src/commands/group/media.cjs +120 -0
  52. package/src/commands/group/members.cjs +59 -0
  53. package/src/commands/group/note.cjs +32 -0
  54. package/src/commands/group/promote.cjs +60 -0
  55. package/src/commands/group/removeall.cjs +42 -0
  56. package/src/commands/group/rules.cjs +31 -0
  57. package/src/commands/group/save.cjs +36 -0
  58. package/src/commands/group/setname.cjs +41 -0
  59. package/src/commands/group/setpp.cjs +51 -0
  60. package/src/commands/group/setrules.cjs +46 -0
  61. package/src/commands/group/setwarn.cjs +58 -0
  62. package/src/commands/group/tagadmins.cjs +50 -0
  63. package/src/commands/group/warn.cjs +105 -0
  64. package/src/commands/group/warns.cjs +66 -0
  65. package/src/commands/group/welcome.cjs +109 -0
  66. package/src/commands/group.cjs +160 -0
  67. package/src/commands/help.cjs +230 -0
  68. package/src/commands/listschedules.cjs +47 -0
  69. package/src/commands/loop.cjs +163 -0
  70. package/src/commands/memory.cjs +201 -0
  71. package/src/commands/mod.cjs +155 -0
  72. package/src/commands/notes.cjs +28 -0
  73. package/src/commands/perm.cjs +236 -0
  74. package/src/commands/ping.cjs +17 -0
  75. package/src/commands/poll.cjs +80 -0
  76. package/src/commands/prayer.cjs +64 -0
  77. package/src/commands/qr.cjs +39 -0
  78. package/src/commands/rand.cjs +130 -0
  79. package/src/commands/restart.cjs +25 -0
  80. package/src/commands/schedule.cjs +86 -0
  81. package/src/commands/score.cjs +130 -0
  82. package/src/commands/setprefix.cjs +40 -0
  83. package/src/commands/shortlink.cjs +74 -0
  84. package/src/commands/shutdown.cjs +45 -0
  85. package/src/commands/status.cjs +75 -0
  86. package/src/commands/stt.cjs +150 -0
  87. package/src/commands/todo.cjs +88 -0
  88. package/src/commands/tts.cjs +170 -0
  89. package/src/commands/unblock.cjs +57 -0
  90. package/src/commands/weather.cjs +79 -0
  91. package/src/config/ai-persona.md +17 -0
  92. package/src/config/baileys.config.js +51 -0
  93. package/src/config/brand.cjs +36 -0
  94. package/src/config/brand.esm.js +17 -0
  95. package/src/config/constants.js +16 -0
  96. package/src/config/defaults.cjs +91 -0
  97. package/src/config/lock.cjs +110 -0
  98. package/src/config/paths.cjs +90 -0
  99. package/src/config/runtime-config.cjs +219 -0
  100. package/src/config/secrets.cjs +130 -0
  101. package/src/config/settings.cjs +386 -0
  102. package/src/core/connection.js +199 -0
  103. package/src/core/events.js +61 -0
  104. package/src/core/socket.js +64 -0
  105. package/src/db/db.cjs +282 -0
  106. package/src/db/store.cjs +837 -0
  107. package/src/db/store.esm.js +17 -0
  108. package/src/domain/caddy.js +120 -0
  109. package/src/domain/command.js +380 -0
  110. package/src/domain/detect.js +173 -0
  111. package/src/domain/nginx.js +138 -0
  112. package/src/domain/system.js +148 -0
  113. package/src/handlers/command.handler.js +414 -0
  114. package/src/handlers/group.handler.js +73 -0
  115. package/src/handlers/message.handler.js +149 -0
  116. package/src/index.js +217 -0
  117. package/src/middleware/antispam.middleware.js +68 -0
  118. package/src/middleware/blacklist.middleware.js +29 -0
  119. package/src/middleware/forward-tracking.middleware.js +49 -0
  120. package/src/middleware/permissions.middleware.js +114 -0
  121. package/src/routes/dashboard.api.esm.js +779 -0
  122. package/src/services/aiAgent.cjs +317 -0
  123. package/src/services/aiTools.cjs +635 -0
  124. package/src/utils/datetime.cjs +61 -0
  125. package/src/utils/lid-helper.esm.js +143 -0
  126. package/src/utils/logger.cjs +72 -0
  127. package/src/utils/memory.cjs +385 -0
  128. package/src/utils/normalizeJid.cjs +65 -0
  129. package/src/utils/normalizeJid.esm.js +37 -0
  130. package/src/utils/openBrowser.cjs +129 -0
  131. package/src/utils/permissions.cjs +154 -0
  132. package/src/utils/permissions.esm.js +375 -0
  133. package/src/utils/recentMessageCache.esm.js +70 -0
  134. package/src/utils/requestOrigin.cjs +102 -0
  135. package/src/utils/sendBotMessage.cjs +43 -0
  136. package/src/utils/sendBotMessage.esm.js +139 -0
  137. package/src/utils/statusMessage.cjs +179 -0
  138. package/src/utils/storage-hub.cjs +49 -0
  139. package/src/utils/storage-hub.esm.js +24 -0
  140. package/src/utils/storage.cjs +80 -0
  141. package/src/utils/storage.esm.js +88 -0
  142. package/src/utils/textDecode.cjs +197 -0
  143. package/src/utils/thumbnail.cjs +591 -0
  144. package/src/utils/typing.esm.js +102 -0
  145. package/views/dashboard.ejs +329 -0
  146. package/views/login.ejs +105 -0
  147. package/views/qr.ejs +108 -0
  148. package/views/setup.ejs +122 -0
@@ -0,0 +1,70 @@
1
+ // In-memory store for messages the bot itself sent.
2
+ //
3
+ // Sole purpose: back Baileys' getMessage callback so retries succeed. When a
4
+ // recipient can't decrypt something we sent, Baileys asks for it again through
5
+ // getMessage; rc10's enableAutoSessionRecreation needs this to recover from
6
+ // "this message can take a while".
7
+ //
8
+ // Nothing is written to disk and nothing other people send is kept here.
9
+ //
10
+ // Eviction: time-based + size-cap, both small — a retry request arrives within
11
+ // minutes or not at all.
12
+
13
+ const DEFAULT_TTL_MS = 1000 * 60 * 60; // 1 hour
14
+ const DEFAULT_MAX_ITEMS = 500;
15
+
16
+ const recent = new Map(); // id -> { value, expiresAt }
17
+ let ttlMs = DEFAULT_TTL_MS;
18
+ let maxItems = DEFAULT_MAX_ITEMS;
19
+
20
+ function gc(now = Date.now()) {
21
+ // Drop expired entries first.
22
+ for (const [id, entry] of recent) {
23
+ if (entry.expiresAt <= now) recent.delete(id);
24
+ }
25
+ // If still over the cap, drop oldest insertions (Map preserves order).
26
+ while (recent.size > maxItems) {
27
+ const oldest = recent.keys().next().value;
28
+ if (oldest === undefined) break;
29
+ recent.delete(oldest);
30
+ }
31
+ }
32
+
33
+ export function configureRecentCache({ ttlMs: nextTtl, maxItems: nextMax } = {}) {
34
+ if (Number.isFinite(nextTtl) && nextTtl > 0) ttlMs = nextTtl;
35
+ if (Number.isFinite(nextMax) && nextMax > 0) maxItems = nextMax;
36
+ }
37
+
38
+ export function rememberMessage(id, value) {
39
+ if (!id) return;
40
+ const expiresAt = Date.now() + ttlMs;
41
+ recent.set(id, { value, expiresAt });
42
+ if (recent.size > maxItems) gc();
43
+ }
44
+
45
+ export function recallMessage(id) {
46
+ if (!id) return null;
47
+ const entry = recent.get(id);
48
+ if (!entry) return null;
49
+ if (entry.expiresAt <= Date.now()) {
50
+ recent.delete(id);
51
+ return null;
52
+ }
53
+ return entry.value;
54
+ }
55
+
56
+ export function forgetMessage(id) {
57
+ if (!id) return;
58
+ recent.delete(id);
59
+ }
60
+
61
+ // Used by Baileys getMessage; expects a `proto.IMessage` (or undefined).
62
+ export async function getMessageFromRecent(key) {
63
+ if (!key?.id) return undefined;
64
+ const stored = recallMessage(key.id);
65
+ if (!stored) return undefined;
66
+ // Stored archive uses { message, ... } shape -- we only need .message
67
+ return stored.message || stored.rawMessage || undefined;
68
+ }
69
+
70
+ setInterval(() => gc(), 1000 * 60 * 5).unref?.();
@@ -0,0 +1,102 @@
1
+ // Who is really on the other end of this request.
2
+ //
3
+ // WHY THIS IS NOT `req.ip`
4
+ // ------------------------
5
+ // Express's `req.ip` honours `X-Forwarded-For` as soon as `trust proxy` is set
6
+ // — including a hop count like `1`, which is exactly what the panel tells an
7
+ // nginx user to configure. From a non-loopback address:
8
+ //
9
+ // trust proxy = 1, header "X-Forwarded-For: 127.0.0.1" -> req.ip = 127.0.0.1
10
+ //
11
+ // So a stranger could have made an unclaimed bot believe they were sitting at
12
+ // its keyboard, and claim it without the setup code. `req.ip` is the right
13
+ // answer for logging and for "who is this user"; it is the wrong answer for
14
+ // "is this physically the same machine".
15
+ //
16
+ // The socket address on its own isn't enough either: a reverse proxy on the
17
+ // same host connects from 127.0.0.1, so every remote visitor through it would
18
+ // look local.
19
+ //
20
+ // THE RULE
21
+ // --------
22
+ // A request counts as local only when all three hold:
23
+ //
24
+ // 1. the operator has NOT configured a proxy (`trust_proxy` is empty) —
25
+ // if they told us something is in front, anything could have come through it
26
+ // 2. no forwarding header is present — anything that sets one is a proxy,
27
+ // declared or not
28
+ // 3. the TCP peer is a loopback address — the only part of this that a
29
+ // remote client cannot influence
30
+ //
31
+ // A reverse-proxy deployment therefore always uses the setup code, which is
32
+ // what we want: the code is printed on the server's own console, so having it
33
+ // proves access to the machine. `levix` on a laptop stays code-free.
34
+
35
+ // Headers that mean "a proxy handled this". Any of them present is enough to
36
+ // stop calling the request local, whether or not we trust the value.
37
+ const FORWARDING_HEADERS = [
38
+ "x-forwarded-for",
39
+ "x-real-ip",
40
+ "forwarded",
41
+ "x-forwarded-host",
42
+ "x-forwarded-proto",
43
+ "x-client-ip",
44
+ "cf-connecting-ip",
45
+ "true-client-ip",
46
+ ];
47
+
48
+ /** 127.0.0.0/8, ::1, and the IPv4-mapped forms of both. */
49
+ function isLoopbackAddress(address) {
50
+ if (typeof address !== "string" || !address) return false;
51
+
52
+ // Strip a zone id ("fe80::1%eth0") and any IPv4-mapped IPv6 prefix.
53
+ let value = address.trim().toLowerCase().split("%")[0];
54
+ if (value.startsWith("[") && value.endsWith("]")) value = value.slice(1, -1);
55
+ if (value.startsWith("::ffff:")) value = value.slice("::ffff:".length);
56
+
57
+ if (value === "::1" || value === "0:0:0:0:0:0:0:1") return true;
58
+
59
+ // 127.0.0.0/8 — and only that: "127.0.0.1.evil.com" or "1270.0.1" must not pass.
60
+ const octets = value.split(".");
61
+ if (octets.length !== 4) return false;
62
+ if (!octets.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)) {
63
+ return false;
64
+ }
65
+ return octets[0] === "127";
66
+ }
67
+
68
+ function hasForwardingHeader(headers = {}) {
69
+ return FORWARDING_HEADERS.some((name) => {
70
+ const value = headers[name];
71
+ return typeof value === "string" && value.trim() !== "";
72
+ });
73
+ }
74
+
75
+ /**
76
+ * The address of the machine that actually opened the TCP connection.
77
+ * Never derived from a header, so it can't be forged — which is what makes it
78
+ * the right key for rate limiting a password or setup-code guess.
79
+ */
80
+ function clientAddress(req) {
81
+ return req?.socket?.remoteAddress || req?.connection?.remoteAddress || "";
82
+ }
83
+
84
+ /**
85
+ * Is this request coming straight from this machine, with nothing in between?
86
+ *
87
+ * @param {object} req
88
+ * @param {{ proxyConfigured?: boolean }} [options]
89
+ */
90
+ function isDirectLocalRequest(req, { proxyConfigured = false } = {}) {
91
+ if (proxyConfigured) return false;
92
+ if (hasForwardingHeader(req?.headers)) return false;
93
+ return isLoopbackAddress(clientAddress(req));
94
+ }
95
+
96
+ module.exports = {
97
+ FORWARDING_HEADERS,
98
+ isLoopbackAddress,
99
+ hasForwardingHeader,
100
+ clientAddress,
101
+ isDirectLocalRequest,
102
+ };
@@ -0,0 +1,43 @@
1
+ // CommonJS shim around sendBotMessage.esm.js so .cjs commands can use it.
2
+ // We lazy-import the ESM module on first call and cache the resolved exports.
3
+
4
+ const logger = require("./logger.cjs");
5
+
6
+ let cached = null;
7
+
8
+ async function getModule() {
9
+ if (cached) return cached;
10
+ cached = await import("./sendBotMessage.esm.js");
11
+ return cached;
12
+ }
13
+
14
+ async function sendBotMessage(sock, jid, content, options = {}) {
15
+ try {
16
+ const mod = await getModule();
17
+ return await mod.sendBotMessage(sock, jid, content, options);
18
+ } catch (err) {
19
+ logger.error({ err }, "[sendBotMessage.cjs] fell back to raw sendMessage");
20
+ const opts = {};
21
+ if (options?.quote !== false) {
22
+ const quoted = options?.quoted || options?.replyTo;
23
+ if (quoted) opts.quoted = quoted;
24
+ }
25
+ return sock.sendMessage(jid, content, opts);
26
+ }
27
+ }
28
+
29
+ async function sendBotError(sock, jid, err, arabicLead = "حدث خطأ", options = {}) {
30
+ try {
31
+ const mod = await getModule();
32
+ return await mod.sendBotError(sock, jid, err, arabicLead, options);
33
+ } catch (innerErr) {
34
+ logger.error({ err: innerErr }, "[sendBotMessage.cjs] error helper failed");
35
+ const name = err?.name || "Error";
36
+ const message = err?.message || String(err);
37
+ return sock.sendMessage(jid, {
38
+ text: `❌ *${arabicLead}*\n\n*النوع:* \`${name}\`\n*التفاصيل:* ${message}`,
39
+ });
40
+ }
41
+ }
42
+
43
+ module.exports = { sendBotMessage, sendBotError };
@@ -0,0 +1,139 @@
1
+ // Unified outbound message helper.
2
+ //
3
+ // Every command / handler in the bot should send replies through
4
+ // sendBotMessage(...) instead of calling sock.sendMessage directly. It does:
5
+ //
6
+ // * typing simulation ("composing") before sending, with realistic delay
7
+ // proportional to text length (capped 300-3000ms).
8
+ // * a randomized inter-message delay per chat so several messages from the
9
+ // same handler don't arrive bunched in the same second.
10
+ // * automatic `quoted: msg` so the bot's reply is threaded under the
11
+ // command. Pass `{ quote: false }` to opt out (e.g. status broadcasts).
12
+ // * tracks the last bot-sent message per chat for follow-up updates.
13
+ //
14
+ // The helper is resilient: if presence updates fail (rare on Baileys v7) the
15
+ // message itself is still sent.
16
+
17
+ import { createRequire } from "module";
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const logger = require("./logger.cjs");
21
+ const settings = require("../config/settings.cjs");
22
+
23
+ const lastSendTime = new Map(); // chatJid -> timestamp ms
24
+
25
+ // Read per send: both are editable from the dashboard.
26
+ function sendGap() {
27
+ const min = Math.max(0, settings.get("bot_min_delay_ms"));
28
+ return { min, max: Math.max(min, settings.get("bot_max_delay_ms")) };
29
+ }
30
+
31
+ function jitter(min, max) {
32
+ if (max <= min) return min;
33
+ return Math.floor(Math.random() * (max - min + 1)) + min;
34
+ }
35
+
36
+ function sleep(ms) {
37
+ if (!ms || ms <= 0) return Promise.resolve();
38
+ return new Promise((resolve) => setTimeout(resolve, ms));
39
+ }
40
+
41
+ function typingDelayFor(content) {
42
+ const text = content?.text || content?.caption || "";
43
+ if (!text) return jitter(300, 700);
44
+ // ~40 chars/sec typing rhythm, clamped to [400, 2500] ms
45
+ const base = Math.min(2500, Math.max(400, (text.length / 40) * 1000));
46
+ return Math.floor(base * (0.85 + Math.random() * 0.3));
47
+ }
48
+
49
+ async function safePresence(sock, jid, presence) {
50
+ try {
51
+ await sock.sendPresenceUpdate(presence, jid);
52
+ } catch (err) {
53
+ logger.debug({ err: err?.message, jid, presence }, "[sendBotMessage] presence failed");
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Send a message with typing simulation, inter-message delay, and quoted reply.
59
+ *
60
+ * @param {object} sock - Baileys socket
61
+ * @param {string} jid - Chat JID
62
+ * @param {object} content - Message content (passed to sock.sendMessage)
63
+ * @param {object} options
64
+ * @param {object} [options.quoted] - Message to quote. Defaults to options.replyTo.
65
+ * @param {object} [options.replyTo] - Original incoming message (auto-quoted).
66
+ * @param {boolean} [options.quote=true] - Set false to skip auto-quoting.
67
+ * @param {boolean} [options.typing=true] - Set false to skip typing presence.
68
+ * @param {number} [options.delayMs] - Override the inter-message gap.
69
+ * @returns {Promise<object>} Baileys send result
70
+ */
71
+ export async function sendBotMessage(sock, jid, content, options = {}) {
72
+ const {
73
+ replyTo = null,
74
+ quoted = null,
75
+ quote = true,
76
+ typing = true,
77
+ delayMs,
78
+ } = options;
79
+
80
+ // Respect the per-chat cooldown so multiple replies don't pile up instantly.
81
+ const now = Date.now();
82
+ const lastAt = lastSendTime.get(jid) || 0;
83
+ const elapsed = now - lastAt;
84
+ let targetGap;
85
+ if (typeof delayMs === "number") {
86
+ targetGap = Math.max(0, delayMs);
87
+ } else {
88
+ const { min, max } = sendGap();
89
+ targetGap = jitter(min, max);
90
+ }
91
+ if (elapsed < targetGap) await sleep(targetGap - elapsed);
92
+
93
+ if (typing) {
94
+ await safePresence(sock, jid, "composing");
95
+ await sleep(typingDelayFor(content));
96
+ await safePresence(sock, jid, "paused");
97
+ }
98
+
99
+ const opts = {};
100
+ if (quote) {
101
+ const quotedMsg = quoted || replyTo;
102
+ if (quotedMsg) opts.quoted = quotedMsg;
103
+ }
104
+
105
+ try {
106
+ const sent = await sock.sendMessage(jid, content, opts);
107
+ lastSendTime.set(jid, Date.now());
108
+ return sent;
109
+ } catch (err) {
110
+ logger.error(
111
+ { err, jid, contentKeys: Object.keys(content || {}) },
112
+ "[sendBotMessage] sock.sendMessage failed"
113
+ );
114
+ throw err;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Helper: send a textual error message that includes the actual error name +
120
+ * a concise reason, so we don't smear the user with "حدث خطأ" alone.
121
+ *
122
+ * @param {object} sock
123
+ * @param {string} jid
124
+ * @param {Error|string} err
125
+ * @param {string} [arabicLead] - optional Arabic prefix ("معرفتش أعمل كذا")
126
+ * @param {object} [options] - forwarded to sendBotMessage
127
+ */
128
+ export async function sendBotError(sock, jid, err, arabicLead = "حدث خطأ", options = {}) {
129
+ const name = err?.name || (typeof err === "string" ? "Error" : "Error");
130
+ const message =
131
+ err?.message || (typeof err === "string" ? err : JSON.stringify(err));
132
+ const text = `❌ *${arabicLead}*\n\n*النوع:* \`${name}\`\n*التفاصيل:* ${message}`.slice(
133
+ 0,
134
+ 1500
135
+ );
136
+ return sendBotMessage(sock, jid, { text }, options);
137
+ }
138
+
139
+ export default sendBotMessage;
@@ -0,0 +1,179 @@
1
+ // One live message per command, edited in place.
2
+ //
3
+ // Commands used to narrate their work with a stream of separate messages
4
+ // ("🔍 جاري التحليل...", "🎬 جاري التحميل...", "📦 الملف كبير..."), which buries
5
+ // the chat. WhatsApp supports editing a message you sent, so a command can
6
+ // instead post ONE line and rewrite it as it progresses:
7
+ //
8
+ // const status = await createStatus(sock, jid, "🔍 بحلل الرابط...", { replyTo: msg });
9
+ // await status.update("🎬 بنزّل الفيديو...");
10
+ // await status.finish("✅ اتبعت — 12.3 MB");
11
+ //
12
+ // Everything here is best-effort: a failed edit never breaks the command. If
13
+ // the message is too old to edit (WhatsApp only allows it for a while) or the
14
+ // edit is rejected, we fall back to a normal send and keep going.
15
+
16
+ const logger = require("./logger.cjs");
17
+ const { sendBotMessage } = require("./sendBotMessage.cjs");
18
+
19
+ // WhatsApp tolerates a few edits per message; spacing them out keeps the
20
+ // server happy and the animation readable.
21
+ const MIN_EDIT_GAP_MS = 600;
22
+
23
+ function sleep(ms) {
24
+ if (!ms || ms <= 0) return Promise.resolve();
25
+ return new Promise((resolve) => setTimeout(resolve, ms));
26
+ }
27
+
28
+ class StatusMessage {
29
+ constructor(sock, jid, options = {}) {
30
+ this.sock = sock;
31
+ this.jid = jid;
32
+ this.replyTo = options.replyTo || null;
33
+ this.quote = options.quote !== false;
34
+ this.key = null;
35
+ this.text = "";
36
+ this.lastEditAt = 0;
37
+ this.closed = false;
38
+ }
39
+
40
+ async start(text) {
41
+ this.text = text;
42
+ try {
43
+ const sent = await sendBotMessage(
44
+ this.sock,
45
+ this.jid,
46
+ { text },
47
+ { replyTo: this.replyTo, quote: this.quote, typing: false, delayMs: 0 }
48
+ );
49
+ this.key = sent?.key || null;
50
+ this.lastEditAt = Date.now();
51
+ } catch (err) {
52
+ logger.warn({ err: err?.message }, "[status] initial send failed");
53
+ this.key = null;
54
+ }
55
+ return this;
56
+ }
57
+
58
+ /** Rewrite the line. Identical text is skipped so we don't burn an edit. */
59
+ async update(text, { force = false } = {}) {
60
+ if (this.closed && !force) return false;
61
+ const next = String(text || "").trim();
62
+ if (!next || next === this.text) return false;
63
+
64
+ // No message to edit (initial send failed) — send one and adopt it.
65
+ if (!this.key) return this._sendFresh(next);
66
+
67
+ const wait = MIN_EDIT_GAP_MS - (Date.now() - this.lastEditAt);
68
+ if (wait > 0) await sleep(wait);
69
+
70
+ try {
71
+ // `quoted: null` on purpose: the command handler wraps sock.sendMessage
72
+ // and auto-quotes anything that doesn't set it, and an edit/delete is a
73
+ // protocolMessage — there is nowhere to hang a contextInfo.
74
+ await this.sock.sendMessage(
75
+ this.jid,
76
+ { text: next, edit: this.key },
77
+ { quoted: null }
78
+ );
79
+ this.text = next;
80
+ this.lastEditAt = Date.now();
81
+ return true;
82
+ } catch (err) {
83
+ logger.debug({ err: err?.message }, "[status] edit failed — sending instead");
84
+ return this._sendFresh(next);
85
+ }
86
+ }
87
+
88
+ async _sendFresh(text) {
89
+ try {
90
+ const sent = await sendBotMessage(
91
+ this.sock,
92
+ this.jid,
93
+ { text },
94
+ { replyTo: this.replyTo, quote: this.quote, typing: false, delayMs: 0 }
95
+ );
96
+ this.key = sent?.key || null;
97
+ this.text = text;
98
+ this.lastEditAt = Date.now();
99
+ return true;
100
+ } catch (err) {
101
+ logger.warn({ err: err?.message }, "[status] fallback send failed");
102
+ return false;
103
+ }
104
+ }
105
+
106
+ /** Last update for this run. Always delivered, even if an edit is refused. */
107
+ async finish(text) {
108
+ const ok = await this.update(text, { force: true });
109
+ this.closed = true;
110
+ return ok;
111
+ }
112
+
113
+ /** Turn the line into the standard error card. */
114
+ async fail(err, arabicLead = "حصلت مشكلة") {
115
+ const name = err?.name || "Error";
116
+ const message = err?.message || String(err || "غير معروف");
117
+ return this.finish(
118
+ `❌ *${arabicLead}*\n\n*النوع:* \`${name}\`\n*التفاصيل:* ${message}`.slice(0, 1500)
119
+ );
120
+ }
121
+
122
+ /** Remove the status line entirely (used when the result speaks for itself). */
123
+ async remove() {
124
+ this.closed = true;
125
+ if (!this.key) return false;
126
+ try {
127
+ await this.sock.sendMessage(
128
+ this.jid,
129
+ { delete: this.key },
130
+ { quoted: null }
131
+ );
132
+ this.key = null;
133
+ return true;
134
+ } catch (err) {
135
+ logger.debug({ err: err?.message }, "[status] delete failed");
136
+ return false;
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Post the first status line and hand back the handle used to rewrite it.
143
+ *
144
+ * @param {object} sock
145
+ * @param {string} jid
146
+ * @param {string} text initial line
147
+ * @param {object} [options]
148
+ * @param {object} [options.replyTo] message to quote
149
+ * @param {boolean}[options.quote] set false to not quote anything
150
+ * @returns {Promise<StatusMessage>}
151
+ */
152
+ async function createStatus(sock, jid, text, options = {}) {
153
+ return new StatusMessage(sock, jid, options).start(text);
154
+ }
155
+
156
+ /**
157
+ * A status handle that does nothing — lets helpers take an optional status
158
+ * without every call site guarding for null.
159
+ */
160
+ function nullStatus() {
161
+ return {
162
+ key: null,
163
+ text: "",
164
+ async update() {
165
+ return false;
166
+ },
167
+ async finish() {
168
+ return false;
169
+ },
170
+ async fail() {
171
+ return false;
172
+ },
173
+ async remove() {
174
+ return false;
175
+ },
176
+ };
177
+ }
178
+
179
+ module.exports = { createStatus, nullStatus, StatusMessage };
@@ -0,0 +1,49 @@
1
+ // CommonJS face of storage-hub.esm.js — the AI conversation history.
2
+ //
3
+ // Both halves are the same SQLite store, so this one can require it directly
4
+ // instead of lazy-importing the ESM module the way it had to when the history
5
+ // lived in a separate async database. The signatures stay async for callers.
6
+
7
+ const logger = require("./logger.cjs");
8
+ const store = require("../db/store.cjs");
9
+
10
+ async function getChatHistoryAsync(chatId) {
11
+ try {
12
+ return store.getChatHistory(chatId);
13
+ } catch (err) {
14
+ logger.error({ err }, "[storage-hub] getChatHistoryAsync failed");
15
+ return [];
16
+ }
17
+ }
18
+
19
+ async function saveChatHistoryAsync(chatId, historyArray) {
20
+ try {
21
+ store.saveChatHistory(chatId, historyArray);
22
+ } catch (err) {
23
+ logger.error({ err }, "[storage-hub] saveChatHistoryAsync failed");
24
+ }
25
+ }
26
+
27
+ async function deleteChatHistoryAsync(chatId) {
28
+ try {
29
+ return store.deleteChatHistory(chatId);
30
+ } catch (err) {
31
+ logger.error({ err }, "[storage-hub] deleteChatHistoryAsync failed");
32
+ return false;
33
+ }
34
+ }
35
+
36
+ async function deleteAllChatHistoriesAsync() {
37
+ try {
38
+ store.deleteAllChatHistories();
39
+ } catch (err) {
40
+ logger.error({ err }, "[storage-hub] deleteAllChatHistoriesAsync failed");
41
+ }
42
+ }
43
+
44
+ module.exports = {
45
+ getChatHistoryAsync,
46
+ saveChatHistoryAsync,
47
+ deleteChatHistoryAsync,
48
+ deleteAllChatHistoriesAsync,
49
+ };
@@ -0,0 +1,24 @@
1
+ // Gemini conversation history.
2
+ //
3
+ // It lives in the same SQLite file as everything else, so these are ordinary
4
+ // synchronous queries. The functions stay `async` because every caller already
5
+ // awaits them (they date from when this was a separate database), and changing
6
+ // forty call sites to save a microtask buys nothing.
7
+
8
+ import store from "../db/store.esm.js";
9
+
10
+ export async function getChatHistoryAsync(chatId) {
11
+ return store.getChatHistory(chatId);
12
+ }
13
+
14
+ export async function saveChatHistoryAsync(chatId, historyArray) {
15
+ store.saveChatHistory(chatId, historyArray);
16
+ }
17
+
18
+ export async function deleteChatHistoryAsync(chatId) {
19
+ return store.deleteChatHistory(chatId);
20
+ }
21
+
22
+ export async function deleteAllChatHistoriesAsync() {
23
+ store.deleteAllChatHistories();
24
+ }
@@ -0,0 +1,80 @@
1
+ // The CommonJS door onto storage.
2
+ //
3
+ // Everything lives in one SQLite file (src/db/db.cjs); src/db/store.cjs holds
4
+ // the queries. This module just re-exports them under the names the commands
5
+ // have always used — and yes, the API is synchronous, because SQLite is.
6
+ //
7
+ // The ESM half is src/utils/storage.esm.js; both point at the same singleton.
8
+
9
+ const store = require("../db/store.cjs");
10
+
11
+ module.exports = {
12
+ // Bot Settings
13
+ getBotSetting: store.getBotSetting,
14
+ saveBotSetting: store.saveBotSetting,
15
+ deleteBotSetting: store.deleteBotSetting,
16
+ // Group Settings
17
+ getGroupSettings: store.getGroupSettings,
18
+ saveGroupSettings: store.saveGroupSettings,
19
+ getAllGroupSettings: store.getAllGroupSettings,
20
+ // Warnings
21
+ getUserWarnings: store.getUserWarnings,
22
+ saveUserWarnings: store.saveUserWarnings,
23
+ clearUserWarnings: store.clearUserWarnings,
24
+ getAllWarnings: store.getAllWarnings,
25
+ // Todos
26
+ getUserTodos: store.getUserTodos,
27
+ saveUserTodos: store.saveUserTodos,
28
+ // Notes
29
+ saveNote: store.saveNote,
30
+ getNote: store.getNote,
31
+ getAllNotes: store.getAllNotes,
32
+ deleteNote: store.deleteNote,
33
+ getAllNotesFlat: store.getAllNotesFlat,
34
+ // QR Code
35
+ saveQrCode: store.saveQrCode,
36
+ getQrCode: store.getQrCode,
37
+ deleteQrCode: store.deleteQrCode,
38
+ // Forward Scores
39
+ incrementForwardScore: store.incrementForwardScore,
40
+ getForwardScore: store.getForwardScore,
41
+ getTopForwardedMessages: store.getTopForwardedMessages,
42
+ // User Metadata
43
+ saveUserMetadata: store.saveUserMetadata,
44
+ getUserMetadata: store.getUserMetadata,
45
+ isUserOwner: store.isUserOwner,
46
+ isUserBotAdmin: store.isUserBotAdmin,
47
+ getAllOwners: store.getAllOwners,
48
+ getAllBotAdmins: store.getAllBotAdmins,
49
+ setUserRole: store.setUserRole,
50
+ updateUserLastSeen: store.updateUserLastSeen,
51
+ // LID Mapping (Baileys v7)
52
+ storeLidPnMapping: store.storeLidPnMapping,
53
+ storeLidPnMappings: store.storeLidPnMappings,
54
+ getLidForPn: store.getLidForPn,
55
+ getLidsForPns: store.getLidsForPns,
56
+ getPnForLid: store.getPnForLid,
57
+ getAllLidMappings: store.getAllLidMappings,
58
+ // Debts
59
+ addDebt: store.addDebt,
60
+ getDebt: store.getDebt,
61
+ deleteDebt: store.deleteDebt,
62
+ listDebts: store.listDebts,
63
+ getRecentDebts: store.getRecentDebts,
64
+ countDebts: store.countDebts,
65
+ // Dashboard counters + listings
66
+ countGroups: store.countGroups,
67
+ countWarnings: store.countWarnings,
68
+ countTodos: store.countTodos,
69
+ countNotes: store.countNotes,
70
+ getAllTodos: store.getAllTodos,
71
+ getAllUsers: store.getAllUsers,
72
+ countUsers: store.countUsers,
73
+ // Scheduled messages
74
+ getSchedules: store.getSchedules,
75
+ getSchedule: store.getSchedule,
76
+ saveSchedule: store.saveSchedule,
77
+ setScheduleStatus: store.setScheduleStatus,
78
+ deleteSchedule: store.deleteSchedule,
79
+ countSchedules: store.countSchedules,
80
+ };