chrxmaticc-framework 1.2.1 → 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.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * core/ChrxEmbedBuilder.js
3
+ * Simple and Advanced embed builder.
4
+ *
5
+ * Simple mode → key/value syntax (title, desc, color)
6
+ * AdvancedEmbed → parses raw JSON or HTML-like string into a full embed
7
+ */
8
+
9
+ const { EmbedBuilder } = require("discord.js");
10
+
11
+ class ChrxEmbedBuilder {
12
+ /**
13
+ * Simple mode — just pass title, desc, color etc.
14
+ * @param {object} options
15
+ * @param {string} [options.title]
16
+ * @param {string} [options.desc]
17
+ * @param {string} [options.color]
18
+ * @param {string} [options.thumbnail]
19
+ * @param {string} [options.image]
20
+ * @param {string} [options.footer]
21
+ * @param {boolean} [options.timestamp]
22
+ * @param {object[]} [options.fields] [{ name, value, inline? }]
23
+ */
24
+ static build(options = {}) {
25
+ const embed = new EmbedBuilder();
26
+
27
+ if (options.title) embed.setTitle(options.title);
28
+ if (options.desc) embed.setDescription(options.desc);
29
+ if (options.color) embed.setColor(options.color);
30
+ if (options.thumbnail) embed.setThumbnail(options.thumbnail);
31
+ if (options.image) embed.setImage(options.image);
32
+ if (options.footer) embed.setFooter({ text: options.footer });
33
+ if (options.timestamp) embed.setTimestamp();
34
+ if (options.fields) embed.addFields(options.fields);
35
+
36
+ return embed;
37
+ }
38
+
39
+ /**
40
+ * AdvancedEmbed — reads raw JSON string or HTML-like string and parses it.
41
+ * Supports both:
42
+ * - JSON: { "title": "Hi", "desc": "ok bro", "color": "#5865F2", ... }
43
+ * - HTML-like: <title>Hi</title><desc>ok bro</desc><color>#5865F2</color>
44
+ *
45
+ * @param {string|object} input Raw JSON string, HTML-like string, or plain object
46
+ * @returns {EmbedBuilder}
47
+ */
48
+ static advanced(input) {
49
+ let data = {};
50
+
51
+ if (typeof input === "object") {
52
+ // Already a plain object
53
+ data = input;
54
+
55
+ } else if (typeof input === "string") {
56
+ // Try JSON first
57
+ try {
58
+ data = JSON.parse(input);
59
+ } catch {
60
+ // Fall back to HTML-like tag parser
61
+ data = ChrxEmbedBuilder._parseHTML(input);
62
+ }
63
+ }
64
+
65
+ return ChrxEmbedBuilder.build(data);
66
+ }
67
+
68
+ /**
69
+ * Parse HTML-like tag syntax into a plain object.
70
+ * Supports: <title> <desc> <color> <thumbnail> <image> <footer> <timestamp> <field>
71
+ *
72
+ * Example input:
73
+ * <title>Hello!</title>
74
+ * <desc>This is my embed</desc>
75
+ * <color>#5865F2</color>
76
+ * <field name="Field 1" value="Value 1" inline="true"/>
77
+ */
78
+ static _parseHTML(html) {
79
+ const data = {};
80
+
81
+ const extract = (tag) => {
82
+ const match = html.match(new RegExp(`<${tag}[^>]*>(.*?)<\\/${tag}>`, "si"));
83
+ return match ? match[1].trim() : null;
84
+ };
85
+
86
+ data.title = extract("title") || undefined;
87
+ data.desc = extract("desc") || extract("description") || undefined;
88
+ data.color = extract("color") || undefined;
89
+ data.thumbnail = extract("thumbnail") || undefined;
90
+ data.image = extract("image") || undefined;
91
+ data.footer = extract("footer") || undefined;
92
+ data.timestamp = html.includes("<timestamp") ? true : undefined;
93
+
94
+ // Parse <field name="x" value="y" inline="true"/> tags
95
+ const fieldRegex = /<field\s+name="([^"]+)"\s+value="([^"]+)"(?:\s+inline="(true|false)")?[^/]*\/>/gi;
96
+ const fields = [];
97
+ let fieldMatch;
98
+ while ((fieldMatch = fieldRegex.exec(html)) !== null) {
99
+ fields.push({
100
+ name: fieldMatch[1],
101
+ value: fieldMatch[2],
102
+ inline: fieldMatch[3] === "true",
103
+ });
104
+ }
105
+ if (fields.length > 0) data.fields = fields;
106
+
107
+ return data;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * EmbedTrigger — fires a ChrxEmbedBuilder embed in a reply/send.
113
+ * Drop this anywhere in a ChrxCommandBuilder run() function.
114
+ *
115
+ * @param {import("discord.js").CommandInteraction} interaction
116
+ * @param {EmbedBuilder} embed
117
+ * @param {object} [options]
118
+ * @param {boolean} [options.ephemeral]
119
+ */
120
+ async function EmbedTrigger(interaction, embed, options = {}) {
121
+ if (interaction.replied || interaction.deferred) {
122
+ return interaction.followUp({ embeds: [embed], ephemeral: options.ephemeral ?? false });
123
+ }
124
+ return interaction.reply({ embeds: [embed], ephemeral: options.ephemeral ?? false });
125
+ }
126
+
127
+ module.exports = { ChrxEmbedBuilder, EmbedTrigger };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * core/ChrxModalBuilder.js
3
+ * Simple modal builder with built in validation and rate limiting.
4
+ */
5
+
6
+ const {
7
+ ModalBuilder,
8
+ TextInputBuilder,
9
+ TextInputStyle,
10
+ ActionRowBuilder,
11
+ } = require("discord.js");
12
+
13
+ // ── Rate limiter ──────────────────────────────────────────────────────────
14
+ // Tracks how many modals a user has triggered in the last minute
15
+ const modalRateLimit = new Map(); // userId -> { count, resetAt }
16
+ const RATE_LIMIT = 50; // max modals per minute per user
17
+
18
+ function checkRateLimit(userId) {
19
+ const now = Date.now();
20
+ const data = modalRateLimit.get(userId);
21
+
22
+ if (!data || now > data.resetAt) {
23
+ modalRateLimit.set(userId, { count: 1, resetAt: now + 60000 });
24
+ return true;
25
+ }
26
+
27
+ if (data.count >= RATE_LIMIT) return false;
28
+
29
+ data.count++;
30
+ return true;
31
+ }
32
+
33
+ // ── Type map ──────────────────────────────────────────────────────────────
34
+ const STYLE_MAP = {
35
+ short: TextInputStyle.Short,
36
+ paragraph: TextInputStyle.Paragraph,
37
+ long: TextInputStyle.Paragraph, // alias
38
+ };
39
+
40
+ class ChrxModalBuilder {
41
+ /**
42
+ * @param {object} options
43
+ * @param {string} options.name Internal name (for reference)
44
+ * @param {string} options.id Modal custom ID
45
+ * @param {string} options.title Modal title (max 45 chars)
46
+ * @param {object[]} options.inputs Input fields (max 5)
47
+ * @param {string} options.inputs[].id Input custom ID
48
+ * @param {string} options.inputs[].label Input label (max 100 chars)
49
+ * @param {string} [options.inputs[].type] "short" | "paragraph" (default: short)
50
+ * @param {boolean} [options.inputs[].required] Required? (default: false)
51
+ * @param {string} [options.inputs[].placeholder] Placeholder text
52
+ * @param {string} [options.inputs[].value] Pre-filled value
53
+ * @param {number} [options.inputs[].minLength]
54
+ * @param {number} [options.inputs[].maxLength]
55
+ */
56
+ constructor(options = {}) {
57
+ if (!options.id) throw new Error("[ChrxModal] id is required.");
58
+ if (!options.title) throw new Error("[ChrxModal] title is required.");
59
+ if (!options.inputs || options.inputs.length === 0) throw new Error("[ChrxModal] At least one input is required.");
60
+
61
+ // ── Validation ────────────────────────────────────────────────────────
62
+ if (options.title.length > 45) {
63
+ throw new Error(`[ChrxModal] Title exceeds 45 characters (got ${options.title.length}).`);
64
+ }
65
+ if (options.inputs.length > 5) {
66
+ throw new Error(`[ChrxModal] Maximum 5 inputs allowed (got ${options.inputs.length}).`);
67
+ }
68
+ for (const input of options.inputs) {
69
+ if (!input.id) throw new Error("[ChrxModal] Every input must have an id.");
70
+ if (!input.label) throw new Error("[ChrxModal] Every input must have a label.");
71
+ if (input.label.length > 100) {
72
+ throw new Error(`[ChrxModal] Input label "${input.label}" exceeds 100 characters.`);
73
+ }
74
+ }
75
+
76
+ this._options = options;
77
+ this.name = options.name ?? options.id;
78
+ }
79
+
80
+ /**
81
+ * Build and return the raw discord.js Modal.
82
+ * @returns {ModalBuilder}
83
+ */
84
+ build() {
85
+ const modal = new ModalBuilder()
86
+ .setCustomId(this._options.id)
87
+ .setTitle(this._options.title);
88
+
89
+ const rows = this._options.inputs.map((input) => {
90
+ const textInput = new TextInputBuilder()
91
+ .setCustomId(input.id)
92
+ .setLabel(input.label)
93
+ .setStyle(STYLE_MAP[input.type ?? "short"] ?? TextInputStyle.Short);
94
+
95
+ if (input.required) textInput.setRequired(input.required);
96
+ if (input.placeholder) textInput.setPlaceholder(input.placeholder);
97
+ if (input.value) textInput.setValue(input.value);
98
+ if (input.minLength) textInput.setMinLength(input.minLength);
99
+ if (input.maxLength) textInput.setMaxLength(input.maxLength);
100
+
101
+ return new ActionRowBuilder().addComponents(textInput);
102
+ });
103
+
104
+ modal.addComponents(...rows);
105
+ return modal;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * ModalTrigger — shows a ChrxModalBuilder modal to the user.
111
+ * Handles rate limiting automatically.
112
+ *
113
+ * @param {import("discord.js").CommandInteraction} interaction
114
+ * @param {ChrxModalBuilder} modal
115
+ */
116
+ async function ModalTrigger(interaction, modal) {
117
+ if (!checkRateLimit(interaction.user.id)) {
118
+ return interaction.reply({
119
+ content: "⏱ You're triggering too many modals! Slow down.",
120
+ ephemeral: true,
121
+ });
122
+ }
123
+
124
+ await interaction.showModal(modal.build());
125
+ }
126
+
127
+ /**
128
+ * onModalSubmit — listen for a modal submission by ID.
129
+ * @param {import("discord.js").Client} client
130
+ * @param {string} modalId
131
+ * @param {Function} handler (interaction) => void
132
+ */
133
+ function onModalSubmit(client, modalId, handler) {
134
+ client.on("interactionCreate", async (interaction) => {
135
+ if (!interaction.isModalSubmit()) return;
136
+ if (interaction.customId !== modalId) return;
137
+ try {
138
+ await handler(interaction);
139
+ } catch (err) {
140
+ console.error(`[ChrxModal] Error in modal "${modalId}":`, err);
141
+ const msg = { content: err.message || "❌ Something went wrong.", ephemeral: true };
142
+ if (interaction.replied || interaction.deferred) {
143
+ interaction.followUp(msg).catch(() => {});
144
+ } else {
145
+ interaction.reply(msg).catch(() => {});
146
+ }
147
+ }
148
+ });
149
+ }
150
+
151
+ module.exports = { ChrxModalBuilder, ModalTrigger, onModalSubmit };
@@ -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
+ };