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,155 @@
1
+ // commands/mod.js
2
+
3
+ const { getGroupSettings, saveGroupSettings } = require("../utils/storage.cjs");
4
+ const {
5
+ isOwnerJidSync,
6
+ isAdminInGroupSync,
7
+ } = require("../utils/permissions.cjs");
8
+
9
+ // الهيكل الافتراضي لإعدادات المراقبة
10
+ const defaultModSettings = {
11
+ antiLink: { enabled: false },
12
+ antiSpam: { enabled: true }, // نفعّله افتراضيًا لأنه مهم
13
+ forbiddenWords: {
14
+ enabled: false,
15
+ list: [],
16
+ },
17
+ };
18
+
19
+ module.exports = {
20
+ name: "mod",
21
+ aliases: ["moderation", "settings"],
22
+ description: "التحكم في إعدادات المراقبة للجروب.",
23
+ usage: "mod status\nmod <enable|disable> <antiLink|antiSpam|forbiddenWords>\nmod words <add|remove> <الكلمة>\nmod words list",
24
+ chat: "group", // يعمل في الجروبات فقط
25
+ botAdminRequired: false, // لا يتطلب أن يكون البوت مشرفًا لتغيير الإعدادات
26
+
27
+ async execute(sock, msg, args) {
28
+ const groupId = msg.key.remoteJid;
29
+ const senderId = msg.key.participant;
30
+
31
+ // التحقق من صلاحيات المشرف (LID-aware via central module). Owners can
32
+ // also drive moderation settings.
33
+ const groupMetadata = await sock.groupMetadata(groupId);
34
+ const senderIsAdmin = isAdminInGroupSync(groupMetadata, senderId);
35
+ const isOwner = msg.key.fromMe || isOwnerJidSync(senderId);
36
+ if (!senderIsAdmin && !isOwner) {
37
+ return sock.sendMessage(
38
+ groupId,
39
+ { text: "🚫 هذا الأمر للمشرفين فقط." },
40
+ { quoted: msg }
41
+ );
42
+ }
43
+
44
+ const command = args[0]?.toLowerCase();
45
+ const feature = args[1]?.toLowerCase();
46
+
47
+ // الحصول على الإعدادات الحالية للجروب أو إنشاء إعدادات افتراضية
48
+ let settings = getGroupSettings(groupId) || {};
49
+ if (!settings.moderation) {
50
+ settings.moderation = JSON.parse(JSON.stringify(defaultModSettings)); // نسخة عميقة
51
+ }
52
+
53
+ const modSettings = settings.moderation;
54
+
55
+ switch (command) {
56
+ case "enable":
57
+ if (!modSettings.hasOwnProperty(feature)) {
58
+ return sock.sendMessage(
59
+ groupId,
60
+ { text: `⚠️ الميزة "${feature}" غير موجودة.` },
61
+ { quoted: msg }
62
+ );
63
+ }
64
+ modSettings[feature].enabled = true;
65
+ saveGroupSettings(groupId, settings);
66
+ await sock.sendMessage(
67
+ groupId,
68
+ { text: `✅ تم تفعيل ميزة *${feature}*.` },
69
+ { quoted: msg }
70
+ );
71
+ break;
72
+
73
+ case "disable":
74
+ if (!modSettings.hasOwnProperty(feature)) {
75
+ return sock.sendMessage(
76
+ groupId,
77
+ { text: `⚠️ الميزة "${feature}" غير موجودة.` },
78
+ { quoted: msg }
79
+ );
80
+ }
81
+ modSettings[feature].enabled = false;
82
+ saveGroupSettings(groupId, settings);
83
+ await sock.sendMessage(
84
+ groupId,
85
+ { text: `❌ تم تعطيل ميزة *${feature}*.` },
86
+ { quoted: msg }
87
+ );
88
+ break;
89
+
90
+ case "status":
91
+ let statusText = "📊 *حالة إعدادات المراقبة* 📊\n\n";
92
+ for (const key in modSettings) {
93
+ const status = modSettings[key].enabled ? "✅ مفعل" : "❌ معطل";
94
+ statusText += `› *${key}*: ${status}\n`;
95
+ }
96
+ statusText += `\n- لعرض قائمة الكلمات الممنوعة، اكتب: \`!mod words list\``;
97
+ await sock.sendMessage(groupId, { text: statusText }, { quoted: msg });
98
+ break;
99
+
100
+ case "words":
101
+ const wordAction = args[1]?.toLowerCase();
102
+ const word = args.slice(2).join(" ").toLowerCase();
103
+
104
+ if (wordAction === "add") {
105
+ if (!word)
106
+ return sock.sendMessage(groupId, {
107
+ text: "الرجاء كتابة الكلمة التي تريد إضافتها.",
108
+ });
109
+ modSettings.forbiddenWords.list.push(word);
110
+ saveGroupSettings(groupId, settings);
111
+ await sock.sendMessage(groupId, {
112
+ text: `✅ تم إضافة "${word}" إلى قائمة الكلمات الممنوعة.`,
113
+ });
114
+ } else if (wordAction === "remove") {
115
+ if (!word)
116
+ return sock.sendMessage(groupId, {
117
+ text: "الرجاء كتابة الكلمة التي تريد إزالتها.",
118
+ });
119
+ modSettings.forbiddenWords.list =
120
+ modSettings.forbiddenWords.list.filter((w) => w !== word);
121
+ saveGroupSettings(groupId, settings);
122
+ await sock.sendMessage(groupId, {
123
+ text: `🗑️ تم إزالة "${word}" من القائمة.`,
124
+ });
125
+ } else if (wordAction === "list") {
126
+ let listText = "🚫 *قائمة الكلمات الممنوعة* 🚫\n\n";
127
+ if (modSettings.forbiddenWords.list.length === 0) {
128
+ listText += "القائمة فارغة حاليًا.";
129
+ } else {
130
+ modSettings.forbiddenWords.list.forEach((w, i) => {
131
+ listText += `${i + 1}. ${w}\n`;
132
+ });
133
+ }
134
+ await sock.sendMessage(groupId, { text: listText });
135
+ } else {
136
+ await sock.sendMessage(groupId, {
137
+ text: "استخدام خاطئ. الأوامر المتاحة: `add`, `remove`, `list`",
138
+ });
139
+ }
140
+ break;
141
+
142
+ default:
143
+ let helpText = "🤖 *أوامر التحكم في المراقبة*\n\n";
144
+ helpText +=
145
+ "• `!mod enable <feature>`\nلتفعيل ميزة (antilink, antispam, forbiddenwords)\n\n";
146
+ helpText += "• `!mod disable <feature>`\nلتعطيل ميزة.\n\n";
147
+ helpText += "• `!mod status`\nلعرض حالة الميزات الحالية.\n\n";
148
+ helpText += "• `!mod words add <word>`\nلإضافة كلمة ممنوعة.\n\n";
149
+ helpText += "• `!mod words remove <word>`\nلإزالة كلمة ممنوعة.\n\n";
150
+ helpText += "• `!mod words list`\nلعرض قائمة الكلمات الممنوعة.";
151
+ await sock.sendMessage(groupId, { text: helpText }, { quoted: msg });
152
+ break;
153
+ }
154
+ },
155
+ };
@@ -0,0 +1,28 @@
1
+ // file: /commands/notes.js
2
+ const { getAllNotes } = require("../utils/storage.cjs");
3
+
4
+ module.exports = {
5
+ name: "notes",
6
+ description: "Lists all saved note keywords for the group.",
7
+ usage: "notes",
8
+ chat: "all",
9
+
10
+ async execute(sock, msg) {
11
+ const groupId = msg.key.remoteJid;
12
+ const notes = getAllNotes(groupId);
13
+ const groupNotes = notes[groupId] || {};
14
+ const keywords = Object.keys(groupNotes);
15
+
16
+ if (keywords.length === 0) {
17
+ return await sock.sendMessage(groupId, {
18
+ text: "لا توجد ملاحظات محفوظة في هذا الجروب.",
19
+ });
20
+ }
21
+
22
+ let reply = "*🔑 الكلمات المفتاحية للملاحظات المحفوظة:*\n\n";
23
+ reply += keywords.map((kw) => `\`#${kw}\``).join("\n");
24
+ reply += "\n\nللحصول على ملاحظة، استخدم: `!note #keyword`";
25
+
26
+ await sock.sendMessage(groupId, { text: reply });
27
+ },
28
+ };
@@ -0,0 +1,236 @@
1
+ // Bot-level roles: owner and admin.
2
+ //
3
+ // Two ways to hand out privileges, as the operator asked:
4
+ // * manually — `!perm add admin @حد` (or a reply, or a bare number),
5
+ // * by asking the AI — the agent exposes grant_role/revoke_role and calls
6
+ // into the very same module (utils/permissions.esm.js).
7
+ //
8
+ // owner : everything, including OWNER_ONLY commands.
9
+ // admin : counts as an admin in every chat (group admin OR this role satisfies
10
+ // ADMINS_ONLY / ADMINS_OWNER) — but never OWNER_ONLY.
11
+ //
12
+ // Roles live in `user_metadata` (is_owner / is_admin) and in the in-memory
13
+ // roster, so a grant is effective on the very next message — no restart.
14
+
15
+ const {
16
+ grantRole,
17
+ revokeRole,
18
+ listRoles,
19
+ isOwnerJidSync,
20
+ } = require("../utils/permissions.cjs");
21
+ const { sendBotMessage } = require("../utils/sendBotMessage.cjs");
22
+ const normalizeJid = require("../utils/normalizeJid.cjs");
23
+ const logger = require("../utils/logger.cjs");
24
+
25
+ const ROLE_WORDS = {
26
+ owner: "owner",
27
+ owners: "owner",
28
+ مالك: "owner",
29
+ المالك: "owner",
30
+ admin: "admin",
31
+ admins: "admin",
32
+ مشرف: "admin",
33
+ ادمن: "admin",
34
+ أدمن: "admin",
35
+ };
36
+
37
+ const ADD_WORDS = new Set(["add", "grant", "set", "give", "اضف", "أضف", "ضيف"]);
38
+ const DEL_WORDS = new Set([
39
+ "remove",
40
+ "revoke",
41
+ "del",
42
+ "delete",
43
+ "drop",
44
+ "شيل",
45
+ "احذف",
46
+ "امسح",
47
+ ]);
48
+ const LIST_WORDS = new Set(["list", "ls", "show", "قائمة", "عرض", "الكل"]);
49
+
50
+ function senderOf(msg) {
51
+ return msg.key.remoteJid?.endsWith("@g.us")
52
+ ? msg.key.participant || msg.key.participantAlt
53
+ : msg.key.remoteJid;
54
+ }
55
+
56
+ /**
57
+ * Work out who the command is about: a mention, a reply, a bare number, or
58
+ * "me". Returns a normalized JID/LID or null.
59
+ */
60
+ function resolveTarget(msg, args) {
61
+ const contextInfo =
62
+ msg.message?.extendedTextMessage?.contextInfo ||
63
+ msg.message?.imageMessage?.contextInfo ||
64
+ msg.message?.videoMessage?.contextInfo ||
65
+ null;
66
+
67
+ const mentioned = contextInfo?.mentionedJid?.[0];
68
+ if (mentioned) return normalizeJid(mentioned);
69
+
70
+ const quotedParticipant =
71
+ contextInfo?.participant || contextInfo?.participantAlt;
72
+ if (quotedParticipant) return normalizeJid(quotedParticipant);
73
+
74
+ for (const arg of args) {
75
+ const value = String(arg);
76
+ if (/^(me|انا|أنا|نفسي)$/i.test(value)) return normalizeJid(senderOf(msg));
77
+ if (value.includes("@")) return normalizeJid(value);
78
+ const digits = (value.match(/\d{6,}/) || [])[0];
79
+ if (digits) return `${digits}@s.whatsapp.net`;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ function labelFor(user) {
85
+ const name = user.displayName ? `${user.displayName} — ` : "";
86
+ return `${name}${user.phone || user.jid || user.lid}`;
87
+ }
88
+
89
+ // permissions.cjs bridges to an ESM module, so every non-`Sync` helper here
90
+ // is async — awaiting them is not optional.
91
+ async function rolesReport() {
92
+ const { owners, admins, bootstrapOwners, bootstrapAdmins } = await listRoles();
93
+ const lines = ["*🔑 صلاحيات البوت*", ""];
94
+
95
+ lines.push(`*👑 المالكين (${owners.length}):*`);
96
+ lines.push(
97
+ owners.length ? owners.map((o) => `• ${labelFor(o)}`).join("\n") : "• (مفيش)"
98
+ );
99
+
100
+ lines.push("", `*🛡️ الأدمنز (${admins.length}):*`);
101
+ lines.push(
102
+ admins.length ? admins.map((a) => `• ${labelFor(a)}`).join("\n") : "• (مفيش)"
103
+ );
104
+
105
+ const envOnly = [
106
+ ...bootstrapOwners.filter(
107
+ (jid) => !owners.some((o) => o.jid === jid || o.lid === jid)
108
+ ),
109
+ ...bootstrapAdmins.filter(
110
+ (jid) => !admins.some((a) => a.jid === jid || a.lid === jid)
111
+ ),
112
+ ];
113
+ if (envOnly.length) {
114
+ lines.push(
115
+ "",
116
+ `_من الإعدادات (env/config): ${[...new Set(envOnly)].length} إدخال._`
117
+ );
118
+ }
119
+
120
+ return lines.join("\n");
121
+ }
122
+
123
+ module.exports = {
124
+ name: "perm",
125
+ aliases: ["perms", "owner", "admin", "role", "roles", "صلاحيات"],
126
+ description:
127
+ "إدارة صلاحيات البوت: إضافة/إزالة مالك أو أدمن، وعرض القائمة الحالية.",
128
+ usage:
129
+ "perm list\nperm add admin @عضو\nperm add owner <رقم>\nperm remove admin @عضو",
130
+ chat: "all",
131
+
132
+ async execute(sock, msg, args) {
133
+ const jid = msg.key.remoteJid;
134
+ const action = String(args[0] || "").toLowerCase();
135
+
136
+ if (!action || LIST_WORDS.has(action)) {
137
+ return sendBotMessage(
138
+ sock,
139
+ jid,
140
+ { text: await rolesReport() },
141
+ { replyTo: msg }
142
+ );
143
+ }
144
+
145
+ const isAdd = ADD_WORDS.has(action);
146
+ const isRemove = DEL_WORDS.has(action);
147
+ if (!isAdd && !isRemove) {
148
+ return sendBotMessage(
149
+ sock,
150
+ jid,
151
+ {
152
+ text:
153
+ "استخدام غير صحيح.\n\n" +
154
+ "`!perm list` — القائمة الحالية\n" +
155
+ "`!perm add admin @عضو` — تعيين أدمن\n" +
156
+ "`!perm add owner <رقم>` — تعيين مالك\n" +
157
+ "`!perm remove admin @عضو` — سحب الصلاحية",
158
+ },
159
+ { replyTo: msg }
160
+ );
161
+ }
162
+
163
+ // Role word is optional and can sit anywhere; default to admin (the safer
164
+ // of the two) so `!perm add @حد` doesn't hand out ownership by accident.
165
+ let role = "admin";
166
+ for (const arg of args.slice(1)) {
167
+ const word = ROLE_WORDS[String(arg).toLowerCase()];
168
+ if (word) {
169
+ role = word;
170
+ break;
171
+ }
172
+ }
173
+
174
+ const target = resolveTarget(msg, args.slice(1));
175
+ if (!target) {
176
+ return sendBotMessage(
177
+ sock,
178
+ jid,
179
+ {
180
+ text:
181
+ "محتاج تحدد الشخص: منشن (@) أو رد على رسالته أو اكتب رقمه.\n" +
182
+ "مثال: `!perm add admin @عضو`",
183
+ },
184
+ { replyTo: msg }
185
+ );
186
+ }
187
+
188
+ // Only an owner may create another owner. (Reaching this command at all
189
+ // already requires OWNER_ONLY by default — this is the belt to that
190
+ // brace, and it also guards direct calls from elsewhere.)
191
+ const sender = senderOf(msg);
192
+ if (role === "owner" && !msg.key.fromMe && !isOwnerJidSync(sender)) {
193
+ return sendBotMessage(
194
+ sock,
195
+ jid,
196
+ { text: "🚫 تعيين مالك جديد للمالك فقط." },
197
+ { replyTo: msg }
198
+ );
199
+ }
200
+
201
+ try {
202
+ const record = isAdd
203
+ ? await grantRole(target, role)
204
+ : await revokeRole(target, role);
205
+ const who = record ? labelFor(record) : target;
206
+ const roleLabel = role === "owner" ? "مالك 👑" : "أدمن 🛡️";
207
+
208
+ logger.info(
209
+ { target, role, granted: isAdd, by: sender },
210
+ "[perm] role change"
211
+ );
212
+
213
+ return sendBotMessage(
214
+ sock,
215
+ jid,
216
+ {
217
+ text: isAdd
218
+ ? `✅ تم تعيين *${who}* كـ *${roleLabel}*.`
219
+ : `✅ تم سحب صلاحية *${roleLabel}* من *${who}*.`,
220
+ mentions: target.includes("@") ? [target] : undefined,
221
+ },
222
+ { replyTo: msg }
223
+ );
224
+ } catch (error) {
225
+ logger.error({ err: error }, "[perm] role change failed");
226
+ return sendBotMessage(
227
+ sock,
228
+ jid,
229
+ {
230
+ text: `❌ *مقدرتش أعدّل الصلاحية*\n\n*التفاصيل:* ${error.message}`,
231
+ },
232
+ { replyTo: msg }
233
+ );
234
+ }
235
+ },
236
+ };
@@ -0,0 +1,17 @@
1
+ // file: /commands/ping.js
2
+
3
+ module.exports = {
4
+ name: "ping",
5
+ description: "A simple command to check if the bot is responsive.",
6
+ usage: "ping",
7
+ chat: "all", // <-- الخاصية الجديدة. يمكن أن تكون 'group' أو 'private'
8
+
9
+ async execute(sock, msg) {
10
+ // The bot will reply with "Pong!" and quote the original message
11
+ await sock.sendMessage(
12
+ msg.key.remoteJid,
13
+ { text: "Pong! 🏓" },
14
+ { quoted: msg }
15
+ );
16
+ },
17
+ };
@@ -0,0 +1,80 @@
1
+ // استفتاء — !poll
2
+ //
3
+ // !poll نروح فين النهاردة؟ | السينما | البحر | نقعد في البيت
4
+ // !poll --multi اختر أكلك | كشري | فراخ | بيتزا (يسمح بأكتر من اختيار)
5
+ //
6
+ // Sends a native WhatsApp poll (not a text list), so members vote with a tap
7
+ // and WhatsApp counts the results.
8
+
9
+ const { sendBotMessage } = require("../utils/sendBotMessage.cjs");
10
+
11
+ const MIN_OPTIONS = 2;
12
+ const MAX_OPTIONS = 12; // WhatsApp's own ceiling
13
+ const MAX_QUESTION_CHARS = 255;
14
+ const MAX_OPTION_CHARS = 100;
15
+
16
+ const MULTI_FLAGS = new Set(["--multi", "-m", "متعدد"]);
17
+
18
+ const HELP_TEXT =
19
+ "📊 *استفتاء*\n\n" +
20
+ "*الاستخدام:*\n" +
21
+ "`!poll <السؤال> | <خيار> | <خيار> ...`\n\n" +
22
+ "*خيارات:*\n" +
23
+ "`--multi` يسمح باختيار أكتر من إجابة\n\n" +
24
+ "*مثال:*\n" +
25
+ "`!poll نروح فين؟ | السينما | البحر | البيت`\n\n" +
26
+ `_من ${MIN_OPTIONS} لـ ${MAX_OPTIONS} خيارات._`;
27
+
28
+ module.exports = {
29
+ name: "poll",
30
+ aliases: ["vote", "استفتاء", "تصويت"],
31
+ description: "ينشئ استفتاء (تصويت) داخل الشات.",
32
+ usage: "poll <السؤال> | <خيار1> | <خيار2> [| ...] [--multi]",
33
+ chat: "all",
34
+
35
+ async execute(sock, msg, args) {
36
+ const chatId = msg.key.remoteJid;
37
+ const reply = (text) => sendBotMessage(sock, chatId, { text }, { replyTo: msg });
38
+
39
+ let multi = false;
40
+ const rest = args.filter((arg) => {
41
+ if (MULTI_FLAGS.has(arg.toLowerCase())) {
42
+ multi = true;
43
+ return false;
44
+ }
45
+ return true;
46
+ });
47
+
48
+ const parts = rest
49
+ .join(" ")
50
+ .split(/\s*[|،]\s*/)
51
+ .map((part) => part.trim())
52
+ .filter(Boolean);
53
+
54
+ if (parts.length < MIN_OPTIONS + 1) return reply(HELP_TEXT);
55
+
56
+ const question = parts[0].slice(0, MAX_QUESTION_CHARS);
57
+ // WhatsApp rejects a poll with duplicate options.
58
+ const options = [...new Set(parts.slice(1).map((o) => o.slice(0, MAX_OPTION_CHARS)))];
59
+
60
+ if (options.length < MIN_OPTIONS) {
61
+ return reply("❌ محتاج خيارين مختلفين على الأقل.");
62
+ }
63
+ if (options.length > MAX_OPTIONS) {
64
+ return reply(`❌ الحد الأقصى ${MAX_OPTIONS} خيار (انت كاتب ${options.length}).`);
65
+ }
66
+
67
+ return sendBotMessage(
68
+ sock,
69
+ chatId,
70
+ {
71
+ poll: {
72
+ name: question,
73
+ values: options,
74
+ selectableCount: multi ? options.length : 1,
75
+ },
76
+ },
77
+ { replyTo: msg }
78
+ );
79
+ },
80
+ };
@@ -0,0 +1,64 @@
1
+ // Simple time formatter (inline replacement for deleted formatTime module)
2
+ function formatTime12Hour(time24) {
3
+ const [hours, minutes] = time24.split(":");
4
+ const hour = parseInt(hours);
5
+ const ampm = hour >= 12 ? "PM" : "AM";
6
+ const hour12 = hour % 12 || 12;
7
+ return `${hour12}:${minutes} ${ampm}`;
8
+ }
9
+
10
+ const axios = require("axios");
11
+ const logger = require("../utils/logger.cjs");
12
+
13
+ module.exports = {
14
+ name: "prayer",
15
+ description: "Gets prayer times for a specific city.",
16
+ usage: "prayer <المدينة بالإنجليزية>",
17
+ chat: "all", // This command can be used anywhere
18
+
19
+ async execute(sock, msg, args) {
20
+ // 2. Check if the user provided a city name
21
+ if (!args || args.length === 0) {
22
+ return await sock.sendMessage(msg.key.remoteJid, {
23
+ text: "يرجى تقديم اسم المدينة باللغة الانجليزية.\n\nمثال:* `!prayer cairo`",
24
+ });
25
+ }
26
+
27
+ // 3. Join the arguments to form the city name (in case it's multi-word like "New York")
28
+ const city = args.join(" ");
29
+
30
+ // 4. We will build the API URL here once you provide it
31
+ const API_URL = `https://api.aladhan.com/v1/timingsByCity/06-06-2025?city=${city}&country=EG&state=Egypt&method=5&shafaq=general&tune=5&timezonestring=Africa%2FCairo&calendarMethod=UAQ`;
32
+
33
+ try {
34
+ await sock.sendMessage(msg.key.remoteJid, {
35
+ text: "يتم جلب البيانات...",
36
+ });
37
+ // 5. We will make the API call using axios
38
+ const response = await axios.get(API_URL);
39
+ // 6. We will parse the prayer timings from the response data
40
+ const timings = response.data.data.timings;
41
+
42
+ // 7. We will format the reply message
43
+ const reply =
44
+ `*مواقيت الصلاة لمدينة ${city}:*\n\n` +
45
+ `الفجر: ${formatTime12Hour(timings.Fajr)}\n` +
46
+ `الشروق: ${formatTime12Hour(timings.Sunrise)}\n` +
47
+ `الظهر: ${formatTime12Hour(timings.Dhuhr)}\n` +
48
+ `العصر: ${formatTime12Hour(timings.Asr)}\n` +
49
+ `المغرب: ${formatTime12Hour(timings.Maghrib)}\n` +
50
+ `العشاء: ${formatTime12Hour(timings.Isha)}`;
51
+
52
+ // 8. Send the formatted message
53
+ await sock.sendMessage(msg.key.remoteJid, {
54
+ text: reply,
55
+ });
56
+ } catch (error) {
57
+ logger.error(error.message, "[Error] in Prayer API:");
58
+ // 9. Handle errors, like city not found or API failure
59
+ await sock.sendMessage(msg.key.remoteJid, {
60
+ text: `عذراً، لم أتمكن من العثور على وقت الصلاة في "${city}". يرجى التحقق من اسم المدينة ومحاولة مرة أخرى.`,
61
+ });
62
+ }
63
+ },
64
+ };
@@ -0,0 +1,39 @@
1
+ // file: /commands/qr.js
2
+ const qrcode = require("qrcode");
3
+ const logger = require("../utils/logger.cjs");
4
+
5
+ module.exports = {
6
+ name: "qr",
7
+ description: "Generates a QR code from text.",
8
+ usage: "qr <نص|رابط>",
9
+ chat: "all",
10
+
11
+ async execute(sock, msg, args) {
12
+ const remoteJid = msg.key.remoteJid;
13
+ const textToEncode = args.join(" ");
14
+
15
+ if (!textToEncode) {
16
+ return await sock.sendMessage(remoteJid, {
17
+ text: "يرجى كتابة النص أو الرابط الذي تريد تحويله بعد الأمر.\n*مثال:*\n`!qr https://google.com`",
18
+ });
19
+ }
20
+
21
+ try {
22
+ // Generate the QR code and get it as a Buffer (raw image data)
23
+ const qrImageBuffer = await qrcode.toBuffer(textToEncode, {
24
+ errorCorrectionLevel: "H", // High error correction
25
+ });
26
+
27
+ // Send the image buffer as a photo
28
+ await sock.sendMessage(remoteJid, {
29
+ image: qrImageBuffer,
30
+ caption: `*QR Code for:*\n\`\`\`${textToEncode}\`\`\``,
31
+ });
32
+ } catch (error) {
33
+ logger.error({ err: error }, "Failed to generate QR code");
34
+ await sock.sendMessage(remoteJid, {
35
+ text: "حدث خطأ أثناء إنشاء QR code.",
36
+ });
37
+ }
38
+ },
39
+ };