├── data ├── file1.txt ├── file10.txt ├── file11.txt ├── file12.txt ├── file2.txt ├── file3.txt ├── file4.txt ├── file5.txt ├── file6.txt ├── file7.txt ├── file8.txt └── file9.txt ├── .env ├── package.json ├── LICENSE ├── README.md ├── rules.json ├── alive.js └── index.js /data/file1.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/file10.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/file11.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/file12.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/file2.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/file3.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/file4.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/file5.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/file6.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/file7.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/file8.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/file9.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Discord bot token 2 | TOKEN=YOUR_TOKEN 3 | 4 | PREFIX=! 5 | 6 | # Port for the keep-alive HTTP server 7 | PORT=PORT 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rules-bot", 3 | "version": "1.0.0", 4 | "description": "bot that send rules as embed with select menu", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "dependencies": { 10 | "discord.js": "^14.0.0", 11 | "dotenv": "^16.4.5" 12 | } 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Wick Studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Discord Rules Bot 3 | 4 | بوت ديسكورد بسيط يعرض القوانين عند دخول الأعضاء الجدد. 5 | تم تطويره من قبل Ghlais & Wick Studio © 6 | 7 | ## ✨ المميزات 8 | - إرسال القوانين بشكل مرتب. 9 | - تخزين القوانين في ملف `rules.json` مع دعم ملفات إضافية في مجلد `data/`. 10 | - سهل التعديل والتخصيص. 11 | - يعمل على Node.js مع مكتبة Discord.js. 12 | 13 | ## 📦 التثبيت 14 | ```bash 15 | git clone https://github.com/wickstudio/discord-rules-bot.git 16 | cd discord-rules-bot-main 17 | npm install 18 | ```` 19 | 20 | ## ⚙️ الإعداد 21 | 22 | 1. أنشئ ملف `.env` يحتوي على: 23 | 24 | ``` 25 | TOKEN=توكن البوت 26 | PREFIX= 27 | PORT=بورت الموقع 28 | ``` 29 | 2. عدل القوانين في `rules.json` أو أضف قوانين نصية في `data/`. 30 | 31 | ## ▶️ التشغيل 32 | 33 | ```bash 34 | node index.js 35 | ``` 36 | 37 | ## 🛠️ الملفات 38 | 39 | * `index.js`: الملف الأساسي لتشغيل البوت. 40 | * `alive.js`: ملف الموقع. 41 | * `rules.json`: يحتوي على القوانين الأساسية. 42 | * `data/`: يحتوي ملفات نصية إضافية يمكن استدعاؤها. 43 | * `.env`: متغيرات البيئة (توكن وإعدادات). 44 | * `package.json`: تعريف الحزم والاعتماديات. 45 | 46 | ## 📜 الحقوق 47 | 48 | * المطور: Ghlais 49 | * الدعم والتطوير: Wick Studio 50 | * جميع الحقوق محفوظة © 2025 51 | -------------------------------------------------------------------------------- /rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "rules1", 4 | "title": "rules2", 5 | "description": "data/file1.txt", 6 | "emoji": "💬" 7 | }, 8 | { 9 | "id": "rule2", 10 | "title": "test 2", 11 | "description": "data/file2.txt", 12 | "emoji": "💬" 13 | }, 14 | { 15 | "id": "rule3", 16 | "title": "test 3", 17 | "description": "data/file3.txt", 18 | "emoji": "💬" 19 | }, 20 | { 21 | "id": "rule4", 22 | "title": "test 3", 23 | "description": "data/file4.txt", 24 | "emoji": "💬" 25 | }, 26 | { 27 | "id": "rule5", 28 | "title": "test 3", 29 | "description": "data/file5.txt", 30 | "emoji": "💬" 31 | }, 32 | { 33 | "id": "rule6", 34 | "title": "test 3", 35 | "description": "data/file6.txt", 36 | "emoji": "💬" 37 | }, 38 | { 39 | "id": "rule7", 40 | "title": "test 3", 41 | "description": "data/file7.txt", 42 | "emoji": "💬" 43 | }, 44 | { 45 | "id": "rule8", 46 | "title": "test 3", 47 | "description": "data/file8.txt", 48 | "emoji": "💬" 49 | }, 50 | { 51 | "id": "rule9", 52 | "title": "test 3", 53 | "description": "data/file9.txt", 54 | "emoji": "💬" 55 | }, 56 | { 57 | "id": "rule10", 58 | "title": "test 3", 59 | "description": "data/file10.txt", 60 | "emoji": "💬" 61 | }, 62 | { 63 | "id": "rule11", 64 | "title": "test 3", 65 | "description": "data/file11.txt", 66 | "emoji": "💬" 67 | }, 68 | { 69 | "id": "rule12", 70 | "title": "test 3", 71 | "description": "data/file12.txt", 72 | "emoji": "💬" 73 | } 74 | 75 | ] 76 | -------------------------------------------------------------------------------- /alive.js: -------------------------------------------------------------------------------- 1 | var http = require("http"); 2 | 3 | function startServer() { 4 | http 5 | .createServer(function (req, res) { 6 | res.writeHead(200, { "Content-Type": "text/html" }); 7 | var htmlContent = ` 8 | 9 | 10 | 11 | 15 | Wick Studio 16 | 197 | 198 | 199 |
200 |

