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,74 @@
1
+ // Everything Levix needs to be a WhatsApp bot.
2
+ //
3
+ // Database, settings, commands, the WhatsApp socket, the scheduler. No HTTP,
4
+ // no Express, no socket.io — those live in bootstrap/panel.js and are entirely
5
+ // optional. `levix headless` runs this file and nothing else, which is what
6
+ // makes "no panel port is opened" a fact rather than a promise.
7
+
8
+ import { createRequire } from "module";
9
+ import { createWhatsAppSocket } from "../core/socket.js";
10
+ import { setupEventListeners } from "../core/events.js";
11
+ import { loadCommands, getLoadedCommands } from "../handlers/command.handler.js";
12
+ import { initStore } from "../db/store.esm.js";
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const logger = require("../utils/logger.cjs");
16
+ const { sink } = require("./events.cjs");
17
+ const { acquireSingleInstanceLock } = require("../config/lock.cjs");
18
+ const { ensureDataDir } = require("../config/paths.cjs");
19
+ const store = require("../db/store.cjs");
20
+ const { initializeScheduledJobs } = require("../../scheduler.cjs");
21
+
22
+ /**
23
+ * Start the bot.
24
+ *
25
+ * @param {object} [options]
26
+ * @param {(sock: object, controls: object) => void} [options.onSocket]
27
+ * Called with every socket, including the ones a reconnect produces — the
28
+ * panel uses it to keep its "unlink" button pointed at the live connection.
29
+ * @returns {Promise<{ sock: object, commandCount: number, wasPaired: boolean }>}
30
+ */
31
+ export async function bootstrapCore({ onSocket } = {}) {
32
+ // Claim the data directory before anything touches the WhatsApp session. A
33
+ // second copy has to fail here, not after it has written to the auth rows
34
+ // the first one is using.
35
+ acquireSingleInstanceLock();
36
+
37
+ // Temp media the commands write while answering lives with the rest of the
38
+ // bot's state, not next to the code.
39
+ ensureDataDir("media");
40
+
41
+ // Whether this install has ever been paired decides what the terminal says
42
+ // next, so read it before the socket has a chance to create credentials.
43
+ const wasPaired = store.hasCredentials();
44
+
45
+ await initStore();
46
+ await loadCommands();
47
+
48
+ const sock = await connect({ onSocket });
49
+
50
+ return { sock, commandCount: getLoadedCommands().size, wasPaired };
51
+ }
52
+
53
+ async function connect({ onSocket, isReconnect = false } = {}) {
54
+ const { sock, saveCreds, clearAll } = await createWhatsAppSocket();
55
+
56
+ setupEventListeners(
57
+ sock,
58
+ saveCreds,
59
+ clearAll,
60
+ // The bot reports into the hub; whoever is listening decides what that
61
+ // means — a socket.io broadcast, a line in the terminal, or nothing.
62
+ sink,
63
+ initializeScheduledJobs,
64
+ async () => {
65
+ logger.info("[Reconnect] Attempting to reconnect to WhatsApp...");
66
+ await connect({ onSocket, isReconnect: true });
67
+ }
68
+ );
69
+
70
+ if (onSocket) onSocket(sock, { clearAll, isReconnect });
71
+
72
+ logger.info("WhatsApp socket initialized successfully");
73
+ return sock;
74
+ }
@@ -0,0 +1,43 @@
1
+ // The seam between the bot and whatever is watching it.
2
+ //
3
+ // The connection handler already reports its state by calling `io.emit(...)`.
4
+ // That was fine while socket.io was the only listener, but headless mode has
5
+ // no socket.io and still needs to know when a QR arrives or the pairing
6
+ // succeeds — and the panel may attach long after the bot has started.
7
+ //
8
+ // So the bot emits into this hub instead, and listeners attach to it:
9
+ //
10
+ // panel -> forwards every event to socket.io
11
+ // headless -> prints the ones a person waiting at a terminal cares about
12
+ //
13
+ // `sink` is shaped like the socket.io server object because that is what
14
+ // `setupEventListeners` is handed. Nothing in src/core had to change.
15
+
16
+ const listeners = new Set();
17
+
18
+ /** Attach a listener. Returns a function that detaches it. */
19
+ function attach(listener) {
20
+ listeners.add(listener);
21
+ return () => listeners.delete(listener);
22
+ }
23
+
24
+ function emit(event, payload) {
25
+ for (const listener of listeners) {
26
+ try {
27
+ listener(event, payload);
28
+ } catch {
29
+ // A broken listener must never take the bot down with it.
30
+ }
31
+ }
32
+ }
33
+
34
+ // The `io`-shaped object handed to the WhatsApp event wiring. `use` and `on`
35
+ // are no-ops: only the panel's real socket.io server implements those, and it
36
+ // wires them itself.
37
+ const sink = {
38
+ emit,
39
+ use() {},
40
+ on() {},
41
+ };
42
+
43
+ module.exports = { sink, attach, emit };
@@ -0,0 +1,97 @@
1
+ // The web control panel: Express, sessions, EJS, socket.io, the routes.
2
+ //
3
+ // Loaded only when the panel is wanted. `levix headless` never imports this
4
+ // file, so none of it is ever constructed — no app, no server, no socket.io
5
+ // namespace, and nothing bound to a port.
6
+
7
+ import { createRequire } from "module";
8
+
9
+ const require = createRequire(import.meta.url);
10
+ const logger = require("../utils/logger.cjs");
11
+ const settings = require("../config/settings.cjs");
12
+ const { attach } = require("./events.cjs");
13
+
14
+ /**
15
+ * Start the panel and bind its port.
16
+ *
17
+ * @param {object} options
18
+ * @param {object} options.core - what bootstrapCore() returned
19
+ * @returns {Promise<{ port: number, host: string, server: object }>}
20
+ */
21
+ export async function bootstrapPanel({ core } = {}) {
22
+ // Required here, not at the top of the module: requiring app.cjs constructs
23
+ // the Express app and the socket.io server, and headless mode must not.
24
+ const {
25
+ app,
26
+ server,
27
+ io,
28
+ dashboardJson,
29
+ requireLoginApi,
30
+ noStore,
31
+ installFinalHandlers,
32
+ } = require("../../app.cjs");
33
+
34
+ const { default: dashboardApiRoutes, setBotInstance, setBotControls } =
35
+ await import("../routes/dashboard.api.esm.js");
36
+
37
+ // Same session as the control panel: these routes hand back groups, debts
38
+ // and warnings with people's numbers in them.
39
+ app.use("/dashboard/api", requireLoginApi, noStore, dashboardJson, dashboardApiRoutes);
40
+ logger.info("Dashboard API routes registered");
41
+
42
+ // Everything the bot reports goes out to the browsers watching the panel.
43
+ attach((event, payload) => io.emit(event, payload));
44
+
45
+ if (core?.sock) {
46
+ setBotInstance(core.sock);
47
+ setBotControls({ clearAll: core.clearAll });
48
+ }
49
+
50
+ // After every route is registered — the 404 and the error handler have to be
51
+ // last, or they swallow what comes after them.
52
+ installFinalHandlers();
53
+
54
+ const port = settings.get("port");
55
+ const host = resolveBindAddress();
56
+
57
+ await new Promise((resolve, reject) => {
58
+ // "EADDRINUSE" plus a stack trace is not an answer anyone can act on, and
59
+ // the overwhelmingly likely cause is a second copy of the bot.
60
+ server.once("error", (error) => {
61
+ if (error.code === "EADDRINUSE") {
62
+ const friendly = new Error(
63
+ `Port ${port} is already taken.\n\n` +
64
+ ` Levix is probably already running — open http://localhost:${port}/ and\n` +
65
+ ` check. If something else owns the port, change it in the control panel\n` +
66
+ ` under Settings -> Server, or start with a different one.`
67
+ );
68
+ friendly.code = "EADDRINUSE";
69
+ return reject(friendly);
70
+ }
71
+ reject(error);
72
+ });
73
+ server.listen(port, host, resolve);
74
+ });
75
+
76
+ return { port, host, server, setBotInstance, setBotControls };
77
+ }
78
+
79
+ /**
80
+ * Which address the panel binds to.
81
+ *
82
+ * The default is every interface, because that is what a Docker publish and a
83
+ * "open it from my phone on the same wifi" both need. Once `levix domain` has
84
+ * actually put a reverse proxy in front, it sets this to 127.0.0.1 so nobody
85
+ * can reach the panel by its raw port and skip the TLS in front of it.
86
+ */
87
+ export function resolveBindAddress() {
88
+ const configured = settings.get("bind_address");
89
+ return configured || "0.0.0.0";
90
+ }
91
+
92
+ /** The address a person should type, given how this install is reachable. */
93
+ export function panelUrl({ port, firstRun = false } = {}) {
94
+ const domain = settings.get("public_domain");
95
+ const base = domain ? `https://${domain}` : `http://localhost:${port}`;
96
+ return firstRun ? `${base}/setup` : base;
97
+ }
package/src/cli.js ADDED
@@ -0,0 +1,153 @@
1
+ // The `levix` command.
2
+ //
3
+ // Kept here rather than in bin/levix.js so the packaged executable can use the
4
+ // same entry point: bin/ is a two-line wrapper, and scripts/build-sea.mjs
5
+ // bundles this file. Every branch imports what it needs and nothing else —
6
+ // `levix where` must not start a database, let alone WhatsApp.
7
+
8
+ import { createRequire } from "node:module";
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const pkg = require("../package.json");
12
+
13
+ const MIN_MAJOR = 24;
14
+
15
+ function checkNode() {
16
+ const major = Number(process.versions.node.split(".")[0]);
17
+ if (major >= MIN_MAJOR) return;
18
+
19
+ console.error(
20
+ [
21
+ "",
22
+ ` ${pkg.name} needs Node ${MIN_MAJOR} or newer (you have ${process.versions.node}).`,
23
+ "",
24
+ " It stores everything in SQLite, which Node ships with itself from",
25
+ ` version ${MIN_MAJOR} on — that is what makes the install a single command`,
26
+ " with nothing to compile.",
27
+ "",
28
+ " Get it from https://nodejs.org (pick the LTS build).",
29
+ "",
30
+ ].join("\n")
31
+ );
32
+ process.exit(1);
33
+ }
34
+
35
+ function help() {
36
+ console.log(
37
+ [
38
+ "",
39
+ ` ${pkg.name} ${pkg.version} — ${pkg.description}`,
40
+ "",
41
+ " Usage",
42
+ " levix start Levix with the web panel",
43
+ " levix headless start Levix with no web UI at all",
44
+ " levix where print the data directory",
45
+ " levix reset-password reset the panel password",
46
+ " levix domain [name] point a domain at Levix (may need sudo)",
47
+ "",
48
+ " Options",
49
+ " --data <dir> keep the database and files somewhere else",
50
+ " --headless same as the headless command",
51
+ " --no-open don't open a browser on start",
52
+ " --version, --help",
53
+ "",
54
+ " Everything else is configured from the control panel itself,",
55
+ " which prints its address when the bot starts.",
56
+ "",
57
+ ].join("\n")
58
+ );
59
+ }
60
+
61
+ // The command is the first argument that isn't a flag or a flag's value.
62
+ // `levix --data /var/lib/levix where` has to run `where`, not start the bot —
63
+ // which is exactly how someone debugging a systemd install types it.
64
+ const FLAGS_WITH_VALUES = new Set(["--data"]);
65
+
66
+ export function parseArgv(argv) {
67
+ const flags = new Set();
68
+ const positional = [];
69
+
70
+ for (let i = 2; i < argv.length; i += 1) {
71
+ const arg = argv[i];
72
+ if (FLAGS_WITH_VALUES.has(arg)) {
73
+ i += 1; // skip its value
74
+ continue;
75
+ }
76
+ if (arg.startsWith("-")) {
77
+ flags.add(arg.split("=")[0]);
78
+ continue;
79
+ }
80
+ positional.push(arg);
81
+ }
82
+
83
+ return { command: positional[0] ?? null, positional, flags };
84
+ }
85
+
86
+ export async function main(argv = process.argv) {
87
+ const { command, positional, flags } = parseArgv(argv);
88
+
89
+ if (flags.has("--help") || flags.has("-h") || command === "help") {
90
+ help();
91
+ return 0;
92
+ }
93
+
94
+ if (flags.has("--version") || flags.has("-v")) {
95
+ console.log(pkg.version);
96
+ return 0;
97
+ }
98
+
99
+ checkNode();
100
+
101
+ switch (command) {
102
+ case "where": {
103
+ const { DATA_DIR } = await import("./config/paths.cjs");
104
+ console.log(DATA_DIR);
105
+ return 0;
106
+ }
107
+
108
+ case "reset-password": {
109
+ const store = (await import("./db/store.cjs")).default;
110
+ const { close } = await import("./db/db.cjs");
111
+ store.deleteBotSetting("auth:password");
112
+ close();
113
+ console.log(
114
+ "\n Password cleared. Start the bot and open the panel to choose a new one.\n"
115
+ );
116
+ return 0;
117
+ }
118
+
119
+ case "domain": {
120
+ const { runDomainCommand } = await import("./domain/command.js");
121
+ return runDomainCommand({
122
+ domain: positional[1] ?? null,
123
+ assumeYes: flags.has("--yes") || flags.has("-y"),
124
+ });
125
+ }
126
+
127
+ case "headless":
128
+ case null:
129
+ case undefined: {
130
+ const { start } = await import("./index.js");
131
+ await start({
132
+ headless: command === "headless" || flags.has("--headless"),
133
+ open: !flags.has("--no-open"),
134
+ });
135
+ // The bot keeps the process alive on its own from here.
136
+ return null;
137
+ }
138
+
139
+ default:
140
+ console.error(`\n Unknown command: ${command}\n`);
141
+ help();
142
+ return 1;
143
+ }
144
+ }
145
+
146
+ /** Entry point for bin/levix.js and for the packaged executable. */
147
+ export async function run(argv = process.argv) {
148
+ const code = await main(argv);
149
+ // `null` means "the bot is running now" — exiting would kill it.
150
+ if (code !== null) process.exit(code);
151
+ }
152
+
153
+ export default run;
@@ -0,0 +1,84 @@
1
+ const {
2
+ scheduleNewJob,
3
+ saveScheduledJob,
4
+ } = require("../../scheduler.cjs");
5
+ const { v4: uuidv4 } = require("uuid"); // Use UUID for unique IDs
6
+ const { defaultTimezone } = require("../utils/datetime.cjs");
7
+
8
+ module.exports = {
9
+ name: "autoschedule",
10
+ description: "Schedules a recurring message (daily, weekly).",
11
+ usage: "autoschedule daily <HH:mm> <الرسالة>\nautoschedule weekly <اليوم> <HH:mm> <الرسالة>",
12
+ chat: "all",
13
+
14
+ async execute(sock, msg, args) {
15
+ const creatorJid = msg.key.participant || msg.key.remoteJid;
16
+ const targetJid = msg.key.remoteJid;
17
+
18
+ const type = args[0]?.toLowerCase();
19
+ const time = args[1];
20
+ const message = args.slice(2).join(" ");
21
+
22
+ if (!type || !time || !message) {
23
+ return await sock.sendMessage(creatorJid, {
24
+ text: "الصيغة غير صحيحة. استخدم:\n`!autoschedule daily HH:mm رسالتك`\n`!autoschedule weekly day HH:mm رسالتك`",
25
+ });
26
+ }
27
+
28
+ // من غير الفحص ده أي كتابة غلط ("abc:xyz") بتتحوّل لكرون باظ يتخزّن في
29
+ // الملف، وبعدين كل تشغيل للبوت بيقع وهو بيحاول يجدوله.
30
+ const timeMatch = /^(\d{1,2}):(\d{2})$/.exec(String(time).trim());
31
+ const hour = timeMatch && Number(timeMatch[1]);
32
+ const minute = timeMatch && Number(timeMatch[2]);
33
+
34
+ if (!timeMatch || hour > 23 || minute > 59) {
35
+ return await sock.sendMessage(creatorJid, {
36
+ text: "الوقت غير صالح. استخدم صيغة `HH:mm` (مثال: `09:30`).",
37
+ });
38
+ }
39
+
40
+ let cronString = "";
41
+
42
+ if (type === "daily") {
43
+ cronString = `${minute} ${hour} * * *`;
44
+ } else if (type === "weekly") {
45
+ // This part can be enhanced later to support specific days like 'monday'
46
+ // For now, let's keep it simple. Example: !autoschedule weekly 1 10:30 "..." for Monday 10:30
47
+ const dayOfWeek = args[1];
48
+ const weeklyTime = args[2];
49
+ const weeklyMessage = args.slice(3).join(" ");
50
+ // ... logic to build weekly cron string ...
51
+ return await sock.sendMessage(creatorJid, {
52
+ text: "جدولة أسبوعية قيد التطوير.",
53
+ });
54
+ } else {
55
+ return await sock.sendMessage(creatorJid, {
56
+ text: "النوع غير مدعوم. استخدم: `daily`",
57
+ });
58
+ }
59
+
60
+ const newJob = {
61
+ id: uuidv4(),
62
+ type: "recurring",
63
+ cronString: cronString,
64
+ message: message,
65
+ targetJid: targetJid,
66
+ creatorJid: creatorJid,
67
+ status: "active",
68
+ };
69
+
70
+ // الجدولة الأول: جوب مش قادرين نجدوله ما يتخزّنش في الملف أصلاً.
71
+ if (!scheduleNewJob(sock, newJob)) {
72
+ return await sock.sendMessage(creatorJid, {
73
+ text: "معرفتش أجدول الرسالة دي. راجع الوقت وجرب تاني.",
74
+ });
75
+ }
76
+
77
+ saveScheduledJob(newJob);
78
+
79
+ await sock.sendMessage(creatorJid, {
80
+ text: `✅ تم جدولة رسالة يومية بنجاح في الساعة ${time} (${defaultTimezone()})`,
81
+ });
82
+ },
83
+ };
84
+ // Note: You need to run `npm install uuid` for this command.
@@ -0,0 +1,116 @@
1
+ // file: /commands/blacklist.js (Corrected Logic)
2
+ const { getGroupSettings, saveGroupSettings } = require("../utils/storage.cjs");
3
+ const logger = require("../utils/logger.cjs");
4
+ const normalizeJid = require("../utils/normalizeJid.esm.js").default;
5
+
6
+ module.exports = {
7
+ name: "blacklist",
8
+ aliases: ["unblacklist", "block", "unblock"], // We keep aliases here for a reason
9
+ description: "Manages the group's user blacklist.",
10
+ usage: "blacklist [add|remove|list] [@عضو|رقم]",
11
+ chat: "group",
12
+ userAdminRequired: true,
13
+
14
+ async execute(sock, msg, args, body, groupMetadata) {
15
+ const groupId = msg.key.remoteJid;
16
+ const command = body.slice(1).trim().split(/ +/)[0].toLowerCase();
17
+
18
+ // --- New, smarter action parser ---
19
+ let action;
20
+ // Explicit sub-command like !blacklist list
21
+ if (args[0] && ["add", "remove", "list"].includes(args[0].toLowerCase())) {
22
+ action = args[0].toLowerCase();
23
+ args.shift(); // Remove the action from args, so the rest are the target
24
+ }
25
+ // Action inferred from alias like !block or !unblock
26
+ else if (["blacklist", "block"].includes(command)) {
27
+ action = "add";
28
+ } else if (["unblacklist", "unblock"].includes(command)) {
29
+ action = "remove";
30
+ } else {
31
+ action = "list"; // Default action is to list
32
+ }
33
+
34
+ try {
35
+ const settings = getGroupSettings(groupId);
36
+ if (!settings.blacklist) settings.blacklist = [];
37
+
38
+ // For list action, no target is needed
39
+ if (action === "list") {
40
+ if (settings.blacklist.length === 0)
41
+ return await sock.sendMessage(groupId, {
42
+ text: "القائمة السوداء فارغة حاليًا.",
43
+ });
44
+
45
+ let listReply = "*🚫 قائمة المستخدمين المحظورين من البوت:*\n\n";
46
+ settings.blacklist.forEach((jid) => {
47
+ listReply += `» @${jid.split("@")[0]}\n`;
48
+ });
49
+ return await sock.sendMessage(groupId, {
50
+ text: listReply,
51
+ mentions: settings.blacklist,
52
+ });
53
+ }
54
+
55
+ // For add/remove actions, a target is required
56
+ const targetJid =
57
+ msg.message?.extendedTextMessage?.contextInfo?.mentionedJid?.[0] ||
58
+ (args[0] ? `${args[0].replace(/\D/g, "")}@s.whatsapp.net` : null);
59
+
60
+ if (!targetJid)
61
+ return await sock.sendMessage(groupId, {
62
+ text: `لتنفيذ هذا الإجراء، قم بعمل منشن للعضو أو كتابة رقمه.`,
63
+ });
64
+
65
+ const normalizedTargetJid = normalizeJid(targetJid);
66
+
67
+ if (action === "add") {
68
+ const targetUser = groupMetadata.participants.find(
69
+ (p) => p.id === normalizedTargetJid
70
+ );
71
+ if (targetUser?.admin)
72
+ return await sock.sendMessage(groupId, {
73
+ text: "لا يمكن حظر المشرفين.",
74
+ });
75
+ if (settings.blacklist.includes(normalizedTargetJid))
76
+ return await sock.sendMessage(groupId, {
77
+ text: `العضو @${normalizedTargetJid.split("@")[0]} محظور بالفعل.`,
78
+ mentions: [normalizedTargetJid],
79
+ });
80
+
81
+ settings.blacklist.push(normalizedTargetJid);
82
+ await sock.sendMessage(groupId, {
83
+ text: `🚫 تم إضافة @${
84
+ normalizedTargetJid.split("@")[0]
85
+ } إلى القائمة السوداء بنجاح.`,
86
+ mentions: [normalizedTargetJid],
87
+ });
88
+ } else if (action === "remove") {
89
+ if (!settings.blacklist.includes(normalizedTargetJid))
90
+ return await sock.sendMessage(groupId, {
91
+ text: `العضو @${
92
+ normalizedTargetJid.split("@")[0]
93
+ } ليس في القائمة السوداء أصلاً.`,
94
+ mentions: [normalizedTargetJid],
95
+ });
96
+
97
+ settings.blacklist = settings.blacklist.filter(
98
+ (jid) => jid !== normalizedTargetJid
99
+ );
100
+ await sock.sendMessage(groupId, {
101
+ text: `✅ تم إزالة @${
102
+ normalizedTargetJid.split("@")[0]
103
+ } من القائمة السوداء.`,
104
+ mentions: [normalizedTargetJid],
105
+ });
106
+ }
107
+
108
+ saveGroupSettings(groupId, settings);
109
+ } catch (error) {
110
+ logger.error(
111
+ { err: error, command: "blacklist" },
112
+ "Error in !blacklist command"
113
+ );
114
+ }
115
+ },
116
+ };
@@ -0,0 +1,70 @@
1
+ // file: /commands/block.js (Upgraded with private chat context)
2
+ const logger = require("../utils/logger.cjs");
3
+ const { isOwnerJidSync } = require("../utils/permissions.cjs");
4
+ const normalizeJid = require("../utils/normalizeJid.esm.js");
5
+
6
+ module.exports = {
7
+ name: "block",
8
+ description: "Blocks a user from contacting the bot on WhatsApp.",
9
+ usage: "block [@عضو|رقم] (أو رد على رسالته)",
10
+ chat: "all",
11
+
12
+ async execute(sock, msg, args) {
13
+ const remoteJid = msg.key.remoteJid;
14
+ const isGroup = remoteJid.endsWith("@g.us");
15
+ let targetJid;
16
+
17
+ // --- NEW: Smart Target Identification ---
18
+ // Case 1: The command is '!block' with no arguments in a private chat
19
+ if (!isGroup && args.length === 0) {
20
+ targetJid = remoteJid;
21
+ }
22
+ // Case 2: A target is specified (mention, reply, or number)
23
+ else {
24
+ const mentionedJid =
25
+ msg.message?.extendedTextMessage?.contextInfo?.mentionedJid?.[0];
26
+ const repliedToJid =
27
+ msg.message?.extendedTextMessage?.contextInfo?.participant;
28
+ const numberArg = args[0];
29
+
30
+ if (mentionedJid) {
31
+ targetJid = mentionedJid;
32
+ } else if (repliedToJid) {
33
+ targetJid = repliedToJid;
34
+ } else if (numberArg) {
35
+ targetJid = `${numberArg.replace(/\D/g, "")}@s.whatsapp.net`;
36
+ } else {
37
+ return await sock.sendMessage(remoteJid, {
38
+ text: "لكي يعمل هذا الأمر، استخدمه في شات خاص بدون وسائط، أو قم بعمل منشن/رد/كتابة رقم المستخدم.",
39
+ });
40
+ }
41
+ }
42
+
43
+ const normalizedTargetJid = normalizeJid(targetJid);
44
+
45
+ // Safety Check
46
+ if (isOwnerJidSync(normalizedTargetJid)) {
47
+ return await sock.sendMessage(remoteJid, {
48
+ text: "لا يمكنك حظر مالك البوت.",
49
+ });
50
+ }
51
+
52
+ try {
53
+ // ✅ --- THE FIX IS HERE ---
54
+ await sock.updateBlockStatus(normalizedTargetJid, "block");
55
+
56
+ // const successMsg = `🚫 تم حظر @${
57
+ // normalizedTargetJid.split("@")[0]
58
+ // } بنجاح.`;
59
+ // await sock.sendMessage(remoteJid, {
60
+ // text: successMsg,
61
+ // mentions: [normalizedTargetJid],
62
+ // });
63
+ } catch (error) {
64
+ logger.error({ err: error, command: "block" }, "Error in !block command");
65
+ await sock.sendMessage(remoteJid, {
66
+ text: "حدث خطأ أثناء محاولة حظر المستخدم.",
67
+ });
68
+ }
69
+ },
70
+ };