chrxmaticc-framework 1.1.0 → 1.1.2

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/index.js CHANGED
@@ -1,13 +1,10 @@
1
- /**
2
- * index.js
3
- * Main entry point — exports everything.
4
- */
5
-
6
- const { ChrxClient } = require("./Client");
7
- const Database = require("./Database");
8
- const XPSystem = require("./XPSystem");
9
- const AIWrapper = require("./AIWrapper");
10
- const MusicManager = require("./MusicManager");
1
+ // FINALLY IT L0ADS FUCKING PLUGINS AND FUCKING CORE SHIT
2
+ // Chrxmaticc Framework is so tuff gng💔
3
+ const { ChrxClient } = require("./core/Client");
4
+ const Database = require("./core/Database");
5
+ const XPSystem = require("./core/XPSystem");
6
+ const AIWrapper = require("./core/AIWrapper");
7
+ const MusicManager = require("./core/MusicManager");
11
8
  const {
12
9
  parseTime,
13
10
  formatTime,
@@ -17,7 +14,7 @@ const {
17
14
  applyStartMarker,
18
15
  startEndMarkerWatcher,
19
16
  stopEndMarkerWatcher,
20
- } = require("./songMarkers");
17
+ } = require("./core/songMarkers");
21
18
 
22
19
  // ── Plugins ───────────────────────────────────────────────────────────────
23
20
  const Economy = require("./plugins/Economy");
@@ -31,16 +28,11 @@ const Starboard = require("./plugins/Starboard");
31
28
  const AutoMod = require("./plugins/AutoMod");
32
29
 