Welcome to Wick Studio!

201 |

You can reach us through the links provided in the buttons below

202 |
203 |
204 | 205 | 208 | 209 | 212 |
213 | 235 |
236 |
237 | 248 | 254 | 262 | 263 | 264 | `; 265 | 266 | res.write(htmlContent); 267 | res.end(); 268 | }) 269 | .listen(5000, () => 270 | console.log("HTTP server running on http://localhost:5000"), 271 | ); 272 | } 273 | 274 | module.exports = { startServer }; 275 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | require('dotenv').config(); 3 | 4 | const { 5 | Client, 6 | GatewayIntentBits, 7 | Partials, 8 | EmbedBuilder, 9 | ActionRowBuilder, 10 | StringSelectMenuBuilder, 11 | Events, 12 | PermissionsBitField, 13 | MessageFlags, 14 | AttachmentBuilder, 15 | } = require('discord.js'); 16 | 17 | const fs = require('fs'); 18 | const path = require('path'); 19 | const rules = require('./rules.json'); 20 | const { startServer } = require('./alive.js'); 21 | 22 | const PREFIX = process.env.PREFIX || '!'; 23 | const COMMAND = (process.env.CMD || 'rules').toLowerCase(); 24 | 25 | const client = new Client({ 26 | intents: [ 27 | GatewayIntentBits.Guilds, 28 | GatewayIntentBits.GuildMessages, 29 | GatewayIntentBits.MessageContent, 30 | ], 31 | partials: [Partials.Channel], 32 | }); 33 | 34 | // ===== Utilities ===== 35 | 36 | const fileCache = new Map(); 37 | function readTextFile(p) { 38 | const abs = path.resolve(process.cwd(), p); 39 | if (fileCache.has(abs)) return fileCache.get(abs); 40 | const data = fs.readFileSync(abs, 'utf8'); 41 | fileCache.set(abs, data); 42 | return data; 43 | } 44 | 45 | function pickFirstEmoji(str) { 46 | if (typeof str !== 'string') return null; 47 | const re = /\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?)*?/gu; 48 | const m = re.exec(str); 49 | return m ? m[0] : null; 50 | } 51 | 52 | function normalizeEmoji(input) { 53 | if (!input) return null; 54 | 55 | if (typeof input === 'string' && input.includes(':')) { 56 | const m = input.match(/^$/); 57 | if (m) return { id: m[1] }; 58 | const uni = pickFirstEmoji(input); 59 | return uni || null; 60 | } 61 | 62 | if (typeof input === 'string') { 63 | const uni = pickFirstEmoji(input); 64 | return uni || null; 65 | } 66 | 67 | if (typeof input === 'object' && input.id) { 68 | return { id: String(input.id), name: input.name, animated: !!input.animated }; 69 | } 70 | 71 | return null; 72 | } 73 | 74 | function chunk(text, max = 4000) { 75 | const out = []; 76 | let cur = ''; 77 | for (const line of String(text).split('\n')) { 78 | if ((cur + line + '\n').length > max) { 79 | if (cur) out.push(cur); 80 | if (line.length > max) { 81 | for (let i = 0; i < line.length; i += max) out.push(line.slice(i, i + max)); 82 | cur = ''; 83 | } else { 84 | cur = line + '\n'; 85 | } 86 | } else { 87 | cur += line + '\n'; 88 | } 89 | } 90 | if (cur) out.push(cur); 91 | return out; 92 | } 93 | 94 | const lastUsePerChannel = new Map(); 95 | function canRunInChannel(channelId, cooldownMs = 3000) { 96 | const now = Date.now(); 97 | const last = lastUsePerChannel.get(channelId) || 0; 98 | if (now - last < cooldownMs) return false; 99 | lastUsePerChannel.set(channelId, now); 100 | return true; 101 | } 102 | 103 | function buildSelectOptionsFromRules(list) { 104 | const used = new Set(); 105 | const fixes = []; 106 | 107 | return list.slice(0, 25).map((rule, idx) => { 108 | let value = String(rule?.id ?? `rule${idx + 1}`).trim(); 109 | if (!value) value = `rule${idx + 1}`; 110 | if (used.has(value)) { 111 | const base = value; 112 | let k = 2; 113 | while (used.has(`${base}_${k}`)) k++; 114 | value = `${base}_${k}`; 115 | fixes.push(`${base} -> ${value}`); 116 | } 117 | used.add(value); 118 | 119 | const opt = { 120 | label: String(rule?.title ?? value).slice(0, 100) || value, 121 | description: value.slice(0, 100), 122 | value, 123 | }; 124 | 125 | const em = normalizeEmoji(rule?.emoji); 126 | if (em) opt.emoji = em; 127 | 128 | return opt; 129 | }); 130 | } 131 | 132 | // ===== Bot ===== 133 | 134 | client.once(Events.ClientReady, () => { 135 | console.log(`Bot is Ready! ${client.user.tag}`); 136 | console.log(`by ghlais`); 137 | console.log(`Code by Wick Studio`); 138 | console.log(`discord.gg/wicks`); 139 | }); 140 | 141 | // يبني منيو القوانين (Embed + Row) 142 | async function makeMenu() { 143 | const options = buildSelectOptionsFromRules(rules); 144 | 145 | const row = new ActionRowBuilder().addComponents( 146 | new StringSelectMenuBuilder() 147 | .setCustomId('rules_select') 148 | .setPlaceholder('قائمة القوانين') 149 | .addOptions(options), 150 | ); 151 | 152 | const embed = new EmbedBuilder() 153 | .setColor('#24242c') 154 | .setThumbnail('https://media.discordapp.net/attachments/1244648462100860949/1244648462327480441/bbbff967f9b04bf6_1-18.png.webp?ex=68df2b75&is=68ddd9f5&hm=fb7425f2697943ff68c79f6ba8ea01fc0a012c92be11bf0d1cc429e16cee40f6&format=webp&width=550&height=309&') 155 | .setTitle('قوانين السيرفر') 156 | .setDescription('الرجاء اختيار أحد القوانين لقرائته من قائمة الاختيارات تحت') 157 | .setImage('https://media.discordapp.net/attachments/1244648462100860949/1244648462327480441/bbbff967f9b04bf6_1-18.png.webp?ex=68df2b75&is=68ddd9f5&hm=fb7425f2697943ff68c79f6ba8ea01fc0a012c92be11bf0d1cc429e16cee40f6&format=webp&width=550&height=309&') 158 | .setFooter({ text: 'Rules Bot' }) 159 | .setTimestamp(); 160 | 161 | return { embed, row }; 162 | } 163 | 164 | client.on(Events.MessageCreate, async (message) => { 165 | if (message.author.bot || !message.guild) return; 166 | if (!message.content.startsWith(PREFIX)) return; 167 | 168 | const args = message.content.slice(PREFIX.length).trim().split(/\s+/); 169 | const cmd = (args.shift() || '').toLowerCase(); 170 | 171 | // !rules 172 | if (cmd === COMMAND) { 173 | if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) { 174 | return message.reply({ content: 'تحتاج صلاحيات ادمن لاستخدام الأمر' }); 175 | } 176 | if (!canRunInChannel(message.channel.id)) { 177 | return message.react('⏳').catch(() => {}); 178 | } 179 | 180 | try { 181 | const { embed, row } = await makeMenu(); 182 | await message.channel.send({ embeds: [embed], components: [row] }); 183 | try { await message.delete(); } catch {} 184 | } catch (e) { 185 | console.error('send menu error', e); 186 | try { 187 | const lines = rules.slice(0, 25).map((r, i) => { 188 | const em = normalizeEmoji(r.emoji); 189 | const emStr = typeof em === 'string' ? em : ''; 190 | const val = String(r?.id ?? `rule${i + 1}`); 191 | return `${emStr || '📜'} ${r.title || val} — ${val}`; 192 | }); 193 | await message.channel.send('تعذر إرسال المنيو، عرضت قائمة نصية:\n' + lines.join('\n')); 194 | } catch {} 195 | message.reply('تعذر إرسال المنيو: تأكد صلاحيات البوت (Send Messages + Embed Links) وصيغة الإيموجي وقيم IDs غير مكررة.').catch(() => {}); 196 | } 197 | } 198 | 199 | if (cmd === 'ping') return message.reply('pong'); 200 | }); 201 | 202 | client.on(Events.InteractionCreate, async (interaction) => { 203 | if (!interaction.isStringSelectMenu() || interaction.customId !== 'rules_select') return; 204 | 205 | async function safeDefer() { 206 | try { 207 | await interaction.deferReply({ flags: MessageFlags.Ephemeral }); 208 | } catch { 209 | try { await interaction.deferReply({ ephemeral: true }); } catch {} 210 | } 211 | } 212 | if (!interaction.deferred && !interaction.replied) { 213 | await safeDefer(); 214 | } 215 | 216 | try { 217 | const selectedId = interaction.values[0]; 218 | const baseId = String(selectedId).split('_')[0]; 219 | const rule = rules.find((r) => String(r.id) === baseId) || 220 | rules.find((r) => String(r.id) === String(selectedId)); 221 | 222 | if (!rule) { 223 | return interaction.editReply({ content: 'القانون غير موجود' }); 224 | } 225 | 226 | let text = 'لا يوجد وصف'; 227 | try { 228 | text = readTextFile(rule.description); 229 | } catch { 230 | text = 'تعذر قراءة ملف الوصف تأكد من المسار في rules.json'; 231 | } 232 | 233 | const parts = chunk(text, 4000); 234 | const first = parts.shift() || 'لا يوجد وصف'; 235 | 236 | const base = new EmbedBuilder() 237 | .setColor('#24242c') 238 | .setTitle(rule.title || String(rule.id)) 239 | .setDescription(first) 240 | .setFooter({ text: 'Rules Bot' }) 241 | .setTimestamp(); 242 | 243 | await interaction.editReply({ embeds: [base] }); 244 | 245 | if (parts.length > 4) { 246 | const file = new AttachmentBuilder(Buffer.from(text, 'utf8'), { name: `${rule.id || 'rule'}.txt` }); 247 | await interaction.followUp({ 248 | content: 'النص طويل تم إرساله كملف', 249 | files: [file], 250 | flags: MessageFlags.Ephemeral, 251 | }).catch(async () => { 252 | await interaction.followUp({ content: 'النص طويل تم إرساله كملف', files: [file], ephemeral: true }); 253 | }); 254 | } else { 255 | for (let i = 0; i < parts.length; i++) { 256 | const cont = new EmbedBuilder() 257 | .setColor('#24242c') 258 | .setTitle(`${rule.title || rule.id} — تابع ${i + 2}`) 259 | .setDescription(parts[i]) 260 | .setFooter({ text: 'Rules Bot' }) 261 | .setTimestamp(); 262 | 263 | await interaction.followUp({ embeds: [cont], flags: MessageFlags.Ephemeral }) 264 | .catch(async () => { await interaction.followUp({ embeds: [cont], ephemeral: true }); }); 265 | } 266 | } 267 | } catch (err) { 268 | console.error('rules_select error', err); 269 | try { 270 | await interaction.editReply({ content: 'صار خطأ داخلي، حاول مرة ثانية.' }); 271 | } catch {} 272 | } 273 | }); 274 | 275 | startServer(); 276 | client.login(process.env.TOKEN); 277 | 278 | 279 | 280 | --------------------------------------------------------------------------------