chrxmaticc-framework 1.0.3 → 1.1.1
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 +32 -10
- package/package.json +6 -4
- package/plugins/autoMod.js +144 -0
- package/plugins/economy.js +181 -0
- package/plugins/giveaway.js +108 -0
- package/plugins/moderation.js +208 -0
- package/plugins/polls.js +104 -0
- package/plugins/reminder.js +91 -0
- package/plugins/starboard.js +68 -0
- package/plugins/tickets.js +116 -0
- package/plugins/welcome.js +81 -0
package/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Main entry point — exports everything
|
|
2
|
+
* index.js
|
|
3
|
+
* Main entry point — exports everything.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const { ChrxClient } = require("./
|
|
7
|
-
const Database = require("./
|
|
8
|
-
const XPSystem = require("./
|
|
9
|
-
const AIWrapper = require("./
|
|
10
|
-
const MusicManager = require("./
|
|
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");
|
|
11
11
|
const {
|
|
12
12
|
parseTime,
|
|
13
13
|
formatTime,
|
|
@@ -17,7 +17,18 @@ const {
|
|
|
17
17
|
applyStartMarker,
|
|
18
18
|
startEndMarkerWatcher,
|
|
19
19
|
stopEndMarkerWatcher,
|
|
20
|
-
} = require("./
|
|
20
|
+
} = require("./songMarkers");
|
|
21
|
+
|
|
22
|
+
// ── Plugins ───────────────────────────────────────────────────────────────
|
|
23
|
+
const Economy = require("./plugins/Economy");
|
|
24
|
+
const Moderation = require("./plugins/Moderation");
|
|
25
|
+
const Giveaways = require("./plugins/Giveaways");
|
|
26
|
+
const Tickets = require("./plugins/Tickets");
|
|
27
|
+
const Welcome = require("./plugins/Welcome");
|
|
28
|
+
const Polls = require("./plugins/Polls");
|
|
29
|
+
const Reminders = require("./plugins/Reminders");
|
|
30
|
+
const Starboard = require("./plugins/Starboard");
|
|
31
|
+
const AutoMod = require("./plugins/AutoMod");
|
|
21
32
|
|
|
22
33
|
module.exports = {
|
|
23
34
|
// Core
|
|
@@ -29,7 +40,7 @@ module.exports = {
|
|
|
29
40
|
AIWrapper,
|
|
30
41
|
MusicManager,
|
|
31
42
|
|
|
32
|
-
// Song marker utilities
|
|
43
|
+
// Song marker utilities
|
|
33
44
|
parseTime,
|
|
34
45
|
formatTime,
|
|
35
46
|
setMarker,
|
|
@@ -38,4 +49,15 @@ module.exports = {
|
|
|
38
49
|
applyStartMarker,
|
|
39
50
|
startEndMarkerWatcher,
|
|
40
51
|
stopEndMarkerWatcher,
|
|
41
|
-
|
|
52
|
+
|
|
53
|
+
// Plugins
|
|
54
|
+
Economy,
|
|
55
|
+
Moderation,
|
|
56
|
+
Giveaways,
|
|
57
|
+
Tickets,
|
|
58
|
+
Welcome,
|
|
59
|
+
Polls,
|
|
60
|
+
Reminders,
|
|
61
|
+
Starboard,
|
|
62
|
+
AutoMod,
|
|
63
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrxmaticc-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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;
|
package/plugins/polls.js
ADDED
|
@@ -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;
|