├── 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 |
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 |
--------------------------------------------------------------------------------