33
30
  module.exports = {
34
- // Core
35
31
  ChrxClient,
36
-
37
- // Modules
38
32
  Database,
39
33
  XPSystem,
40
34
  AIWrapper,
41
35
  MusicManager,
42
-
43
- // Song marker utilities
44
36
  parseTime,
45
37
  formatTime,
46
38
  setMarker,
@@ -49,8 +41,6 @@ module.exports = {
49
41
  applyStartMarker,
50
42
  startEndMarkerWatcher,
51
43
  stopEndMarkerWatcher,
52
-
53
- // Plugins
54
44
  Economy,
55
45
  Moderation,
56
46
  Giveaways,
@@ -60,4 +50,4 @@ module.exports = {
60
50
  Reminders,
61
51
  Starboard,
62
52
  AutoMod,
63
- };
53
+ };
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "chrxmaticc-framework",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "A batteries-included Discord bot framework with music, AI, XP and database support.",
5
5
  "main": "index.js",
6
6
  "files": [
7
- "index.js",
8
- "*.js",
9
- "core/**/*"
7
+ "index.js",
8
+ "*.js",
9
+ "core/**/*",
10
+ "plugins/**/*"
11
+
10
12
  ],
11
13
  "keywords": [
12
14
  "discord",
@@ -0,0 +1,144 @@
1
+ /**
2
+ * plugins/AutoMod.js
3
+ * AutoMod — bad word filter, link blocker, caps spam detection.
4
+ */
5
+
6
+ const { EmbedBuilder } = require("discord.js");
7
+
8
+ class AutoMod {
9
+ constructor(client) {
10
+ this.client = client;
11
+ this.configs = new Map(); // guildId -> config
12
+ this.logChannels = new Map(); // guildId -> channelId
13
+ this._registerEvents();
14
+ }
15
+
16
+ /**
17
+ * Configure AutoMod for a guild.
18
+ * @param {string} guildId
19
+ * @param {object} options
20
+ * @param {string[]} [options.bannedWords] Words to filter
21
+ * @param {boolean} [options.blockLinks] Block all links
22
+ * @param {string[]} [options.allowedDomains] Domains to allow if blockLinks is true
23
+ * @param {number} [options.capsThreshold] % caps before flagging (0-100, default 70)
24
+ * @param {number} [options.capsMinLength] Min message length to check caps (default 10)
25
+ * @param {string} [options.logChannelId] Channel to log violations
26
+ */
27
+ setup(guildId, options) {
28
+ this.configs.set(guildId, {
29
+ bannedWords: [],
30
+ blockLinks: false,
31
+ allowedDomains: [],
32
+ capsThreshold: 70,
33
+ capsMinLength: 10,
34
+ ...options,
35
+ });
36
+ if (options.logChannelId) this.logChannels.set(guildId, options.logChannelId);
37
+ }
38
+
39
+ async _log(guildId, embed) {
40
+ const channelId = this.logChannels.get(guildId);
41
+ if (!channelId) return;
42
+ const channel = this.client.channels.cache.get(channelId);
43
+ if (channel) channel.send({ embeds: [embed] }).catch(() => {});
44
+ }
45
+
46
+ _registerEvents() {
47
+ this.client.on("messageCreate", async (message) => {
48
+ if (message.author.bot || !message.guild) return;
49
+
50
+ const cfg = this.configs.get(message.guild.id);
51
+ if (!cfg) return;
52
+
53
+ const content = message.content;
54
+
55
+ // ── Bad word filter ─────────────────────────────────────────────────
56
+ if (cfg.bannedWords.length > 0) {
57
+ const lower = content.toLowerCase();
58
+ const found = cfg.bannedWords.find((w) => lower.includes(w.toLowerCase()));
59
+ if (found) {
60
+ await message.delete().catch(() => {});
61
+ const warning = await message.channel.send(
62
+ `🚫 ${message.author}, that word is not allowed here.`
63
+ ).catch(() => {});
64
+ if (warning) setTimeout(() => warning.delete().catch(() => {}), 4000);
65
+
66
+ await this._log(message.guild.id, new EmbedBuilder()
67
+ .setColor("#ED4245")
68
+ .setTitle("🤖 AutoMod — Bad Word")
69
+ .addFields(
70
+ { name: "User", value: `${message.author} (${message.author.id})` },
71
+ { name: "Word matched", value: `\`${found}\`` }
72
+ )
73
+ .setTimestamp()
74
+ );
75
+ return;
76
+ }
77
+ }
78
+
79
+ // ── Link blocker ────────────────────────────────────────────────────
80
+ if (cfg.blockLinks) {
81
+ const urlRegex = /(https?:\/\/[^\s]+)/gi;
82
+ const links = content.match(urlRegex);
83
+ if (links) {
84
+ const blocked = links.filter((link) => {
85
+ try {
86
+ const host = new URL(link).hostname;
87
+ return !cfg.allowedDomains.some((d) => host.endsWith(d));
88
+ } catch {
89
+ return true;
90
+ }
91
+ });
92
+
93
+ if (blocked.length > 0) {
94
+ await message.delete().catch(() => {});
95
+ const warning = await message.channel.send(
96
+ `🔗 ${message.author}, links are not allowed here.`
97
+ ).catch(() => {});
98
+ if (warning) setTimeout(() => warning.delete().catch(() => {}), 4000);
99
+
100
+ await this._log(message.guild.id, new EmbedBuilder()
101
+ .setColor("#ED4245")
102
+ .setTitle("🤖 AutoMod — Link Blocked")
103
+ .addFields(
104
+ { name: "User", value: `${message.author} (${message.author.id})` },
105
+ { name: "Link", value: blocked[0].slice(0, 100) }
106
+ )
107
+ .setTimestamp()
108
+ );
109
+ return;
110
+ }
111
+ }
112
+ }
113
+
114
+ // ── Caps filter ─────────────────────────────────────────────────────
115
+ if (content.length >= cfg.capsMinLength) {
116
+ const letters = content.replace(/[^a-zA-Z]/g, "");
117
+ if (letters.length > 0) {
118
+ const capsCount = (letters.match(/[A-Z]/g) || []).length;
119
+ const capsPct = (capsCount / letters.length) * 100;
120
+
121
+ if (capsPct >= cfg.capsThreshold) {
122
+ await message.delete().catch(() => {});
123
+ const warning = await message.channel.send(
124
+ `🔠 ${message.author}, please don't use excessive caps.`
125
+ ).catch(() => {});
126
+ if (warning) setTimeout(() => warning.delete().catch(() => {}), 4000);
127
+
128
+ await this._log(message.guild.id, new EmbedBuilder()
129
+ .setColor("#FEE75C")
130
+ .setTitle("🤖 AutoMod — Caps Spam")
131
+ .addFields(
132
+ { name: "User", value: `${message.author} (${message.author.id})` },
133
+ { name: "Caps %", value: `${Math.round(capsPct)}%` }
134
+ )
135
+ .setTimestamp()
136
+ );
137
+ }
138
+ }
139
+ }
140
+ });
141
+ }
142
+ }
143
+
144
+ module.exports = AutoMod;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * plugins/Economy.js
3
+ * Economy plugin — coins, daily, work, rob, balance, shop.
4
+ * Requires Database module to be enabled.
5
+ */
6
+
7
+ class Economy {
8
+ constructor(client) {
9
+ this.client = client;
10
+ this.cooldowns = new Map();
11
+ }
12
+
13
+ // ── Internal DB helpers ───────────────────────────────────────────────
14
+
15
+ async _ensure(userId, guildId) {
16
+ await this.client.db.query(`
17
+ INSERT INTO chrx_economy (user_id, guild_id, balance, bank)
18
+ VALUES ($1, $2, 0, 0)
19
+ ON CONFLICT DO NOTHING
20
+ `, [BigInt(userId), BigInt(guildId)]);
21
+ }
22
+
23
+ async _get(userId, guildId) {
24
+ await this._ensure(userId, guildId);
25
+ const res = await this.client.db.query(
26
+ `SELECT balance, bank FROM chrx_economy WHERE user_id = $1 AND guild_id = $2`,
27
+ [BigInt(userId), BigInt(guildId)]
28
+ );
29
+ return res.rows[0];
30
+ }
31
+
32
+ // ── Cooldown helper ───────────────────────────────────────────────────
33
+
34
+ _checkCooldown(key, ms) {
35
+ const now = Date.now();
36
+ if (this.cooldowns.has(key)) {
37
+ const remaining = ms - (now - this.cooldowns.get(key));
38
+ if (remaining > 0) return remaining;
39
+ }
40
+ this.cooldowns.set(key, now);
41
+ return 0;
42
+ }
43
+
44
+ // ── Public API ────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Get a user's balance.
48
+ */
49
+ async getBalance(userId, guildId) {
50
+ return this._get(userId, guildId);
51
+ }
52
+
53
+ /**
54
+ * Add coins to a user's wallet.
55
+ */
56
+ async addCoins(userId, guildId, amount) {
57
+ await this._ensure(userId, guildId);
58
+ await this.client.db.query(
59
+ `UPDATE chrx_economy SET balance = balance + $1 WHERE user_id = $2 AND guild_id = $3`,
60
+ [amount, BigInt(userId), BigInt(guildId)]
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Remove coins from a user's wallet.
66
+ */
67
+ async removeCoins(userId, guildId, amount) {
68
+ await this._ensure(userId, guildId);
69
+ await this.client.db.query(
70
+ `UPDATE chrx_economy SET balance = GREATEST(balance - $1, 0) WHERE user_id = $2 AND guild_id = $3`,
71
+ [amount, BigInt(userId), BigInt(guildId)]
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Daily reward — 24h cooldown.
77
+ * @returns {{ amount: number, cooldown: number }} cooldown in ms if on cooldown
78
+ */
79
+ async daily(userId, guildId) {
80
+ const key = `daily-${userId}-${guildId}`;
81
+ const remaining = this._checkCooldown(key, 86400000);
82
+ if (remaining > 0) return { amount: 0, cooldown: remaining };
83
+
84
+ const amount = Math.floor(Math.random() * 200) + 100; // 100-300 coins
85
+ await this.addCoins(userId, guildId, amount);
86
+ return { amount, cooldown: 0 };
87
+ }
88
+
89
+ /**
90
+ * Work command — 1h cooldown.
91
+ */
92
+ async work(userId, guildId) {
93
+ const key = `work-${userId}-${guildId}`;
94
+ const remaining = this._checkCooldown(key, 3600000);
95
+ if (remaining > 0) return { amount: 0, cooldown: remaining };
96
+
97
+ const amount = Math.floor(Math.random() * 50) + 20; // 20-70 coins
98
+ await this.addCoins(userId, guildId, amount);
99
+ return { amount, cooldown: 0 };
100
+ }
101
+
102
+ /**
103
+ * Rob a user — 30% success rate, 1h cooldown.
104
+ */
105
+ async rob(robberId, targetId, guildId) {
106
+ const key = `rob-${robberId}-${guildId}`;
107
+ const remaining = this._checkCooldown(key, 3600000);
108
+ if (remaining > 0) return { success: false, cooldown: remaining };
109
+
110
+ const target = await this._get(targetId, guildId);
111
+ if (target.balance < 50) return { success: false, broke: true };
112
+
113
+ const success = Math.random() < 0.3;
114
+ if (success) {
115
+ const stolen = Math.floor(target.balance * 0.25);
116
+ await this.removeCoins(targetId, guildId, stolen);
117
+ await this.addCoins(robberId, guildId, stolen);
118
+ return { success: true, amount: stolen, cooldown: 0 };
119
+ }
120
+
121
+ const fine = Math.floor(Math.random() * 100) + 50;
122
+ await this.removeCoins(robberId, guildId, fine);
123
+ return { success: false, fine, cooldown: 0 };
124
+ }
125
+
126
+ /**
127
+ * Deposit coins into bank.
128
+ */
129
+ async deposit(userId, guildId, amount) {
130
+ const data = await this._get(userId, guildId);
131
+ const depositAmount = amount === "all" ? data.balance : Math.min(amount, data.balance);
132
+ await this.client.db.query(
133
+ `UPDATE chrx_economy SET balance = balance - $1, bank = bank + $1 WHERE user_id = $2 AND guild_id = $3`,
134
+ [depositAmount, BigInt(userId), BigInt(guildId)]
135
+ );
136
+ return depositAmount;
137
+ }
138
+
139
+ /**
140
+ * Withdraw coins from bank.
141
+ */
142
+ async withdraw(userId, guildId, amount) {
143
+ const data = await this._get(userId, guildId);
144
+ const withdrawAmount = amount === "all" ? data.bank : Math.min(amount, data.bank);
145
+ await this.client.db.query(
146
+ `UPDATE chrx_economy SET bank = bank - $1, balance = balance + $1 WHERE user_id = $2 AND guild_id = $3`,
147
+ [withdrawAmount, BigInt(userId), BigInt(guildId)]
148
+ );
149
+ return withdrawAmount;
150
+ }
151
+
152
+ /**
153
+ * Leaderboard — top 10 richest users in a guild.
154
+ */
155
+ async leaderboard(guildId) {
156
+ const res = await this.client.db.query(
157
+ `SELECT user_id, balance + bank AS total FROM chrx_economy WHERE guild_id = $1 ORDER BY total DESC LIMIT 10`,
158
+ [BigInt(guildId)]
159
+ );
160
+ return res.rows;
161
+ }
162
+
163
+ /**
164
+ * Create the economy table if it doesn't exist.
165
+ * Called automatically by ChrxClient on ready.
166
+ */
167
+ async initTable() {
168
+ await this.client.db.query(`
169
+ CREATE TABLE IF NOT EXISTS chrx_economy (
170
+ user_id BIGINT NOT NULL,
171
+ guild_id BIGINT NOT NULL,
172
+ balance BIGINT DEFAULT 0,
173
+ bank BIGINT DEFAULT 0,
174
+ PRIMARY KEY (user_id, guild_id)
175
+ )
176
+ `);
177
+ console.log("[Economy] Table ready.");
178
+ }
179
+ }
180
+
181
+ module.exports = Economy;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * plugins/Giveaways.js
3
+ * Giveaway plugin — timed giveaways, reaction entry, winner picking.
4
+ */
5
+
6
+ const { EmbedBuilder } = require("discord.js");
7
+
8
+ class Giveaways {
9
+ constructor(client) {
10
+ this.client = client;
11
+ this.active = new Map(); // messageId -> giveaway data
12
+ }
13
+
14
+ /**
15
+ * Start a giveaway.
16
+ * @param {object} options
17
+ * @param {string} options.channelId
18
+ * @param {string} options.guildId
19
+ * @param {number} options.durationMs
20
+ * @param {string} options.prize
21
+ * @param {number} [options.winners=1]
22
+ * @param {string} options.hostedBy User ID
23
+ */
24
+ async start({ channelId, guildId, durationMs, prize, winners = 1, hostedBy }) {
25
+ const channel = this.client.channels.cache.get(channelId);
26
+ if (!channel) throw new Error("[Giveaways] Channel not found.");
27
+
28
+ const endsAt = new Date(Date.now() + durationMs);
29
+
30
+ const msg = await channel.send({
31
+ embeds: [
32
+ new EmbedBuilder()
33
+ .setColor("#5865F2")
34
+ .setTitle("🎉 Giveaway!")
35
+ .setDescription(`**Prize:** ${prize}\n\nReact with 🎉 to enter!\n\n**Ends:** <t:${Math.floor(endsAt.getTime() / 1000)}:R>\n**Winners:** ${winners}\n**Hosted by:** <@${hostedBy}>`)
36
+ .setTimestamp(endsAt),
37
+ ],
38
+ });
39
+
40
+ await msg.react("🎉");
41
+
42
+ const data = { channelId, guildId, prize, winners, hostedBy, endsAt, messageId: msg.id };
43
+ this.active.set(msg.id, data);
44
+
45
+ setTimeout(() => this._end(msg.id), durationMs);
46
+
47
+ return msg;
48
+ }
49
+
50
+ async _end(messageId) {
51
+ const data = this.active.get(messageId);
52
+ if (!data) return;
53
+
54
+ this.active.delete(messageId);
55
+ const channel = this.client.channels.cache.get(data.channelId);
56
+ if (!channel) return;
57
+
58
+ const msg = await channel.messages.fetch(messageId).catch(() => null);
59
+ if (!msg) return;
60
+
61
+ const reaction = msg.reactions.cache.get("🎉");
62
+ if (!reaction) return;
63
+
64
+ const users = await reaction.users.fetch();
65
+ const eligible = users.filter((u) => !u.bot);
66
+
67
+ if (eligible.size === 0) {
68
+ return channel.send("🎉 Giveaway ended — no valid entries.");
69
+ }
70
+
71
+ const shuffled = eligible.random(Math.min(data.winners, eligible.size));
72
+ const winnerMentions = (Array.isArray(shuffled) ? shuffled : [shuffled]).map((u) => `<@${u.id}>`).join(", ");
73
+
74
+ await msg.edit({
75
+ embeds: [
76
+ new EmbedBuilder()
77
+ .setColor("#57F287")
78
+ .setTitle("🎉 Giveaway Ended!")
79
+ .setDescription(`**Prize:** ${data.prize}\n**Winners:** ${winnerMentions}\n**Hosted by:** <@${data.hostedBy}>`)
80
+ .setTimestamp(),
81
+ ],
82
+ });
83
+
84
+ channel.send(`🎉 Congratulations ${winnerMentions}! You won **${data.prize}**!`);
85
+ }
86
+
87
+ /**
88
+ * Reroll winners for an ended giveaway.
89
+ */
90
+ async reroll(messageId, channelId, count = 1) {
91
+ const channel = this.client.channels.cache.get(channelId);
92
+ if (!channel) return;
93
+ const msg = await channel.messages.fetch(messageId).catch(() => null);
94
+ if (!msg) return;
95
+
96
+ const reaction = msg.reactions.cache.get("🎉");
97
+ if (!reaction) return;
98
+
99
+ const users = await reaction.users.fetch();
100
+ const eligible = users.filter((u) => !u.bot);
101
+ const winners = eligible.random(Math.min(count, eligible.size));
102
+ const mentions = (Array.isArray(winners) ? winners : [winners]).map((u) => `<@${u.id}>`).join(", ");
103
+
104
+ channel.send(`🎉 New winner(s): ${mentions}!`);
105
+ }
106
+ }
107
+
108
+ module.exports = Giveaways;
@@ -0,0 +1,208 @@
1
+ /**
2
+ * plugins/Moderation.js
3
+ * Moderation plugin — warn, ban, kick, timeout, mod logs, anti-spam.
4
+ */
5
+
6
+ class Moderation {
7
+ constructor(client) {
8
+ this.client = client;
9
+ this.spamTracker = new Map(); // userId -> { count, timer }
10
+ this.logChannels = new Map(); // guildId -> channelId
11
+ }
12
+
13
+ // ── Config ────────────────────────────────────────────────────────────
14
+
15
+ /**
16
+ * Set the mod log channel for a guild.
17
+ */
18
+ setLogChannel(guildId, channelId) {
19
+ this.logChannels.set(guildId, channelId);
20
+ }
21
+
22
+ // ── Logging ───────────────────────────────────────────────────────────
23
+
24
+ async _log(guild, embed) {
25
+ const channelId = this.logChannels.get(guild.id);
26
+ if (!channelId) return;
27
+ const channel = guild.channels.cache.get(channelId);
28
+ if (channel) channel.send({ embeds: [embed] }).catch(() => {});
29
+ }
30
+
31
+ // ── Actions ───────────────────────────────────────────────────────────
32
+
33
+ /**
34
+ * Warn a user. Stored in Postgres if database is enabled.
35
+ */
36
+ async warn(guild, target, moderator, reason = "No reason provided") {
37
+ if (this.client.db) {
38
+ await this.client.db.query(`
39
+ INSERT INTO chrx_warnings (user_id, guild_id, moderator_id, reason)
40
+ VALUES ($1, $2, $3, $4)
41
+ `, [BigInt(target.id), BigInt(guild.id), BigInt(moderator.id), reason]);
42
+ }
43
+
44
+ const { EmbedBuilder } = require("discord.js");
45
+ await this._log(guild, new EmbedBuilder()
46
+ .setColor("#FEE75C")
47
+ .setTitle("⚠️ Warning Issued")
48
+ .addFields(
49
+ { name: "User", value: `${target} (${target.id})`, inline: true },
50
+ { name: "Moderator", value: `${moderator}`, inline: true },
51
+ { name: "Reason", value: reason }
52
+ )
53
+ .setTimestamp()
54
+ );
55
+
56
+ return true;
57
+ }
58
+
59
+ /**
60
+ * Get all warnings for a user in a guild.
61
+ */
62
+ async getWarnings(userId, guildId) {
63
+ if (!this.client.db) return [];
64
+ const res = await this.client.db.query(
65
+ `SELECT * FROM chrx_warnings WHERE user_id = $1 AND guild_id = $2 ORDER BY created_at DESC`,
66
+ [BigInt(userId), BigInt(guildId)]
67
+ );
68
+ return res.rows;
69
+ }
70
+
71
+ /**
72
+ * Clear warnings for a user.
73
+ */
74
+ async clearWarnings(userId, guildId) {
75
+ if (!this.client.db) return;
76
+ await this.client.db.query(
77
+ `DELETE FROM chrx_warnings WHERE user_id = $1 AND guild_id = $2`,
78
+ [BigInt(userId), BigInt(guildId)]
79
+ );
80
+ }
81
+
82
+ /**
83
+ * Ban a user.
84
+ */
85
+ async ban(guild, target, moderator, reason = "No reason provided", deleteMessageDays = 0) {
86
+ await guild.members.ban(target, { reason, deleteMessageSeconds: deleteMessageDays * 86400 });
87
+
88
+ const { EmbedBuilder } = require("discord.js");
89
+ await this._log(guild, new EmbedBuilder()
90
+ .setColor("#ED4245")
91
+ .setTitle("🔨 User Banned")
92
+ .addFields(
93
+ { name: "User", value: `${target} (${target.id})`, inline: true },
94
+ { name: "Moderator", value: `${moderator}`, inline: true },
95
+ { name: "Reason", value: reason }
96
+ )
97
+ .setTimestamp()
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Kick a user.
103
+ */
104
+ async kick(guild, target, moderator, reason = "No reason provided") {
105
+ const member = guild.members.cache.get(target.id);
106
+ if (member) await member.kick(reason);
107
+
108
+ const { EmbedBuilder } = require("discord.js");
109
+ await this._log(guild, new EmbedBuilder()
110
+ .setColor("#ED4245")
111
+ .setTitle("👢 User Kicked")
112
+ .addFields(
113
+ { name: "User", value: `${target} (${target.id})`, inline: true },
114
+ { name: "Moderator", value: `${moderator}`, inline: true },
115
+ { name: "Reason", value: reason }
116
+ )
117
+ .setTimestamp()
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Timeout a user.
123
+ * @param {number} durationMs Timeout duration in milliseconds
124
+ */
125
+ async timeout(guild, target, moderator, durationMs, reason = "No reason provided") {
126
+ const member = guild.members.cache.get(target.id);
127
+ if (member) await member.timeout(durationMs, reason);
128
+
129
+ const { EmbedBuilder } = require("discord.js");
130
+ await this._log(guild, new EmbedBuilder()
131
+ .setColor("#FEE75C")
132
+ .setTitle("🔇 User Timed Out")
133
+ .addFields(
134
+ { name: "User", value: `${target} (${target.id})`, inline: true },
135
+ { name: "Moderator", value: `${moderator}`, inline: true },
136
+ { name: "Duration", value: `${Math.floor(durationMs / 60000)} minutes`, inline: true },
137
+ { name: "Reason", value: reason }
138
+ )
139
+ .setTimestamp()
140
+ );
141
+ }
142
+
143
+ // ── Anti-spam ─────────────────────────────────────────────────────────
144
+
145
+ /**
146
+ * Enable anti-spam. Auto-times out users who send 5+ messages in 3 seconds.
147
+ * @param {number} [threshold=5] Messages before action
148
+ * @param {number} [windowMs=3000] Time window in ms
149
+ * @param {number} [timeoutMs=30000] Timeout duration in ms
150
+ */
151
+ enableAntiSpam(threshold = 5, windowMs = 3000, timeoutMs = 30000) {
152
+ this.client.on("messageCreate", async (message) => {
153
+ if (message.author.bot || !message.guild) return;
154
+
155
+ const key = `${message.author.id}-${message.guild.id}`;
156
+ const now = Date.now();
157
+ const tracker = this.spamTracker.get(key) || { count: 0, start: now };
158
+
159
+ if (now - tracker.start > windowMs) {
160
+ this.spamTracker.set(key, { count: 1, start: now });
161
+ return;
162
+ }
163
+
164
+ tracker.count++;
165
+ this.spamTracker.set(key, tracker);
166
+
167
+ if (tracker.count >= threshold) {
168
+ this.spamTracker.delete(key);
169
+ const member = message.guild.members.cache.get(message.author.id);
170
+ if (!member) return;
171
+
172
+ await member.timeout(timeoutMs, "Auto-mod: Spam detected").catch(() => {});
173
+ message.channel.send(`🔇 ${message.author} was timed out for spamming.`).catch(() => {});
174
+
175
+ const { EmbedBuilder } = require("discord.js");
176
+ await this._log(message.guild, new EmbedBuilder()
177
+ .setColor("#ED4245")
178
+ .setTitle("🤖 Auto-Mod: Spam")
179
+ .addFields(
180
+ { name: "User", value: `${message.author} (${message.author.id})` },
181
+ { name: "Action", value: `Timed out for ${timeoutMs / 1000}s` }
182
+ )
183
+ .setTimestamp()
184
+ );
185
+ }
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Create required tables. Called automatically on ready if db is enabled.
191
+ */
192
+ async initTable() {
193
+ if (!this.client.db) return;
194
+ await this.client.db.query(`
195
+ CREATE TABLE IF NOT EXISTS chrx_warnings (
196
+ id SERIAL PRIMARY KEY,
197
+ user_id BIGINT NOT NULL,
198
+ guild_id BIGINT NOT NULL,
199
+ moderator_id BIGINT NOT NULL,
200
+ reason TEXT,
201
+ created_at TIMESTAMP DEFAULT NOW()
202
+ )
203
+ `);
204
+ console.log("[Moderation] Table ready.");
205
+ }
206
+ }
207
+
208
+ module.exports = Moderation;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * plugins/Polls.js
3
+ * Timed polls with reaction voting and result embeds.
4
+ */
5
+
6
+ const { EmbedBuilder } = require("discord.js");
7
+
8
+ const EMOJI_MAP = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"];
9
+
10
+ class Polls {
11
+ constructor(client) {
12
+ this.client = client;
13
+ this.active = new Map(); // messageId -> poll data
14
+ }
15
+
16
+ /**
17
+ * Create a poll.
18
+ * @param {object} options
19
+ * @param {string} options.channelId
20
+ * @param {string} options.question
21
+ * @param {string[]} options.choices 2-10 choices
22
+ * @param {number} [options.durationMs] 0 = no end time
23
+ */
24
+ async create({ channelId, question, choices, durationMs = 0 }) {
25
+ if (choices.length < 2 || choices.length > 10) {
26
+ throw new Error("[Polls] Must have between 2 and 10 choices.");
27
+ }
28
+
29
+ const channel = this.client.channels.cache.get(channelId);
30
+ if (!channel) throw new Error("[Polls] Channel not found.");
31
+
32
+ const desc = choices.map((c, i) => `${EMOJI_MAP[i]} ${c}`).join("\n");
33
+ const endsText = durationMs > 0 ? `\n\n⏱ Ends: <t:${Math.floor((Date.now() + durationMs) / 1000)}:R>` : "";
34
+
35
+ const msg = await channel.send({
36
+ embeds: [
37
+ new EmbedBuilder()
38
+ .setColor("#5865F2")
39
+ .setTitle(`📊 ${question}`)
40
+ .setDescription(desc + endsText)
41
+ .setTimestamp(),
42
+ ],
43
+ });
44
+
45
+ for (let i = 0; i < choices.length; i++) {
46
+ await msg.react(EMOJI_MAP[i]);
47
+ }
48
+
49
+ const data = { question, choices, channelId, messageId: msg.id };
50
+ this.active.set(msg.id, data);
51
+
52
+ if (durationMs > 0) {
53
+ setTimeout(() => this._end(msg.id), durationMs);
54
+ }
55
+
56
+ return msg;
57
+ }
58
+
59
+ async _end(messageId) {
60
+ const data = this.active.get(messageId);
61
+ if (!data) return;
62
+ this.active.delete(messageId);
63
+
64
+ const channel = this.client.channels.cache.get(data.channelId);
65
+ if (!channel) return;
66
+
67
+ const msg = await channel.messages.fetch(messageId).catch(() => null);
68
+ if (!msg) return;
69
+
70
+ const results = data.choices.map((choice, i) => {
71
+ const reaction = msg.reactions.cache.get(EMOJI_MAP[i]);
72
+ const count = (reaction?.count ?? 1) - 1; // subtract bot's own reaction
73
+ return { choice, count };
74
+ });
75
+
76
+ const total = results.reduce((sum, r) => sum + r.count, 0);
77
+ const winner = results.reduce((a, b) => (a.count > b.count ? a : b));
78
+
79
+ const desc = results.map((r, i) => {
80
+ const pct = total > 0 ? Math.round((r.count / total) * 100) : 0;
81
+ const bar = "█".repeat(Math.floor(pct / 10)) + "░".repeat(10 - Math.floor(pct / 10));
82
+ return `${EMOJI_MAP[i]} **${r.choice}**\n${bar} ${pct}% (${r.count} votes)`;
83
+ }).join("\n\n");
84
+
85
+ await msg.edit({
86
+ embeds: [
87
+ new EmbedBuilder()
88
+ .setColor("#57F287")
89
+ .setTitle(`📊 Poll Ended — ${data.question}`)
90
+ .setDescription(desc + `\n\n🏆 **Winner: ${winner.choice}**`)
91
+ .setTimestamp(),
92
+ ],
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Manually end a poll by message ID.
98
+ */
99
+ async end(messageId) {
100
+ await this._end(messageId);
101
+ }
102
+ }
103
+
104
+ module.exports = Polls;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * plugins/Reminders.js
3
+ * DM reminders — persistent via Postgres if database is enabled.
4
+ */
5
+
6
+ class Reminders {
7
+ constructor(client) {
8
+ this.client = client;
9
+ this.pending = new Map(); // reminderId -> timeout
10
+ }
11
+
12
+ /**
13
+ * Set a reminder for a user.
14
+ * @param {string} userId
15
+ * @param {string} channelId Channel to ping them in
16
+ * @param {string} reminder Reminder text
17
+ * @param {number} durationMs When to remind them
18
+ */
19
+ async set(userId, channelId, reminder, durationMs) {
20
+ let dbId = null;
21
+
22
+ if (this.client.db) {
23
+ const res = await this.client.db.query(`
24
+ INSERT INTO chrx_reminders (user_id, channel_id, reminder, remind_at)
25
+ VALUES ($1, $2, $3, $4)
26
+ RETURNING id
27
+ `, [BigInt(userId), BigInt(channelId), reminder, new Date(Date.now() + durationMs)]);
28
+ dbId = res.rows[0].id;
29
+ }
30
+
31
+ const timeout = setTimeout(async () => {
32
+ const channel = this.client.channels.cache.get(channelId);
33
+ if (channel) {
34
+ channel.send(`⏰ <@${userId}> — Reminder: **${reminder}**`).catch(() => {});
35
+ }
36
+ if (dbId && this.client.db) {
37
+ await this.client.db.query(`DELETE FROM chrx_reminders WHERE id = $1`, [dbId]);
38
+ }
39
+ this.pending.delete(dbId ?? `${userId}-${Date.now()}`);
40
+ }, durationMs);
41
+
42
+ this.pending.set(dbId ?? `${userId}-${Date.now()}`, timeout);
43
+ return { durationMs, reminder };
44
+ }
45
+
46
+ /**
47
+ * Restore reminders from DB on startup.
48
+ * Call this in your ready event.
49
+ */
50
+ async restore() {
51
+ if (!this.client.db) return;
52
+ const now = new Date();
53
+ const res = await this.client.db.query(
54
+ `SELECT * FROM chrx_reminders WHERE remind_at > $1`,
55
+ [now]
56
+ );
57
+
58
+ for (const row of res.rows) {
59
+ const remaining = new Date(row.remind_at).getTime() - Date.now();
60
+ if (remaining <= 0) continue;
61
+
62
+ const timeout = setTimeout(async () => {
63
+ const channel = this.client.channels.cache.get(row.channel_id.toString());
64
+ if (channel) channel.send(`⏰ <@${row.user_id}> — Reminder: **${row.reminder}**`).catch(() => {});
65
+ await this.client.db.query(`DELETE FROM chrx_reminders WHERE id = $1`, [row.id]);
66
+ this.pending.delete(row.id);
67
+ }, remaining);
68
+
69
+ this.pending.set(row.id, timeout);
70
+ }
71
+
72
+ console.log(`[Reminders] Restored ${res.rows.length} pending reminder(s).`);
73
+ }
74
+
75
+ async initTable() {
76
+ if (!this.client.db) return;
77
+ await this.client.db.query(`
78
+ CREATE TABLE IF NOT EXISTS chrx_reminders (
79
+ id SERIAL PRIMARY KEY,
80
+ user_id BIGINT NOT NULL,
81
+ channel_id BIGINT NOT NULL,
82
+ reminder TEXT NOT NULL,
83
+ remind_at TIMESTAMP NOT NULL,
84
+ created_at TIMESTAMP DEFAULT NOW()
85
+ )
86
+ `);
87
+ console.log("[Reminders] Table ready.");
88
+ }
89
+ }
90
+
91
+ module.exports = Reminders;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * plugins/Starboard.js
3
+ * React with ⭐ enough times → message gets pinned to a starboard channel.
4
+ */
5
+
6
+ const { EmbedBuilder } = require("discord.js");
7
+
8
+ class Starboard {
9
+ constructor(client) {
10
+ this.client = client;
11
+ this.configs = new Map(); // guildId -> { channelId, threshold, emoji }
12
+ this.posted = new Set(); // messageIds already on starboard
13
+ this._registerEvents();
14
+ }
15
+
16
+ /**
17
+ * Configure starboard for a guild.
18
+ * @param {string} guildId
19
+ * @param {object} options
20
+ * @param {string} options.channelId Starboard channel
21
+ * @param {number} [options.threshold=3] Stars needed
22
+ * @param {string} [options.emoji="⭐"]
23
+ */
24
+ setup(guildId, { channelId, threshold = 3, emoji = "⭐" }) {
25
+ this.configs.set(guildId, { channelId, threshold, emoji });
26
+ }
27
+
28
+ _registerEvents() {
29
+ this.client.on("messageReactionAdd", async (reaction, user) => {
30
+ if (user.bot) return;
31
+ if (reaction.partial) await reaction.fetch().catch(() => {});
32
+
33
+ const cfg = this.configs.get(reaction.message.guildId);
34
+ if (!cfg) return;
35
+ if (reaction.emoji.name !== cfg.emoji) return;
36
+ if (this.posted.has(reaction.message.id)) return;
37
+ if (reaction.count < cfg.threshold) return;
38
+
39
+ this.posted.add(reaction.message.id);
40
+
41
+ const msg = reaction.message.partial
42
+ ? await reaction.message.fetch().catch(() => null)
43
+ : reaction.message;
44
+ if (!msg) return;
45
+
46
+ const starChannel = this.client.channels.cache.get(cfg.channelId);
47
+ if (!starChannel) return;
48
+
49
+ const embed = new EmbedBuilder()
50
+ .setColor("#FEE75C")
51
+ .setAuthor({ name: msg.author.tag, iconURL: msg.author.displayAvatarURL() })
52
+ .setDescription(msg.content || "*No text content*")
53
+ .addFields({ name: "Source", value: `[Jump to message](${msg.url})` })
54
+ .setTimestamp(msg.createdAt);
55
+
56
+ if (msg.attachments.size > 0) {
57
+ embed.setImage(msg.attachments.first().url);
58
+ }
59
+
60
+ await starChannel.send({
61
+ content: `${cfg.emoji} **${reaction.count}** | <#${msg.channelId}>`,
62
+ embeds: [embed],
63
+ }).catch(() => {});
64
+ });
65
+ }
66
+ }
67
+
68
+ module.exports = Starboard;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * plugins/Tickets.js
3
+ * Support ticket system — open, close, log.
4
+ */
5
+
6
+ const { EmbedBuilder, PermissionFlagsBits, ChannelType } = require("discord.js");
7
+
8
+ class Tickets {
9
+ constructor(client) {
10
+ this.client = client;
11
+ this.config = new Map(); // guildId -> { categoryId, logChannelId, supportRoleId }
12
+ }
13
+
14
+ /**
15
+ * Configure the ticket system for a guild.
16
+ */
17
+ setup(guildId, { categoryId, logChannelId, supportRoleId }) {
18
+ this.config.set(guildId, { categoryId, logChannelId, supportRoleId });
19
+ }
20
+
21
+ /**
22
+ * Open a ticket for a user.
23
+ */
24
+ async open(guild, user, topic = "Support") {
25
+ const cfg = this.config.get(guild.id);
26
+ if (!cfg) throw new Error("[Tickets] Not configured for this guild.");
27
+
28
+ const existing = guild.channels.cache.find(
29
+ (c) => c.name === `ticket-${user.username.toLowerCase()}` && c.parentId === cfg.categoryId
30
+ );
31
+ if (existing) return { channel: existing, alreadyOpen: true };
32
+
33
+ const channel = await guild.channels.create({
34
+ name: `ticket-${user.username.toLowerCase()}`,
35
+ type: ChannelType.GuildText,
36
+ parent: cfg.categoryId,
37
+ permissionOverwrites: [
38
+ { id: guild.roles.everyone, deny: [PermissionFlagsBits.ViewChannel] },
39
+ { id: user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] },
40
+ ...(cfg.supportRoleId ? [{ id: cfg.supportRoleId, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] }] : []),
41
+ ],
42
+ });
43
+
44
+ await channel.send({
45
+ embeds: [
46
+ new EmbedBuilder()
47
+ .setColor("#5865F2")
48
+ .setTitle(`🎫 Ticket — ${topic}`)
49
+ .setDescription(`Welcome ${user}! Support will be with you shortly.\nUse \`/ticket close\` to close this ticket.`)
50
+ .setTimestamp(),
51
+ ],
52
+ });
53
+
54
+ if (this.client.db) {
55
+ await this.client.db.query(`
56
+ INSERT INTO chrx_tickets (user_id, guild_id, channel_id, topic)
57
+ VALUES ($1, $2, $3, $4)
58
+ `, [BigInt(user.id), BigInt(guild.id), BigInt(channel.id), topic]);
59
+ }
60
+
61
+ return { channel, alreadyOpen: false };
62
+ }
63
+
64
+ /**
65
+ * Close a ticket channel.
66
+ */
67
+ async close(guild, channel, closedBy) {
68
+ const cfg = this.config.get(guild.id);
69
+
70
+ if (cfg?.logChannelId) {
71
+ const logChannel = guild.channels.cache.get(cfg.logChannelId);
72
+ if (logChannel) {
73
+ await logChannel.send({
74
+ embeds: [
75
+ new EmbedBuilder()
76
+ .setColor("#ED4245")
77
+ .setTitle("🎫 Ticket Closed")
78
+ .addFields(
79
+ { name: "Channel", value: channel.name, inline: true },
80
+ { name: "Closed by", value: `${closedBy}`, inline: true }
81
+ )
82
+ .setTimestamp(),
83
+ ],
84
+ });
85
+ }
86
+ }
87
+
88
+ if (this.client.db) {
89
+ await this.client.db.query(
90
+ `UPDATE chrx_tickets SET closed_at = NOW() WHERE channel_id = $1`,
91
+ [BigInt(channel.id)]
92
+ );
93
+ }
94
+
95
+ await channel.send("🔒 Ticket closing in 5 seconds...").catch(() => {});
96
+ setTimeout(() => channel.delete().catch(() => {}), 5000);
97
+ }
98
+
99
+ async initTable() {
100
+ if (!this.client.db) return;
101
+ await this.client.db.query(`
102
+ CREATE TABLE IF NOT EXISTS chrx_tickets (
103
+ id SERIAL PRIMARY KEY,
104
+ user_id BIGINT NOT NULL,
105
+ guild_id BIGINT NOT NULL,
106
+ channel_id BIGINT NOT NULL,
107
+ topic TEXT,
108
+ closed_at TIMESTAMP,
109
+ created_at TIMESTAMP DEFAULT NOW()
110
+ )
111
+ `);
112
+ console.log("[Tickets] Table ready.");
113
+ }
114
+ }
115
+
116
+ module.exports = Tickets;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * plugins/Welcome.js
3
+ * Welcome/goodbye messages + auto role on join.
4
+ */
5
+
6
+ const { EmbedBuilder } = require("discord.js");
7
+
8
+ class Welcome {
9
+ constructor(client) {
10
+ this.client = client;
11
+ this.configs = new Map(); // guildId -> config
12
+ this._registerEvents();
13
+ }
14
+
15
+ /**
16
+ * Configure welcome for a guild.
17
+ * @param {string} guildId
18
+ * @param {object} options
19
+ * @param {string} [options.channelId] Welcome channel
20
+ * @param {string} [options.message] Custom message. Use {user} and {server} as placeholders.
21
+ * @param {string} [options.goodbyeChannelId]
22
+ * @param {string} [options.goodbyeMessage]
23
+ * @param {string} [options.autoRoleId] Role to give on join
24
+ */
25
+ setup(guildId, options) {
26
+ this.configs.set(guildId, options);
27
+ }
28
+
29
+ _registerEvents() {
30
+ this.client.on("guildMemberAdd", async (member) => {
31
+ const cfg = this.configs.get(member.guild.id);
32
+ if (!cfg) return;
33
+
34
+ if (cfg.autoRoleId) {
35
+ const role = member.guild.roles.cache.get(cfg.autoRoleId);
36
+ if (role) await member.roles.add(role).catch(() => {});
37
+ }
38
+
39
+ if (cfg.channelId) {
40
+ const channel = this.client.channels.cache.get(cfg.channelId);
41
+ if (!channel) return;
42
+ const msg = (cfg.message || "Welcome {user} to **{server}**!")
43
+ .replace("{user}", member.toString())
44
+ .replace("{server}", member.guild.name);
45
+
46
+ await channel.send({
47
+ embeds: [
48
+ new EmbedBuilder()
49
+ .setColor("#57F287")
50
+ .setDescription(msg)
51
+ .setThumbnail(member.user.displayAvatarURL())
52
+ .setTimestamp(),
53
+ ],
54
+ }).catch(() => {});
55
+ }
56
+ });
57
+
58
+ this.client.on("guildMemberRemove", async (member) => {
59
+ const cfg = this.configs.get(member.guild.id);
60
+ if (!cfg?.goodbyeChannelId) return;
61
+
62
+ const channel = this.client.channels.cache.get(cfg.goodbyeChannelId);
63
+ if (!channel) return;
64
+
65
+ const msg = (cfg.goodbyeMessage || "**{user}** has left **{server}**. Goodbye!")
66
+ .replace("{user}", member.user.tag)
67
+ .replace("{server}", member.guild.name);
68
+
69
+ await channel.send({
70
+ embeds: [
71
+ new EmbedBuilder()
72
+ .setColor("#ED4245")
73
+ .setDescription(msg)
74
+ .setTimestamp(),
75
+ ],
76
+ }).catch(() => {});
77
+ });
78
+ }
79
+ }
80
+
81
+ module.exports = Welcome;