@tryty/easydc-js 0.0.0-stage → 1.2.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.
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+ const { BotError, Stop } = require('../errors');
3
+ const { parseTime, formatDuration } = require('../util');
4
+ const { f, list, cleanId } = require('./helpers');
5
+
6
+ const MAX_WAIT = 60 * 1000;
7
+ const MAX_DELETE_IN = 24 * 60 * 60 * 1000;
8
+
9
+ const msgOf = async (api, args, i) => (args[i] ? api.run(args[i]) : '');
10
+
11
+ // Catatan: semua fungsi "only*" bersifat lazy supaya pesan penolakan (arg terakhir)
12
+ // tidak ikut dieksekusi saat syaratnya terpenuhi.
13
+ module.exports = [
14
+ // Struktur (ditangani parser/interpreter, definisi ini untuk validasi & dokumentasi)
15
+ f('if', { b: 'required', min: 1, max: 1, lazy: true, s: '[kondisi]', d: 'Mulai blok kondisi. Tutup dengan $endif' }, () => ''),
16
+ f('elseif', { b: 'required', min: 1, max: 1, lazy: true, s: '[kondisi]', d: 'Kondisi alternatif' }, () => ''),
17
+ f('else', { b: 'none', lazy: true, d: 'Jika semua kondisi sebelumnya salah' }, () => ''),
18
+ f('endif', { b: 'none', lazy: true, d: 'Menutup blok $if' }, () => ''),
19
+
20
+ f('stop', { max: 1, lazy: true, s: '[pesan?]', d: 'Hentikan eksekusi (opsional kirim pesan)' },
21
+ async (ctx, args, api) => { throw new Stop(await msgOf(api, args, 0)); }),
22
+ f('onlyIf', { b: 'required', min: 1, max: 2, lazy: true, s: '[kondisi;pesan?]', d: 'Lanjut hanya jika kondisi benar' },
23
+ async (ctx, args, api) => {
24
+ if (await api.cond(args[0])) return '';
25
+ throw new Stop(await msgOf(api, args, 1));
26
+ }),
27
+ f('onlyPerms', { b: 'required', min: 1, max: 2, lazy: true, s: '[Izin1,Izin2;pesan?]', d: 'Lanjut hanya jika pengirim punya semua izin (mis. BanMembers)' },
28
+ async (ctx, args, api) => {
29
+ const perms = list(await api.run(args[0]));
30
+ if (!perms.length) throw new BotError('Izin belum diisi');
31
+ if (ctx.member?.permissions?.has(perms)) return '';
32
+ throw new Stop(await msgOf(api, args, 1));
33
+ }),
34
+ f('onlyBotPerms', { b: 'required', min: 1, max: 2, lazy: true, s: '[Izin1,Izin2;pesan?]', d: 'Lanjut hanya jika bot punya izin tersebut di server' },
35
+ async (ctx, args, api) => {
36
+ const perms = list(await api.run(args[0]));
37
+ if (!perms.length) throw new BotError('Izin belum diisi');
38
+ if (ctx.guild?.members?.me?.permissions?.has(perms)) return '';
39
+ throw new Stop(await msgOf(api, args, 1));
40
+ }),
41
+ f('onlyAdmin', { max: 1, lazy: true, s: '[pesan?]', d: 'Lanjut hanya jika pengirim administrator' },
42
+ async (ctx, args, api) => {
43
+ if (ctx.member?.permissions?.has('Administrator')) return '';
44
+ throw new Stop(await msgOf(api, args, 0));
45
+ }),
46
+ f('onlyOwner', { max: 1, lazy: true, s: '[pesan?]', d: 'Lanjut hanya jika pengirim ada di opsi owners' },
47
+ async (ctx, args, api) => {
48
+ if (ctx.bot.options.owners.includes(ctx.user?.id)) return '';
49
+ throw new Stop(await msgOf(api, args, 0));
50
+ }),
51
+ f('onlyForIDs', { b: 'required', min: 1, max: 2, lazy: true, s: '[id1,id2;pesan?]', d: 'Lanjut hanya untuk user dengan ID tertentu' },
52
+ async (ctx, args, api) => {
53
+ const ids = list(await api.run(args[0])).map(cleanId);
54
+ if (ids.includes(ctx.user?.id)) return '';
55
+ throw new Stop(await msgOf(api, args, 1));
56
+ }),
57
+ f('onlyInServer', { max: 1, lazy: true, s: '[pesan?]', d: 'Lanjut hanya jika dipakai di server (bukan DM)' },
58
+ async (ctx, args, api) => {
59
+ if (ctx.guild) return '';
60
+ throw new Stop(await msgOf(api, args, 0));
61
+ }),
62
+ f('argsCheck', { b: 'required', min: 1, max: 2, lazy: true, s: '[syarat;pesan?]', d: 'Cek jumlah argumen. "2" = minimal 2; juga >=1 <=3 >1 <3 =2' },
63
+ async (ctx, args, api) => {
64
+ const spec = (await api.run(args[0])).trim();
65
+ const m = /^(>=|<=|>|<|=)?\s*(\d+)$/.exec(spec);
66
+ if (!m) throw new BotError(`Syarat "${spec}" tidak valid (contoh: 2, >=1, <3, =2)`);
67
+ const n = Number(m[2]);
68
+ const c = ctx.args.length;
69
+ let ok;
70
+ switch (m[1]) {
71
+ case '>': ok = c > n; break;
72
+ case '<': ok = c < n; break;
73
+ case '<=': ok = c <= n; break;
74
+ case '=': ok = c === n; break;
75
+ default: ok = c >= n;
76
+ }
77
+ if (ok) return '';
78
+ throw new Stop(await msgOf(api, args, 1));
79
+ }),
80
+ f('cooldown', { b: 'required', min: 1, max: 2, lazy: true, s: '[waktu;pesan?]', d: 'Cooldown per user per command. %time% di pesan = sisa waktu' },
81
+ async (ctx, args, api) => {
82
+ const ms = parseTime(await api.run(args[0]));
83
+ const key = `${ctx.command?.key ?? ctx.command?.name ?? 'anon'}:${ctx.user?.id ?? 'none'}`;
84
+ const now = Date.now();
85
+ const cds = ctx.bot.cooldowns;
86
+ const until = cds.get(key);
87
+ if (until && until > now) {
88
+ const remaining = formatDuration(Math.ceil((until - now) / 1000) * 1000);
89
+ const custom = args[1] ? await api.run(args[1]) : ctx.bot.options.messages.cooldown;
90
+ throw new Stop(custom.replace(/%time%/g, remaining));
91
+ }
92
+ cds.set(key, now + ms);
93
+ if (cds.size > 5000) for (const [k, v] of cds) if (v <= now) cds.delete(k);
94
+ return '';
95
+ }),
96
+
97
+ f('wait', { b: 'required', min: 1, max: 1, s: '[waktu]', d: 'Tunggu sebentar (maks 60 detik)' },
98
+ async (ctx, [t]) => {
99
+ const ms = parseTime(t);
100
+ if (ms > MAX_WAIT) throw new BotError('Waktu tunggu maksimal 60 detik');
101
+ await new Promise((resolve) => { setTimeout(resolve, ms); });
102
+ return '';
103
+ }),
104
+ f('deleteCommand', { b: 'none', d: 'Hapus pesan perintah milik pengirim setelah dijawab' },
105
+ (ctx) => { ctx.deleteCommand = true; return ''; }),
106
+ f('deleteIn', { b: 'required', min: 1, max: 1, s: '[waktu]', d: 'Hapus jawaban bot setelah waktu tertentu (maks 24 jam)' },
107
+ (ctx, [t]) => {
108
+ const ms = parseTime(t);
109
+ if (ms > MAX_DELETE_IN) throw new BotError('Waktu maksimal 24 jam');
110
+ ctx.deleteIn = ms;
111
+ return '';
112
+ }),
113
+ f('ephemeral', { b: 'none', d: 'Slash/tombol: jawaban hanya terlihat oleh pengguna' },
114
+ (ctx) => { ctx.ephemeral = true; return ''; }),
115
+ f('reply', { b: 'none', d: 'Jawab dengan fitur reply pada pesan perintah' },
116
+ (ctx) => { ctx.useReply = true; return ''; }),
117
+ f('update', { b: 'none', d: 'Tombol/select: ubah pesan asal, bukan kirim pesan baru' },
118
+ (ctx) => { ctx.update = true; return ''; }),
119
+ f('typing', { b: 'none', d: 'Tampilkan "sedang mengetik"' },
120
+ async (ctx) => { await ctx.channel?.sendTyping?.(); return ''; }),
121
+ f('defer', { b: 'none', d: 'Slash/tombol: tunda jawaban (untuk proses > 3 detik). Taruh $ephemeral sebelum ini bila perlu' },
122
+ async (ctx) => {
123
+ const it = ctx.interaction;
124
+ if (!it || it.deferred || it.replied) return '';
125
+ if (ctx.update && typeof it.deferUpdate === 'function') await it.deferUpdate();
126
+ else await it.deferReply(ctx.ephemeral ? { flags: 64 } : {});
127
+ return '';
128
+ }),
129
+
130
+ f('let', { b: 'required', min: 2, max: 2, s: '[nama;nilai]', d: 'Simpan variabel sementara (hanya selama command berjalan)' },
131
+ (ctx, [name, value]) => {
132
+ if (!name.trim()) throw new BotError('Nama variabel kosong');
133
+ ctx.temp.set(name.trim(), value);
134
+ return '';
135
+ }),
136
+ f('get', { b: 'required', min: 1, max: 1, s: '[nama]', d: 'Ambil variabel sementara dari $let' },
137
+ (ctx, [name]) => {
138
+ const key = name.trim();
139
+ if (!ctx.temp.has(key)) throw new BotError(`Variabel sementara "${key}" belum dibuat dengan $let`);
140
+ return ctx.temp.get(key);
141
+ }),
142
+ ];
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+ const { BotError } = require('../errors');
3
+ const { f, has, num, url, limit } = require('./helpers');
4
+
5
+ const COLORS = {
6
+ red: 0xed4245, green: 0x57f287, blue: 0x3498db, yellow: 0xfee75c, orange: 0xe67e22,
7
+ purple: 0x9b59b6, pink: 0xeb459e, white: 0xffffff, black: 0x000000, grey: 0x95a5a6,
8
+ gray: 0x95a5a6, blurple: 0x5865f2,
9
+ };
10
+
11
+ function parseColor(v) {
12
+ const s = String(v).trim().toLowerCase();
13
+ if (s === 'random') return Math.floor(Math.random() * 0xffffff);
14
+ if (Object.hasOwn(COLORS, s)) return COLORS[s];
15
+ const m = /^(?:#|0x)?([0-9a-f]{6})$/.exec(s);
16
+ if (!m) throw new BotError(`Warna "${v}" tidak valid (contoh: #5865F2, red, random)`);
17
+ return parseInt(m[1], 16);
18
+ }
19
+
20
+ module.exports = [
21
+ f('newEmbed', { b: 'none', d: 'Mulai embed baru (embed pertama dibuat otomatis). Maks 10' },
22
+ (ctx) => { ctx.newEmbed(); return ''; }),
23
+ f('title', { b: 'required', min: 1, max: 1, s: '[teks]', d: 'Judul embed (maks 256)' },
24
+ (ctx, [t]) => { ctx.currentEmbed().title = limit('title', t, 256); return ''; }),
25
+ f('embedURL', { b: 'required', min: 1, max: 1, s: '[url]', d: 'Tautan pada judul embed' },
26
+ (ctx, [u]) => { ctx.currentEmbed().url = url(u, 'url'); return ''; }),
27
+ f('description', { b: 'required', min: 1, max: 1, s: '[teks]', d: 'Isi embed (maks 4096)' },
28
+ (ctx, [t]) => { ctx.currentEmbed().description = limit('description', t, 4096); return ''; }),
29
+ f('color', { b: 'required', min: 1, max: 1, s: '[#hex|nama|random]', d: 'Warna embed' },
30
+ (ctx, [c]) => { ctx.currentEmbed().color = parseColor(c); return ''; }),
31
+ f('footer', { b: 'required', min: 1, max: 2, s: '[teks;ikonURL?]', d: 'Footer embed (maks 2048)' },
32
+ (ctx, [t, icon]) => {
33
+ const footer = { text: limit('footer', t, 2048) };
34
+ if (has(icon)) footer.icon_url = url(icon, 'ikon footer');
35
+ ctx.currentEmbed().footer = footer;
36
+ return '';
37
+ }),
38
+ f('author', { b: 'required', min: 1, max: 3, s: '[nama;ikonURL?;url?]', d: 'Penulis embed (maks 256)' },
39
+ (ctx, [name, icon, link]) => {
40
+ const author = { name: limit('author', name, 256) };
41
+ if (has(icon)) author.icon_url = url(icon, 'ikon author');
42
+ if (has(link)) author.url = url(link, 'url author');
43
+ ctx.currentEmbed().author = author;
44
+ return '';
45
+ }),
46
+ f('thumbnail', { b: 'required', min: 1, max: 1, s: '[url]', d: 'Gambar kecil embed' },
47
+ (ctx, [u]) => { ctx.currentEmbed().thumbnail = { url: url(u, 'thumbnail') }; return ''; }),
48
+ f('image', { b: 'required', min: 1, max: 1, s: '[url]', d: 'Gambar besar embed' },
49
+ (ctx, [u]) => { ctx.currentEmbed().image = { url: url(u, 'image') }; return ''; }),
50
+ f('addField', { b: 'required', min: 2, max: 3, s: '[nama;isi;inline?]', d: 'Tambah field (maks 25; inline = true/false)' },
51
+ (ctx, [name, value, inline]) => {
52
+ const e = ctx.currentEmbed();
53
+ e.fields ??= [];
54
+ if (e.fields.length >= 25) throw new BotError('Maksimal 25 field per embed');
55
+ if (!name.trim() || !value.trim()) throw new BotError('Nama dan isi field tidak boleh kosong');
56
+ e.fields.push({
57
+ name: limit('nama field', name, 256),
58
+ value: limit('isi field', value, 1024),
59
+ inline: has(inline) && ['true', 'yes', '1'].includes(inline.trim().toLowerCase()),
60
+ });
61
+ return '';
62
+ }),
63
+ f('addTimestamp', { max: 1, s: '[ms?]', d: 'Tambah cap waktu di embed (default: sekarang)' },
64
+ (ctx, [ms]) => {
65
+ const t = has(ms) ? num(ms, 'ms') : Date.now();
66
+ ctx.currentEmbed().timestamp = new Date(t).toISOString();
67
+ return '';
68
+ }),
69
+ ];
70
+
71
+ module.exports.parseColor = parseColor;
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+ const util = require('node:util');
3
+ const { BotError, Stop } = require('../errors');
4
+ const { parse } = require('../parser');
5
+ const { f } = require('./helpers');
6
+
7
+ const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
8
+ const MAX_OUTPUT = 1800;
9
+ const MAX_DEPTH = 3;
10
+ const ZWSP = '​';
11
+
12
+ function guard(ctx, label) {
13
+ if (!ctx.bot.options.eval) {
14
+ throw new Stop(`❌ ${label} dimatikan. Nyalakan dengan new Bot({ eval: true, owners: ['ID_KAMU'] }).`);
15
+ }
16
+ if (!ctx.bot.options.owners.includes(ctx.user?.id)) throw new Stop('❌ Hanya owner bot yang boleh memakai eval.');
17
+ }
18
+
19
+ function clean(ctx, text) {
20
+ let out = String(text);
21
+ const secrets = [ctx.bot.options.token, ctx.client?.token].filter((s) => typeof s === 'string' && s.length > 8);
22
+ for (const s of secrets) out = out.split(s).join('[TOKEN DISENSOR]');
23
+ out = out.replace(/```/g, `\`${ZWSP}\`\``);
24
+ return out.length > MAX_OUTPUT ? `${out.slice(0, MAX_OUTPUT)}\n... (dipotong)` : out;
25
+ }
26
+
27
+ function compile(code, names) {
28
+ try {
29
+ return new AsyncFunction(...names, `return (\n${code}\n);`); // ekspresi tunggal
30
+ } catch (e) {
31
+ if (!(e instanceof SyntaxError)) throw e;
32
+ return new AsyncFunction(...names, code); // beberapa pernyataan (pakai return untuk hasil)
33
+ }
34
+ }
35
+
36
+ // PERINGATAN: eval memberi akses PENUH ke bot dan mesin. Hanya aktif bila opsi `eval: true`
37
+ // DAN pengirim ada di `owners`. Jangan pernah mengaktifkannya jika owners diisi orang yang tidak dipercaya.
38
+ module.exports = [
39
+ f('eval', { b: 'required', min: 1, max: 1, s: '[kode]', d: 'Jalankan kode versi port ($fungsi) dari teks saat berjalan. Khusus owner, butuh opsi eval: true' },
40
+ async (ctx, [code]) => {
41
+ guard(ctx, '$eval');
42
+ if (!code.trim()) return '❌ Kode kosong.';
43
+ if ((ctx.evalDepth ?? 0) >= MAX_DEPTH) throw new BotError('$eval bersarang terlalu dalam');
44
+ ctx.evalDepth = (ctx.evalDepth ?? 0) + 1;
45
+ try {
46
+ const text = await ctx.bot.interpreter.run(parse(code, ctx.bot.registry), ctx);
47
+ return clean(ctx, text);
48
+ } catch (e) {
49
+ if (e instanceof Stop) throw e;
50
+ return clean(ctx, `❌ ${e.message}`);
51
+ } finally {
52
+ ctx.evalDepth -= 1;
53
+ }
54
+ }),
55
+ f('jsEval', { b: 'required', min: 1, max: 1, s: '[kodeJavaScript]', d: 'Jalankan kode discord.js/JavaScript asli. Variabel: ctx client bot guild channel message user member interaction discord db require. Khusus owner, butuh opsi eval: true' },
56
+ async (ctx, [code]) => {
57
+ guard(ctx, '$jsEval');
58
+ if (!code.trim()) return '❌ Kode kosong.';
59
+ let discord = null;
60
+ try { discord = ctx.bot._discord(); } catch { /* discord.js belum terpasang di mesin ini */ }
61
+ const scope = {
62
+ ctx, client: ctx.client, bot: ctx.bot, guild: ctx.guild, channel: ctx.channel, message: ctx.message,
63
+ user: ctx.user, member: ctx.member, interaction: ctx.interaction, discord, db: ctx.bot.db, require,
64
+ };
65
+ try {
66
+ const result = await compile(code, Object.keys(scope))(...Object.values(scope));
67
+ const text = typeof result === 'string' ? result : util.inspect(result, { depth: 1, maxArrayLength: 20, breakLength: 80 });
68
+ return clean(ctx, text);
69
+ } catch (e) {
70
+ return clean(ctx, `❌ ${e?.name ?? 'Error'}: ${e?.message ?? e}`);
71
+ }
72
+ }),
73
+ ];
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+ const { BotError } = require('../errors');
3
+
4
+ const has = (v) => v !== undefined && v !== null && String(v) !== '';
5
+ const truthy = (v) => ['true', 'yes', '1', 'on'].includes(String(v ?? '').trim().toLowerCase());
6
+ const cleanId = (v) => String(v ?? '').trim().replace(/^<(?:@!?|@&|#)(\d+)>$/, '$1');
7
+ const list = (v) => String(v ?? '').split(',').map((s) => s.trim()).filter(Boolean);
8
+
9
+ function num(v, label, { min = -Infinity, max = Infinity, int = false } = {}) {
10
+ const s = String(v ?? '').trim();
11
+ const n = Number(s);
12
+ if (s === '' || !Number.isFinite(n)) throw new BotError(`${label} harus berupa angka (diberi "${s}")`);
13
+ if (int && !Number.isInteger(n)) throw new BotError(`${label} harus bilangan bulat (diberi "${s}")`);
14
+ if (n < min || n > max) throw new BotError(`${label} harus di antara ${min} dan ${max} (diberi ${s})`);
15
+ return n;
16
+ }
17
+
18
+ function url(v, label) {
19
+ const s = String(v ?? '').trim();
20
+ if (!/^https?:\/\/\S+$/i.test(s)) throw new BotError(`${label} harus URL http(s) yang valid (diberi "${s}")`);
21
+ return s;
22
+ }
23
+
24
+ function limit(label, value, max) {
25
+ const s = String(value);
26
+ if (s.length > max) throw new BotError(`${label} terlalu panjang (${s.length} > ${max} karakter)`);
27
+ return s;
28
+ }
29
+
30
+ async function fetchChannel(ctx, id) {
31
+ const clean = cleanId(id);
32
+ const cached = ctx.client.channels.cache.get(clean);
33
+ if (cached) return cached;
34
+ try {
35
+ const ch = await ctx.client.channels.fetch(clean);
36
+ if (ch) return ch;
37
+ } catch { /* jatuh ke error di bawah */ }
38
+ throw new BotError(`Channel "${clean}" tidak ditemukan`);
39
+ }
40
+
41
+ /** Channel dari ID, atau channel saat ini bila ID kosong. */
42
+ async function channelOrCurrent(ctx, id) {
43
+ if (has(id)) return fetchChannel(ctx, id);
44
+ if (!ctx.channel) throw new BotError('Tidak ada channel pada konteks ini, isi channelID');
45
+ return ctx.channel;
46
+ }
47
+
48
+ async function fetchMessage(ctx, messageId, channelId) {
49
+ const ch = await channelOrCurrent(ctx, channelId);
50
+ if (!ch.messages || typeof ch.messages.fetch !== 'function') throw new BotError('Channel itu tidak punya pesan');
51
+ try {
52
+ return await ch.messages.fetch(String(messageId).trim());
53
+ } catch (e) {
54
+ throw new BotError(`Pesan "${messageId}" tidak ditemukan`, { cause: e });
55
+ }
56
+ }
57
+
58
+ function getRole(ctx, id) {
59
+ const clean = cleanId(id);
60
+ const role = ctx.requireGuild().roles.cache.get(clean);
61
+ if (!role) throw new BotError(`Role "${clean}" tidak ditemukan`);
62
+ return role;
63
+ }
64
+
65
+ /**
66
+ * Buat definisi fungsi.
67
+ * spec: b = 'none' | 'optional' | 'required' (tanda kurung), min, max, lazy,
68
+ * s = contoh sintaks, d = deskripsi (dipakai untuk dokumentasi otomatis)
69
+ */
70
+ function f(name, spec, run) {
71
+ if (typeof spec === 'function') { run = spec; spec = {}; }
72
+ const brackets = spec.b ?? 'optional';
73
+ return {
74
+ name: name.toLowerCase(),
75
+ display: `$${name}`,
76
+ brackets,
77
+ min: spec.min ?? 0,
78
+ max: brackets === 'none' ? 0 : (spec.max ?? Infinity),
79
+ lazy: !!spec.lazy,
80
+ sig: spec.s ?? '',
81
+ doc: spec.d ?? '',
82
+ run,
83
+ };
84
+ }
85
+
86
+ module.exports = { f, has, truthy, cleanId, list, num, url, limit, fetchChannel, channelOrCurrent, fetchMessage, getRole };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const CATEGORIES = [
4
+ ['Info (user, server, bot, pesan, waktu)', require('./info')],
5
+ ['Teks, matematika & pengecekan', require('./text')],
6
+ ['Kontrol alur & batasan', require('./control')],
7
+ ['Variabel & database', require('./vars')],
8
+ ['Embed', require('./embed')],
9
+ ['Tombol & select menu', require('./components')],
10
+ ['Aksi & moderasi', require('./actions')],
11
+ ['Pesan, lampiran, polling, thread & webhook', require('./messages')],
12
+ ['Manajemen server (ban, channel, role, undangan, emoji, event, voice)', require('./manage')],
13
+ ['Modal, autocomplete & sasaran', require('./interact')],
14
+ ['Eval (khusus owner)', require('./eval')],
15
+ ];
16
+
17
+ /** Map(namaLowercase -> definisi). Dilempar error bila ada nama ganda. */
18
+ function createRegistry() {
19
+ const map = new Map();
20
+ for (const [category, defs] of CATEGORIES) {
21
+ for (const def of defs) {
22
+ if (map.has(def.name)) throw new Error(`Fungsi ganda: ${def.display}`);
23
+ map.set(def.name, { ...def, category });
24
+ }
25
+ }
26
+ return map;
27
+ }
28
+
29
+ module.exports = { createRegistry, CATEGORIES };
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+ const { BotError } = require('../errors');
3
+ const { formatDuration, formatDate } = require('../util');
4
+ const { f, has, truthy, cleanId, num } = require('./helpers');
5
+
6
+ const emojiText = (e) => {
7
+ if (!e) return '';
8
+ if (e.id) return `<${e.animated ? 'a' : ''}:${e.name}:${e.id}>`;
9
+ return e.name ?? '';
10
+ };
11
+
12
+ module.exports = [
13
+ // ---- User -------------------------------------------------------------------
14
+ f('username', { max: 1, s: '[userID?]', d: 'Username (tanpa ID = pengirim)' },
15
+ async (ctx, [id]) => (await ctx.fetchUser(id)).username),
16
+ f('usertag', { max: 1, s: '[userID?]', d: 'Tag user' },
17
+ async (ctx, [id]) => { const u = await ctx.fetchUser(id); return u.tag ?? u.username; }),
18
+ f('displayName', { max: 1, s: '[userID?]', d: 'Nama tampilan (nickname server / nama global / username)' },
19
+ async (ctx, [id]) => {
20
+ const u = await ctx.fetchUser(id);
21
+ const m = await ctx.tryMember(id);
22
+ return m?.displayName ?? u.globalName ?? u.username;
23
+ }),
24
+ f('nickname', { max: 1, s: '[userID?]', d: 'Nickname di server (kosong -> username)' },
25
+ async (ctx, [id]) => {
26
+ const u = await ctx.fetchUser(id);
27
+ const m = await ctx.tryMember(id);
28
+ return m?.nickname ?? u.username;
29
+ }),
30
+ f('authorID', { b: 'none', d: 'ID pengirim pesan / pengguna interaksi' }, (ctx) => ctx.user?.id ?? ''),
31
+ f('userAvatar', { max: 2, s: '[userID?;ukuran?]', d: 'URL avatar (ukuran 16-4096)' },
32
+ async (ctx, [id, size]) => {
33
+ const u = await ctx.fetchUser(id);
34
+ return u.displayAvatarURL({ size: has(size) ? num(size, 'ukuran', { min: 16, max: 4096, int: true }) : 256, extension: 'png' });
35
+ }),
36
+ f('isBot', { max: 1, s: '[userID?]', d: 'true/false: apakah user adalah bot' },
37
+ async (ctx, [id]) => String(!!(await ctx.fetchUser(id)).bot)),
38
+ f('userCreatedAt', { max: 1, s: '[userID?]', d: 'Waktu akun dibuat (ms)' },
39
+ async (ctx, [id]) => (await ctx.fetchUser(id)).createdTimestamp),
40
+ f('mentioned', { b: 'required', min: 1, max: 2, s: '[nomor;fallbackAuthor?]', d: 'ID user yang di-mention ke-N (isi 2: yes = pakai pengirim bila tidak ada)' },
41
+ (ctx, [n, fallback]) => {
42
+ const i = num(n, 'nomor', { min: 1, int: true });
43
+ const user = ctx.mentionedUsers()[i - 1];
44
+ if (user) return user.id;
45
+ return truthy(fallback) ? (ctx.user?.id ?? '') : '';
46
+ }),
47
+ f('mention', { max: 1, s: '[userID?]', d: 'Mention user, contoh <@123>' },
48
+ (ctx, [id]) => `<@${has(id) ? cleanId(id) : (ctx.user?.id ?? '')}>`),
49
+
50
+ // ---- Server ---------------------------------------------------------------
51
+ f('serverName', { b: 'none', d: 'Nama server' }, (ctx) => ctx.requireGuild().name),
52
+ f('serverID', { b: 'none', d: 'ID server' }, (ctx) => ctx.requireGuild().id),
53
+ f('serverIcon', { b: 'none', d: 'URL ikon server' },
54
+ (ctx) => ctx.requireGuild().iconURL?.({ size: 256, extension: 'png' }) ?? ''),
55
+ f('memberCount', { b: 'none', d: 'Jumlah member server' }, (ctx) => ctx.requireGuild().memberCount),
56
+ f('ownerID', { b: 'none', d: 'ID pemilik server' }, (ctx) => ctx.requireGuild().ownerId),
57
+ f('serverCreatedAt', { b: 'none', d: 'Waktu server dibuat (ms)' }, (ctx) => ctx.requireGuild().createdTimestamp),
58
+ f('roleCount', { b: 'none', d: 'Jumlah role' }, (ctx) => ctx.requireGuild().roles.cache.size),
59
+ f('channelCount', { b: 'none', d: 'Jumlah channel' }, (ctx) => ctx.requireGuild().channels.cache.size),
60
+ f('channelName', { max: 1, s: '[channelID?]', d: 'Nama channel (tanpa ID = channel ini)' },
61
+ (ctx, [id]) => {
62
+ const ch = has(id) ? ctx.client.channels.cache.get(cleanId(id)) : ctx.channel;
63
+ if (!ch) throw new BotError(`Channel "${id}" tidak ditemukan`);
64
+ return ch.name ?? '';
65
+ }),
66
+ f('channelID', { b: 'none', d: 'ID channel saat ini' }, (ctx) => ctx.channel?.id ?? ''),
67
+ f('channelMention', { max: 1, s: '[channelID?]', d: 'Mention channel, contoh <#123>' },
68
+ (ctx, [id]) => `<#${has(id) ? cleanId(id) : (ctx.channel?.id ?? '')}>`),
69
+ f('roleName', { b: 'required', min: 1, max: 1, s: '[roleID]', d: 'Nama role' },
70
+ (ctx, [id]) => {
71
+ const role = ctx.requireGuild().roles.cache.get(cleanId(id));
72
+ if (!role) throw new BotError(`Role "${id}" tidak ditemukan`);
73
+ return role.name;
74
+ }),
75
+ f('roleMention', { b: 'required', min: 1, max: 1, s: '[roleID]', d: 'Mention role, contoh <@&123>' },
76
+ (ctx, [id]) => `<@&${cleanId(id)}>`),
77
+
78
+ // ---- Bot ------------------------------------------------------------------
79
+ f('botID', { b: 'none', d: 'ID bot' }, (ctx) => ctx.client.user.id),
80
+ f('botName', { b: 'none', d: 'Username bot' }, (ctx) => ctx.client.user.username),
81
+ f('ping', { b: 'none', d: 'Ping websocket (ms)' }, (ctx) => Math.round(ctx.client.ws.ping)),
82
+ f('uptime', { b: 'none', d: 'Lama bot menyala, contoh "1d 2h 3m 4s"' }, (ctx) => formatDuration(ctx.client.uptime ?? 0)),
83
+ f('serverCount', { b: 'none', d: 'Jumlah server tempat bot berada' }, (ctx) => ctx.client.guilds.cache.size),
84
+
85
+ // ---- Pesan / interaksi ----------------------------------------------------
86
+ f('message', { max: 1, s: '[nomor?]', d: 'Semua argumen setelah command, atau argumen ke-N' },
87
+ (ctx, [n]) => (has(n) ? (ctx.args[num(n, 'nomor', { min: 1, int: true }) - 1] ?? '') : ctx.rawArgs)),
88
+ f('argsCount', { b: 'none', d: 'Jumlah argumen' }, (ctx) => ctx.args.length),
89
+ f('messageID', { b: 'none', d: 'ID pesan / interaksi' }, (ctx) => ctx.message?.id ?? ctx.interaction?.id ?? ''),
90
+ f('messageContent', { b: 'none', d: 'Isi lengkap pesan pemicu' }, (ctx) => ctx.message?.content ?? ''),
91
+ f('commandName', { b: 'none', d: 'Nama command yang berjalan' }, (ctx) => ctx.command?.name ?? ''),
92
+ f('option', { b: 'required', min: 1, max: 1, s: '[nama]', d: 'Nilai opsi slash command' },
93
+ (ctx, [name]) => {
94
+ const v = ctx.options.get(name);
95
+ return v === undefined || v === null ? '' : String(v);
96
+ }),
97
+ f('customID', { b: 'none', d: 'customId tombol/select yang ditekan' }, (ctx) => ctx.customId ?? ''),
98
+ f('interactionValues', { max: 1, s: '[nomor?]', d: 'Nilai yang dipilih di select menu' },
99
+ (ctx, [i]) => (has(i) ? (ctx.values[num(i, 'nomor', { min: 1, int: true }) - 1] ?? '') : ctx.values.join(','))),
100
+ f('reactionEmoji', { b: 'none', d: 'Emoji pada event reaksi' }, (ctx) => emojiText(ctx.reaction?.emoji)),
101
+
102
+ // ---- Waktu ----------------------------------------------------------------
103
+ f('timestamp', { b: 'none', d: 'Waktu sekarang (ms)' }, () => String(Date.now())),
104
+ f('unix', { b: 'none', d: 'Waktu sekarang (detik)' }, () => String(Math.floor(Date.now() / 1000))),
105
+ f('date', { max: 2, s: '[format?;ms?]', d: 'Tanggal/jam. Token: YYYY MM DD HH mm ss. Zona waktu dari opsi timezone' },
106
+ (ctx, [format, ms]) => formatDate(
107
+ has(ms) ? num(ms, 'ms') : Date.now(),
108
+ has(format) ? format : 'YYYY-MM-DD HH:mm:ss',
109
+ ctx.bot.options.timezone,
110
+ )),
111
+ f('discordTime', { max: 2, s: '[ms?;gaya?]', d: 'Format waktu Discord <t:..>. Gaya: t T d D f F R' },
112
+ (ctx, [ms, style]) => {
113
+ const st = has(style) ? style : 'f';
114
+ if (!['t', 'T', 'd', 'D', 'f', 'F', 'R'].includes(st)) throw new BotError(`Gaya "${st}" tidak valid (t T d D f F R)`);
115
+ return `<t:${Math.floor((has(ms) ? num(ms, 'ms') : Date.now()) / 1000)}:${st}>`;
116
+ }),
117
+ ];
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+ const { BotError } = require('../errors');
3
+ const { f, has, truthy, num, limit } = require('./helpers');
4
+
5
+ module.exports = [
6
+ // ---- Modal ------------------------------------------------------------------
7
+ f('addModal', { b: 'required', min: 2, max: 2, s: '[customID;judul]', d: 'Tampilkan form pop-up (modal). Jadi respons pertama; teks lain diabaikan. Isi dengan $addTextInput' },
8
+ (ctx, [id, title]) => {
9
+ if (ctx.modal) throw new BotError('Hanya satu modal per respons');
10
+ if (!id.trim() || !title.trim()) throw new BotError('customID dan judul modal tidak boleh kosong');
11
+ ctx.modal = { custom_id: limit('customID', id.trim(), 100), title: limit('judul', title.trim(), 45), components: [] };
12
+ return '';
13
+ }),
14
+ f('addTextInput', { b: 'required', min: 2, max: 8, s: '[customID;label;gaya?;placeholder?;wajib?;min?;max?;nilaiAwal?]', d: 'Tambah kolom isian ke modal (maks 5). Gaya: short / paragraph' },
15
+ (ctx, [id, label, style, placeholder, required, min, max, value]) => {
16
+ if (!ctx.modal) throw new BotError('Pakai $addModal dulu sebelum $addTextInput');
17
+ if (ctx.modal.components.length >= 5) throw new BotError('Maksimal 5 kolom per modal');
18
+ if (!id.trim() || !label.trim()) throw new BotError('customID dan label kolom tidak boleh kosong');
19
+ const st = has(style) ? style.trim().toLowerCase() : 'short';
20
+ if (!['short', 'paragraph'].includes(st)) throw new BotError(`Gaya "${style}" tidak valid (short / paragraph)`);
21
+ const input = {
22
+ type: 4,
23
+ custom_id: limit('customID', id.trim(), 100),
24
+ label: limit('label', label.trim(), 45),
25
+ style: st === 'short' ? 1 : 2,
26
+ required: has(required) ? truthy(required) : true,
27
+ };
28
+ if (has(placeholder)) input.placeholder = limit('placeholder', placeholder, 100);
29
+ if (has(min)) input.min_length = num(min, 'min', { min: 0, max: 4000, int: true });
30
+ if (has(max)) input.max_length = num(max, 'max', { min: 1, max: 4000, int: true });
31
+ if (input.min_length !== undefined && input.max_length !== undefined && input.min_length > input.max_length) {
32
+ throw new BotError('min tidak boleh lebih besar dari max');
33
+ }
34
+ if (has(value)) input.value = limit('nilai awal', value, 4000);
35
+ ctx.modal.components.push({ type: 1, components: [input] });
36
+ return '';
37
+ }),
38
+ f('modalValue', { b: 'required', min: 1, max: 1, s: '[customIDKolom]', d: 'Isi kolom modal yang dikirim pengguna (dipakai di handler bot.modal)' },
39
+ (ctx, [id]) => ctx.fields.get(id.trim()) ?? ''),
40
+
41
+ // ---- Autocomplete -----------------------------------------------------------
42
+ f('focusedValue', { b: 'none', d: 'Autocomplete: teks yang sedang diketik pengguna' }, (ctx) => ctx.focused?.value ?? ''),
43
+ f('focusedOption', { b: 'none', d: 'Autocomplete: nama opsi yang sedang diisi' }, (ctx) => ctx.focused?.name ?? ''),
44
+ f('addChoice', { b: 'required', min: 1, max: 2, s: '[nama;nilai?]', d: 'Autocomplete: tambah satu pilihan (maks 25). Nilai bawaan = nama' },
45
+ (ctx, [name, value]) => {
46
+ if (ctx.kind !== 'autocomplete') throw new BotError('$addChoice hanya untuk handler bot.autocomplete');
47
+ if (ctx.choices.length >= 25) throw new BotError('Maksimal 25 pilihan autocomplete');
48
+ if (!name.trim()) throw new BotError('Nama pilihan tidak boleh kosong');
49
+ ctx.choices.push({ name: limit('nama pilihan', name.trim(), 100), value: limit('nilai pilihan', has(value) ? value : name.trim(), 100) });
50
+ return '';
51
+ }),
52
+
53
+ // ---- Target (context menu & event) ----------------------------------------
54
+ f('targetID', { b: 'none', d: 'ID sasaran: user/pesan yang diklik kanan, atau objek event (channel/role/thread/voice/ban)' }, (ctx) => ctx.target?.id ?? ''),
55
+ f('targetName', { b: 'none', d: 'Nama sasaran (username / nama channel / nama role)' }, (ctx) => ctx.target?.name ?? ''),
56
+ f('targetContent', { b: 'none', d: 'Isi pesan yang diklik kanan (context menu pesan)' }, (ctx) => ctx.target?.content ?? ''),
57
+ f('messageFrom', { b: 'required', min: 1, max: 1, s: '[nomor]', d: 'Seluruh teks argumen mulai dari kata ke-N (spasi dan baris baru dipertahankan)' },
58
+ (ctx, [n]) => {
59
+ let s = ctx.rawArgs;
60
+ for (let i = 1; i < num(n, 'nomor', { min: 1, int: true }); i++) s = s.replace(/^\S+\s*/, '');
61
+ return s;
62
+ }),
63
+ ];