chrxmaticc-framework 1.3.0 → 1.4.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.
- package/core/ChrxCommandBuilder.js +17 -1
- package/core/ChrxReply.js +236 -0
- package/core/ChrxRouter.js +233 -0
- package/core/ChrxTimedReply.js +50 -0
- package/index.js +24 -16
- package/package.json +1 -1
|
@@ -52,7 +52,6 @@ class ChrxCommandBuilder {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// ── Critical: expose data and execute so CommandLoader + deploy-commands picks it up ──
|
|
56
55
|
this.data = builder;
|
|
57
56
|
this.execute = this._execute.bind(this);
|
|
58
57
|
}
|
|
@@ -61,20 +60,24 @@ class ChrxCommandBuilder {
|
|
|
61
60
|
const client = interaction.client;
|
|
62
61
|
const opts = this._options;
|
|
63
62
|
|
|
63
|
+
// ── Guild only ────────────────────────────────────────────────────────
|
|
64
64
|
if (opts.guildOnly !== false && !interaction.guild) {
|
|
65
65
|
return interaction.reply({ content: "❌ This command can only be used in a server.", ephemeral: true });
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// ── Owner only ────────────────────────────────────────────────────────
|
|
68
69
|
if (opts.ownerOnly && interaction.user.id !== process.env.OWNER_ID) {
|
|
69
70
|
return interaction.reply({ content: "❌ This command is restricted to the bot owner.", ephemeral: true });
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// ── Permission check ──────────────────────────────────────────────────
|
|
72
74
|
if (opts.permission) {
|
|
73
75
|
if (!interaction.member?.permissions.has(PermissionFlagsBits[opts.permission])) {
|
|
74
76
|
return interaction.reply({ content: `❌ You need the **${opts.permission}** permission to use this.`, ephemeral: true });
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
// ── Cooldown ──────────────────────────────────────────────────────────
|
|
78
81
|
if (opts.cooldown) {
|
|
79
82
|
const key = `${interaction.user.id}-${opts.name}`;
|
|
80
83
|
const now = Date.now();
|
|
@@ -91,6 +94,18 @@ class ChrxCommandBuilder {
|
|
|
91
94
|
setTimeout(() => this._cooldowns.delete(key), opts.cooldown * 1000);
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
// ── TimedReply — auto defer if timedReply option is set ───────────────
|
|
98
|
+
if (opts.timedReply) {
|
|
99
|
+
const minutes = opts.timedReply.minutes ?? 1;
|
|
100
|
+
const ms = Math.min(minutes * 60 * 1000, 15 * 60 * 1000); // Discord hard cap is 15min
|
|
101
|
+
if (ms > 2500) {
|
|
102
|
+
// Only defer if we expect the command to take longer than ~2.5s
|
|
103
|
+
await interaction.deferReply({ ephemeral: opts.timedReply.ephemeral ?? false });
|
|
104
|
+
}
|
|
105
|
+
console.log(`[ChrxCommand] /${opts.name} timedReply active — ${minutes}min window`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Plugin injection ──────────────────────────────────────────────────
|
|
94
109
|
const plugins = {
|
|
95
110
|
economy: client.chrx?.economy,
|
|
96
111
|
moderation: client.chrx?.moderation,
|
|
@@ -106,6 +121,7 @@ class ChrxCommandBuilder {
|
|
|
106
121
|
db: client.db,
|
|
107
122
|
};
|
|
108
123
|
|
|
124
|
+
// ── Run ───────────────────────────────────────────────────────────────
|
|
109
125
|
try {
|
|
110
126
|
await opts.run(interaction, plugins);
|
|
111
127
|
} catch (err) {
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/ChrxReply.js
|
|
3
|
+
* Unified reply system — every Discord reply type simplified into clean triggers.
|
|
4
|
+
*
|
|
5
|
+
* Includes:
|
|
6
|
+
* - ChrxReply → interaction.reply()
|
|
7
|
+
* - ChrxDeferReply → interaction.deferReply()
|
|
8
|
+
* - ChrxEditReply → interaction.editReply()
|
|
9
|
+
* - ChrxFollowUp → interaction.followUp()
|
|
10
|
+
* - ChrxFetchReply → interaction.fetchReply()
|
|
11
|
+
* - ChrxSilentReply → ephemeral reply shortcut
|
|
12
|
+
* - ChrxAutoReply → smart reply (detects state automatically)
|
|
13
|
+
* - ChrxThinkReply → defer + slow handler + editReply
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Normalize input into a Discord reply payload.
|
|
20
|
+
* Accepts: string, EmbedBuilder, or plain payload object.
|
|
21
|
+
*/
|
|
22
|
+
function normalize(input, extra = {}) {
|
|
23
|
+
if (!input) return extra;
|
|
24
|
+
|
|
25
|
+
if (typeof input === "string") {
|
|
26
|
+
return { content: input, ...extra };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// EmbedBuilder instance
|
|
30
|
+
if (input?.data || input?.setTitle) {
|
|
31
|
+
return { embeds: [input], ...extra };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Array of embeds
|
|
35
|
+
if (Array.isArray(input)) {
|
|
36
|
+
return { embeds: input, ...extra };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Plain object payload — merge with extra
|
|
40
|
+
return { ...input, ...extra };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
44
|
+
// ChrxReply — normal reply
|
|
45
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
49
|
+
* @param {string|object|import("discord.js").EmbedBuilder} content
|
|
50
|
+
* @param {object} [options]
|
|
51
|
+
* @param {boolean} [options.ephemeral]
|
|
52
|
+
* @param {object[]} [options.components]
|
|
53
|
+
* @param {object[]} [options.files]
|
|
54
|
+
*/
|
|
55
|
+
async function ChrxReply(interaction, content, options = {}) {
|
|
56
|
+
try {
|
|
57
|
+
return await interaction.reply(normalize(content, options));
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error("[ChrxReply] Error:", err.message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
64
|
+
// ChrxDeferReply — defer the interaction
|
|
65
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
69
|
+
* @param {object} [options]
|
|
70
|
+
* @param {boolean} [options.ephemeral]
|
|
71
|
+
*/
|
|
72
|
+
async function ChrxDeferReply(interaction, options = {}) {
|
|
73
|
+
try {
|
|
74
|
+
if (interaction.deferred || interaction.replied) return;
|
|
75
|
+
return await interaction.deferReply({ ephemeral: options.ephemeral ?? false });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error("[ChrxDeferReply] Error:", err.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
82
|
+
// ChrxEditReply — edit an existing reply
|
|
83
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
87
|
+
* @param {string|object|import("discord.js").EmbedBuilder} content
|
|
88
|
+
* @param {object} [options]
|
|
89
|
+
*/
|
|
90
|
+
async function ChrxEditReply(interaction, content, options = {}) {
|
|
91
|
+
try {
|
|
92
|
+
// Auto defer if not already deferred or replied
|
|
93
|
+
if (!interaction.deferred && !interaction.replied) {
|
|
94
|
+
await interaction.deferReply({ ephemeral: options.ephemeral ?? false });
|
|
95
|
+
}
|
|
96
|
+
return await interaction.editReply(normalize(content, options));
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error("[ChrxEditReply] Error:", err.message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
103
|
+
// ChrxFollowUp — follow up message
|
|
104
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
108
|
+
* @param {string|object|import("discord.js").EmbedBuilder} content
|
|
109
|
+
* @param {object} [options]
|
|
110
|
+
* @param {boolean} [options.ephemeral]
|
|
111
|
+
*/
|
|
112
|
+
async function ChrxFollowUp(interaction, content, options = {}) {
|
|
113
|
+
try {
|
|
114
|
+
return await interaction.followUp(normalize(content, options));
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error("[ChrxFollowUp] Error:", err.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
121
|
+
// ChrxFetchReply — fetch the reply message object
|
|
122
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
126
|
+
* @returns {Promise<import("discord.js").Message>}
|
|
127
|
+
*/
|
|
128
|
+
async function ChrxFetchReply(interaction) {
|
|
129
|
+
try {
|
|
130
|
+
return await interaction.fetchReply();
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error("[ChrxFetchReply] Error:", err.message);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
138
|
+
// ChrxSilentReply — ephemeral reply shortcut
|
|
139
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Always sends as ephemeral. No options needed.
|
|
143
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
144
|
+
* @param {string|object|import("discord.js").EmbedBuilder} content
|
|
145
|
+
*/
|
|
146
|
+
async function ChrxSilentReply(interaction, content) {
|
|
147
|
+
try {
|
|
148
|
+
if (interaction.deferred || interaction.replied) {
|
|
149
|
+
return await interaction.followUp({ ...normalize(content), ephemeral: true });
|
|
150
|
+
}
|
|
151
|
+
return await interaction.reply({ ...normalize(content), ephemeral: true });
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error("[ChrxSilentReply] Error:", err.message);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
158
|
+
// ChrxAutoReply — smart reply, detects state automatically
|
|
159
|
+
// Never throws "interaction already replied" — picks the right method every time
|
|
160
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
164
|
+
* @param {string|object|import("discord.js").EmbedBuilder} content
|
|
165
|
+
* @param {object} [options]
|
|
166
|
+
* @param {boolean} [options.ephemeral]
|
|
167
|
+
*/
|
|
168
|
+
async function ChrxAutoReply(interaction, content, options = {}) {
|
|
169
|
+
try {
|
|
170
|
+
const payload = normalize(content, options);
|
|
171
|
+
|
|
172
|
+
if (interaction.replied) {
|
|
173
|
+
return await interaction.followUp(payload);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (interaction.deferred) {
|
|
177
|
+
return await interaction.editReply(payload);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return await interaction.reply(payload);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error("[ChrxAutoReply] Error:", err.message);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
187
|
+
// ChrxThinkReply — defer + slow async handler + auto editReply
|
|
188
|
+
// Perfect for AI commands, database queries, anything slow
|
|
189
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
193
|
+
* @param {Function} handler Async function that returns the reply content
|
|
194
|
+
* @param {object} [options]
|
|
195
|
+
* @param {boolean} [options.ephemeral]
|
|
196
|
+
* @param {string} [options.errorMessage] Custom error message on failure
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* await ChrxThinkReply(interaction, async () => {
|
|
200
|
+
* const result = await someSlowDatabaseCall();
|
|
201
|
+
* return `Result: ${result}`;
|
|
202
|
+
* });
|
|
203
|
+
*/
|
|
204
|
+
async function ChrxThinkReply(interaction, handler, options = {}) {
|
|
205
|
+
try {
|
|
206
|
+
if (!interaction.deferred && !interaction.replied) {
|
|
207
|
+
await interaction.deferReply({ ephemeral: options.ephemeral ?? false });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result = await handler();
|
|
211
|
+
|
|
212
|
+
if (!result) return;
|
|
213
|
+
|
|
214
|
+
return await interaction.editReply(normalize(result));
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error("[ChrxThinkReply] Error:", err.message);
|
|
217
|
+
await interaction.editReply({
|
|
218
|
+
content: options.errorMessage || err.message || "❌ Something went wrong.",
|
|
219
|
+
}).catch(() => {});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
224
|
+
// Exports
|
|
225
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
ChrxReply,
|
|
229
|
+
ChrxDeferReply,
|
|
230
|
+
ChrxEditReply,
|
|
231
|
+
ChrxFollowUp,
|
|
232
|
+
ChrxFetchReply,
|
|
233
|
+
ChrxSilentReply,
|
|
234
|
+
ChrxAutoReply,
|
|
235
|
+
ChrxThinkReply,
|
|
236
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/ChrxRouter.js
|
|
3
|
+
* Global interaction router — register handlers once, auto-routes everything.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Buttons
|
|
7
|
+
* - Select menus
|
|
8
|
+
* - Modal submits
|
|
9
|
+
* - Context menus
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* ChrxRouter.init(client);
|
|
13
|
+
* ChrxRouter.button("ban_confirm", async (i) => i.reply("✅ Banned!"));
|
|
14
|
+
* ChrxRouter.select("role_select", async (i, values) => i.reply(`Picked: ${values[0]}`));
|
|
15
|
+
* ChrxRouter.modal("feedback_modal", async (i, values) => i.reply(`Got: ${values.feedback}`));
|
|
16
|
+
* ChrxRouter.context("Get Avatar", async (i) => i.reply(i.targetUser.displayAvatarURL()));
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { ComponentType } = require("discord.js");
|
|
20
|
+
|
|
21
|
+
// ── Route registries ──────────────────────────────────────────────────────
|
|
22
|
+
const buttonRoutes = new Map(); // customId -> handler
|
|
23
|
+
const selectRoutes = new Map(); // customId -> handler
|
|
24
|
+
const modalRoutes = new Map(); // customId -> handler
|
|
25
|
+
const contextRoutes = new Map(); // name -> handler
|
|
26
|
+
|
|
27
|
+
// ── Middleware ────────────────────────────────────────────────────────────
|
|
28
|
+
const middlewares = [];
|
|
29
|
+
|
|
30
|
+
// ── Cooldowns ─────────────────────────────────────────────────────────────
|
|
31
|
+
const cooldowns = new Map(); // `${userId}-${id}` -> timestamp
|
|
32
|
+
|
|
33
|
+
let _initialized = false;
|
|
34
|
+
|
|
35
|
+
const ChrxRouter = {
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize the router — call this once after bot is ready.
|
|
39
|
+
* @param {import("discord.js").Client} client
|
|
40
|
+
*/
|
|
41
|
+
init(client) {
|
|
42
|
+
if (_initialized) return;
|
|
43
|
+
_initialized = true;
|
|
44
|
+
|
|
45
|
+
client.on("interactionCreate", async (interaction) => {
|
|
46
|
+
try {
|
|
47
|
+
// ── Run middleware ────────────────────────────────────────────────
|
|
48
|
+
for (const mw of middlewares) {
|
|
49
|
+
const pass = await mw(interaction);
|
|
50
|
+
if (pass === false) return; // middleware blocked it
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Button ────────────────────────────────────────────────────────
|
|
54
|
+
if (interaction.isButton()) {
|
|
55
|
+
const handler = ChrxRouter._resolve(buttonRoutes, interaction.customId);
|
|
56
|
+
if (handler) await handler(interaction);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Select menu ───────────────────────────────────────────────────
|
|
61
|
+
if (interaction.isStringSelectMenu()) {
|
|
62
|
+
const handler = ChrxRouter._resolve(selectRoutes, interaction.customId);
|
|
63
|
+
if (handler) await handler(interaction, interaction.values);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Modal submit ──────────────────────────────────────────────────
|
|
68
|
+
if (interaction.isModalSubmit()) {
|
|
69
|
+
const handler = ChrxRouter._resolve(modalRoutes, interaction.customId);
|
|
70
|
+
if (handler) {
|
|
71
|
+
// Build values object keyed by field customId
|
|
72
|
+
const values = {};
|
|
73
|
+
for (const [key, field] of interaction.fields.fields) {
|
|
74
|
+
values[key] = field.value;
|
|
75
|
+
}
|
|
76
|
+
await handler(interaction, values);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Context menu ──────────────────────────────────────────────────
|
|
82
|
+
if (interaction.isContextMenuCommand()) {
|
|
83
|
+
const handler = contextRoutes.get(interaction.commandName);
|
|
84
|
+
if (handler) await handler(interaction);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error("[ChrxRouter] Unhandled error:", err.message);
|
|
90
|
+
const msg = { content: err.message || "❌ Something went wrong.", ephemeral: true };
|
|
91
|
+
try {
|
|
92
|
+
if (interaction.replied || interaction.deferred) {
|
|
93
|
+
await interaction.followUp(msg);
|
|
94
|
+
} else {
|
|
95
|
+
await interaction.reply(msg);
|
|
96
|
+
}
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
console.log("[ChrxRouter] Initialized — routing all interactions globally.");
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// ── Internal resolver — supports wildcards ────────────────────────────
|
|
105
|
+
// e.g. "ban_confirm_*" matches "ban_confirm_123456"
|
|
106
|
+
_resolve(registry, id) {
|
|
107
|
+
// Exact match first
|
|
108
|
+
if (registry.has(id)) return registry.get(id);
|
|
109
|
+
|
|
110
|
+
// Wildcard match
|
|
111
|
+
for (const [pattern, handler] of registry) {
|
|
112
|
+
if (pattern.endsWith("*") && id.startsWith(pattern.slice(0, -1))) {
|
|
113
|
+
return handler;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
121
|
+
// Registration methods
|
|
122
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Register a button handler.
|
|
126
|
+
* @param {string} id Button customId (supports wildcard e.g. "confirm_*")
|
|
127
|
+
* @param {Function} handler async (interaction) => void
|
|
128
|
+
* @param {object} [options]
|
|
129
|
+
* @param {number} [options.cooldown] Cooldown in seconds per user
|
|
130
|
+
* @param {boolean} [options.ownerOnly] Restrict to OWNER_ID
|
|
131
|
+
*/
|
|
132
|
+
button(id, handler, options = {}) {
|
|
133
|
+
buttonRoutes.set(id, ChrxRouter._wrap(handler, id, options));
|
|
134
|
+
return ChrxRouter; // chainable
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Register a select menu handler.
|
|
139
|
+
* @param {string} id Select menu customId
|
|
140
|
+
* @param {Function} handler async (interaction, values) => void
|
|
141
|
+
* @param {object} [options]
|
|
142
|
+
*/
|
|
143
|
+
select(id, handler, options = {}) {
|
|
144
|
+
selectRoutes.set(id, ChrxRouter._wrap(handler, id, options));
|
|
145
|
+
return ChrxRouter;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Register a modal submit handler.
|
|
150
|
+
* @param {string} id Modal customId
|
|
151
|
+
* @param {Function} handler async (interaction, values) => void
|
|
152
|
+
* values = { inputId: "user typed value", ... }
|
|
153
|
+
* @param {object} [options]
|
|
154
|
+
*/
|
|
155
|
+
modal(id, handler, options = {}) {
|
|
156
|
+
modalRoutes.set(id, ChrxRouter._wrap(handler, id, options));
|
|
157
|
+
return ChrxRouter;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Register a context menu handler.
|
|
162
|
+
* @param {string} name Context menu command name
|
|
163
|
+
* @param {Function} handler async (interaction) => void
|
|
164
|
+
*/
|
|
165
|
+
context(name, handler) {
|
|
166
|
+
contextRoutes.set(name, handler);
|
|
167
|
+
return ChrxRouter;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Remove a registered route.
|
|
172
|
+
* @param {"button"|"select"|"modal"|"context"} type
|
|
173
|
+
* @param {string} id
|
|
174
|
+
*/
|
|
175
|
+
remove(type, id) {
|
|
176
|
+
const map = { button: buttonRoutes, select: selectRoutes, modal: modalRoutes, context: contextRoutes }[type];
|
|
177
|
+
if (map) map.delete(id);
|
|
178
|
+
return ChrxRouter;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Add global middleware — runs before every routed interaction.
|
|
183
|
+
* Return false to block the interaction.
|
|
184
|
+
* @param {Function} fn async (interaction) => boolean | void
|
|
185
|
+
*/
|
|
186
|
+
use(fn) {
|
|
187
|
+
middlewares.push(fn);
|
|
188
|
+
return ChrxRouter;
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* List all registered routes (useful for debugging).
|
|
193
|
+
*/
|
|
194
|
+
list() {
|
|
195
|
+
return {
|
|
196
|
+
buttons: [...buttonRoutes.keys()],
|
|
197
|
+
selects: [...selectRoutes.keys()],
|
|
198
|
+
modals: [...modalRoutes.keys()],
|
|
199
|
+
contexts: [...contextRoutes.keys()],
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// ── Internal: wrap handler with cooldown + ownerOnly ──────────────────
|
|
204
|
+
_wrap(handler, id, options = {}) {
|
|
205
|
+
return async (interaction, ...args) => {
|
|
206
|
+
// Owner only
|
|
207
|
+
if (options.ownerOnly && interaction.user.id !== process.env.OWNER_ID) {
|
|
208
|
+
return interaction.reply({ content: "❌ This is restricted to the bot owner.", ephemeral: true });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Cooldown
|
|
212
|
+
if (options.cooldown) {
|
|
213
|
+
const key = `${interaction.user.id}-${id}`;
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
if (cooldowns.has(key)) {
|
|
216
|
+
const remaining = (options.cooldown * 1000) - (now - cooldowns.get(key));
|
|
217
|
+
if (remaining > 0) {
|
|
218
|
+
return interaction.reply({
|
|
219
|
+
content: `⏱ Wait **${(remaining / 1000).toFixed(1)}s** before doing that again.`,
|
|
220
|
+
ephemeral: true,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
cooldowns.set(key, now);
|
|
225
|
+
setTimeout(() => cooldowns.delete(key), options.cooldown * 1000);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await handler(interaction, ...args);
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
module.exports = ChrxRouter;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/ChrxTimedReply.js
|
|
3
|
+
* Extended deferReply — keeps the interaction alive for a custom duration
|
|
4
|
+
* and handles the reply automatically when your function resolves.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* await ChrxTimedReply(interaction, async () => "Pong!");
|
|
8
|
+
* await ChrxTimedReply(interaction, async () => ({ content: "Pong!", ephemeral: true }));
|
|
9
|
+
* await ChrxTimedReply(interaction, async () => ({ embeds: [myEmbed] }));
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {import("discord.js").CommandInteraction} interaction
|
|
14
|
+
* @param {Function} handler Async function that returns a string, object, or EmbedBuilder
|
|
15
|
+
* @param {object} [options]
|
|
16
|
+
* @param {boolean} [options.ephemeral] Defer as ephemeral (default: false)
|
|
17
|
+
*/
|
|
18
|
+
async function ChrxTimedReply(interaction, handler, options = {}) {
|
|
19
|
+
// Defer immediately to keep the interaction token alive
|
|
20
|
+
await interaction.deferReply({ ephemeral: options.ephemeral ?? false });
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const result = await handler();
|
|
24
|
+
|
|
25
|
+
if (!result) return;
|
|
26
|
+
|
|
27
|
+
// ── Normalize result into a payload ───────────────────────────────────
|
|
28
|
+
let payload = {};
|
|
29
|
+
|
|
30
|
+
if (typeof result === "string") {
|
|
31
|
+
payload.content = result;
|
|
32
|
+
} else if (result?.setTitle || result?.data?.title) {
|
|
33
|
+
// EmbedBuilder instance
|
|
34
|
+
payload.embeds = [result];
|
|
35
|
+
} else {
|
|
36
|
+
// Plain object — pass through as-is
|
|
37
|
+
payload = result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await interaction.editReply(payload);
|
|
41
|
+
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error("[ChrxTimedReply] Handler error:", err);
|
|
44
|
+
await interaction.editReply({
|
|
45
|
+
content: err.message || "❌ Something went wrong.",
|
|
46
|
+
}).catch(() => {});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = ChrxTimedReply;
|
package/index.js
CHANGED
|
@@ -9,26 +9,22 @@ const XPSystem = require("./core/XPSystem");
|
|
|
9
9
|
const AIWrapper = require("./core/AIWrapper");
|
|
10
10
|
const MusicManager = require("./core/MusicManager");
|
|
11
11
|
const ChrxCommandBuilder = require("./core/ChrxCommandBuilder");
|
|
12
|
-
const
|
|
12
|
+
const ChrxTimedReply = require("./core/ChrxTimedReply");
|
|
13
|
+
const ChrxRouter = require("./core/ChrxRouter");
|
|
14
|
+
const { ChrxEmbedBuilder, EmbedTrigger } = require("./core/ChrxEmbedBuilder");
|
|
13
15
|
const { ChrxModalBuilder, ModalTrigger, onModalSubmit } = require("./core/ChrxModalBuilder");
|
|
14
16
|
const {
|
|
15
|
-
ChrxButton,
|
|
16
|
-
|
|
17
|
-
ChrxPagination,
|
|
18
|
-
ChrxContextMenu,
|
|
19
|
-
ChrxMenu,
|
|
20
|
-
ChrxForm,
|
|
21
|
-
ChrxConfirm,
|
|
17
|
+
ChrxButton, ChrxSelectMenu, ChrxPagination,
|
|
18
|
+
ChrxContextMenu, ChrxMenu, ChrxForm, ChrxConfirm,
|
|
22
19
|
} = require("./core/ChrxComponentsV3");
|
|
23
20
|
const {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
startEndMarkerWatcher,
|
|
31
|
-
stopEndMarkerWatcher,
|
|
21
|
+
ChrxReply, ChrxDeferReply, ChrxEditReply,
|
|
22
|
+
ChrxFollowUp, ChrxFetchReply, ChrxSilentReply,
|
|
23
|
+
ChrxAutoReply, ChrxThinkReply,
|
|
24
|
+
} = require("./core/ChrxReply");
|
|
25
|
+
const {
|
|
26
|
+
parseTime, formatTime, setMarker, getMarkers,
|
|
27
|
+
clearMarker, applyStartMarker, startEndMarkerWatcher, stopEndMarkerWatcher,
|
|
32
28
|
} = require("./core/songMarkers");
|
|
33
29
|
|
|
34
30
|
// ── Plugins ───────────────────────────────────────────────────────────────
|
|
@@ -46,6 +42,18 @@ module.exports = {
|
|
|
46
42
|
// Core
|
|
47
43
|
ChrxClient,
|
|
48
44
|
ChrxCommandBuilder,
|
|
45
|
+
ChrxRouter,
|
|
46
|
+
|
|
47
|
+
// Reply system
|
|
48
|
+
ChrxReply,
|
|
49
|
+
ChrxDeferReply,
|
|
50
|
+
ChrxEditReply,
|
|
51
|
+
ChrxFollowUp,
|
|
52
|
+
ChrxFetchReply,
|
|
53
|
+
ChrxSilentReply,
|
|
54
|
+
ChrxAutoReply,
|
|
55
|
+
ChrxThinkReply,
|
|
56
|
+
ChrxTimedReply,
|
|
49
57
|
|
|
50
58
|
// Embed & Modal
|
|
51
59
|
ChrxEmbedBuilder,
|