chrxmaticc-framework 1.0.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.
@@ -0,0 +1,33 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ - run: npm ci
19
+ - run: npm test
20
+
21
+ publish-npm:
22
+ needs: build
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: actions/setup-node@v4
27
+ with:
28
+ node-version: 20
29
+ registry-url: https://registry.npmjs.org/
30
+ - run: npm ci
31
+ - run: npm publish
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{secrets.npm_token}}
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # chrxmaticc-framework
2
+
3
+ A batteries-included Discord bot framework built on discord.js v14 with Lavalink music, AI integration, XP system and Postgres support — all in ~10 lines.
4
+
5
+ ---
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install chrxmaticc-framework discord.js
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Basic setup
16
+
17
+ ```js
18
+ require("dotenv").config();
19
+ const { ChrxClient } = require("chrxmaticc-framework");
20
+
21
+ const bot = new ChrxClient({
22
+ token: process.env.BOT_TOKEN,
23
+ });
24
+
25
+ bot.start();
26
+ ```
27
+
28
+ ---
29
+
30
+ ## With music
31
+
32
+ ```js
33
+ const { ChrxClient } = require("chrxmaticc-framework");
34
+
35
+ const bot = new ChrxClient({
36
+ token: process.env.BOT_TOKEN,
37
+ lavalink: {
38
+ host: process.env.LAVA_HOST,
39
+ port: 2333,
40
+ password: process.env.LAVA_PASS,
41
+ secure: false,
42
+ },
43
+ modules: {
44
+ music: true,
45
+ },
46
+ });
47
+
48
+ bot.start();
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Full setup (music + AI + XP + database)
54
+
55
+ ```js
56
+ require("dotenv").config();
57
+ const { ChrxClient } = require("chrxmaticc-framework");
58
+
59
+ const bot = new ChrxClient({
60
+ token: process.env.BOT_TOKEN,
61
+
62
+ lavalink: {
63
+ host: process.env.LAVA_HOST,
64
+ port: 2333,
65
+ password: process.env.LAVA_PASS,
66
+ secure: false,
67
+ },
68
+
69
+ modules: {
70
+ music: true,
71
+ xp: true,
72
+ database: process.env.DATABASE_URL,
73
+ ai: {
74
+ apiKey: process.env.AI_KEY,
75
+ model: "gpt-3.5-turbo",
76
+ provider: "openai", // or "anthropic"
77
+ },
78
+ },
79
+ });
80
+
81
+ bot.start();
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Commands
87
+
88
+ Put your commands in a `commands/` folder (subfolders supported):
89
+
90
+ ```js
91
+ // commands/ping.js
92
+ const { SlashCommandBuilder } = require("discord.js");
93
+
94
+ module.exports = {
95
+ data: new SlashCommandBuilder()
96
+ .setName("ping")
97
+ .setDescription("Pong!"),
98
+ async execute(interaction) {
99
+ await interaction.reply("Pong!");
100
+ },
101
+ };
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Events
107
+
108
+ Put your events in an `events/` folder:
109
+
110
+ ```js
111
+ // events/ready.js
112
+ module.exports = {
113
+ name: "ready",
114
+ once: true,
115
+ execute(client) {
116
+ console.log(`Logged in as ${client.user.tag}`);
117
+ },
118
+ };
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Song markers (built-in)
124
+
125
+ The music module includes a clip marker system. Use `/player-set` to set start/end timestamps on any song.
126
+
127
+ ```
128
+ /player-set start 1:43 → jumps to 1:43 when the song plays
129
+ /player-set end 2:44 → cuts off at 2:44
130
+ /player-set end 2:44 loop:true → loops between start and end markers
131
+ /player-set clear both → removes all markers from the song
132
+ ```
133
+
134
+ ---
135
+
136
+ ## AI usage in commands
137
+
138
+ ```js
139
+ async execute(interaction) {
140
+ const answer = await interaction.client.ai.ask("What is the meaning of life?");
141
+ await interaction.reply(answer);
142
+ }
143
+ ```
144
+
145
+ ---
146
+
147
+ ## XP usage in commands
148
+
149
+ ```js
150
+ async execute(interaction) {
151
+ const data = await interaction.client.chrx.xp.getUser(
152
+ interaction.user.id,
153
+ interaction.guild.id
154
+ );
155
+ await interaction.reply(`You are level ${data.level} with ${data.xp} XP.`);
156
+ }
157
+ ```
158
+
159
+ ---
160
+
161
+ ## .env example
162
+
163
+ ```env
164
+ BOT_TOKEN=your_bot_token
165
+ CLIENT_ID=your_client_id
166
+ LAVA_HOST=your_lavalink_host
167
+ LAVA_PORT=2333
168
+ LAVA_PASS=your_lavalink_password
169
+ LAVA_SECURE=false
170
+ DATABASE_URL=your_postgres_url
171
+ AI_KEY=your_ai_key
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Made by Chrxmee-Bits
@@ -0,0 +1,73 @@
1
+ /**
2
+ * src/modules/AIWrapper.js
3
+ * Lightweight wrapper for AI completions.
4
+ * Pass your API key and model in the options.
5
+ */
6
+
7
+ class AIWrapper {
8
+ /**
9
+ * @param {object} options
10
+ * @param {string} options.apiKey - Your AI API key
11
+ * @param {string} [options.model] - Model to use (default: gpt-3.5-turbo)
12
+ * @param {string} [options.provider] - "openai" | "anthropic" (default: openai)
13
+ */
14
+ constructor(options = {}) {
15
+ if (!options.apiKey) throw new Error("[AIWrapper] apiKey is required.");
16
+ this.apiKey = options.apiKey;
17
+ this.model = options.model ?? "gpt-3.5-turbo";
18
+ this.provider = options.provider ?? "openai";
19
+ }
20
+
21
+ /**
22
+ * Send a prompt and get a response string back.
23
+ * @param {string} prompt
24
+ * @param {string} [systemPrompt]
25
+ * @returns {Promise<string>}
26
+ */
27
+ async ask(prompt, systemPrompt = "You are a helpful assistant.") {
28
+ if (this.provider === "anthropic") {
29
+ return this._askAnthropic(prompt, systemPrompt);
30
+ }
31
+ return this._askOpenAI(prompt, systemPrompt);
32
+ }
33
+
34
+ async _askOpenAI(prompt, systemPrompt) {
35
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
36
+ method: "POST",
37
+ headers: {
38
+ "Content-Type": "application/json",
39
+ Authorization: `Bearer ${this.apiKey}`,
40
+ },
41
+ body: JSON.stringify({
42
+ model: this.model,
43
+ messages: [
44
+ { role: "system", content: systemPrompt },
45
+ { role: "user", content: prompt },
46
+ ],
47
+ }),
48
+ });
49
+ const data = await res.json();
50
+ return data.choices?.[0]?.message?.content ?? "No response.";
51
+ }
52
+
53
+ async _askAnthropic(prompt, systemPrompt) {
54
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
55
+ method: "POST",
56
+ headers: {
57
+ "Content-Type": "application/json",
58
+ "x-api-key": this.apiKey,
59
+ "anthropic-version": "2023-06-01",
60
+ },
61
+ body: JSON.stringify({
62
+ model: this.model,
63
+ max_tokens: 1024,
64
+ system: systemPrompt,
65
+ messages: [{ role: "user", content: prompt }],
66
+ }),
67
+ });
68
+ const data = await res.json();
69
+ return data.content?.[0]?.text ?? "No response.";
70
+ }
71
+ }
72
+
73
+ module.exports = AIWrapper;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * src/core/CommandLoader.js
3
+ * Auto-loads slash commands from the user's commands folder.
4
+ */
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ class CommandLoader {
10
+ constructor(client, commandsPath) {
11
+ this.client = client;
12
+ this.commandsPath = commandsPath ?? path.join(process.cwd(), "commands");
13
+ }
14
+
15
+ load() {
16
+ if (!fs.existsSync(this.commandsPath)) {
17
+ console.warn(`[CommandLoader] Commands folder not found at ${this.commandsPath}`);
18
+ return;
19
+ }
20
+
21
+ const files = fs.readdirSync(this.commandsPath).filter((f) => f.endsWith(".js"));
22
+
23
+ for (const file of files) {
24
+ const command = require(path.join(this.commandsPath, file));
25
+ if ("data" in command && "execute" in command) {
26
+ this.client.commands.set(command.data.name, command);
27
+ console.log(`[CommandLoader] Loaded command: ${command.data.name}`);
28
+ }
29
+ }
30
+
31
+ // Also scan subfolders one level deep (e.g. commands/music/)
32
+ const dirs = fs.readdirSync(this.commandsPath, { withFileTypes: true })
33
+ .filter((d) => d.isDirectory());
34
+
35
+ for (const dir of dirs) {
36
+ const subPath = path.join(this.commandsPath, dir.name);
37
+ const subFiles = fs.readdirSync(subPath).filter((f) => f.endsWith(".js"));
38
+ for (const file of subFiles) {
39
+ const command = require(path.join(subPath, file));
40
+ if ("data" in command && "execute" in command) {
41
+ this.client.commands.set(command.data.name, command);
42
+ console.log(`[CommandLoader] Loaded command: ${command.data.name} (${dir.name})`);
43
+ }
44
+ }
45
+ }
46
+
47
+ console.log(`[CommandLoader] ${this.client.commands.size} commands loaded.`);
48
+ }
49
+ }
50
+
51
+ module.exports = CommandLoader;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * src/modules/Database.js
3
+ * Simple Postgres connection helper.
4
+ */
5
+
6
+ const { Pool } = require("pg");
7
+
8
+ class Database {
9
+ constructor(connectionString) {
10
+ this.pool = new Pool({
11
+ connectionString,
12
+ ssl: { rejectUnauthorized: false },
13
+ max: 10,
14
+ idleTimeoutMillis: 30000,
15
+ connectionTimeoutMillis: 5000,
16
+ keepAlive: true,
17
+ });
18
+
19
+ this.pool.on("error", (err) => {
20
+ console.error("[Database] Pool error:", err.message);
21
+ });
22
+ }
23
+
24
+ async init() {
25
+ const client = await this.pool.connect();
26
+ await client.query("SELECT 1");
27
+ client.release();
28
+ console.log("[Database] Connection verified.");
29
+ }
30
+
31
+ /**
32
+ * Run a query against the database.
33
+ * @param {string} text SQL query
34
+ * @param {any[]} params Query parameters
35
+ */
36
+ async query(text, params) {
37
+ return this.pool.query(text, params);
38
+ }
39
+
40
+ /**
41
+ * Get a client from the pool for transactions.
42
+ */
43
+ async connect() {
44
+ return this.pool.connect();
45
+ }
46
+ }
47
+
48
+ module.exports = Database;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * src/core/EventLoader.js
3
+ * Auto-loads events from the user's events folder.
4
+ */
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ class EventLoader {
10
+ constructor(client, eventsPath) {
11
+ this.client = client;
12
+ this.eventsPath = eventsPath ?? path.join(process.cwd(), "events");
13
+ }
14
+
15
+ load() {
16
+ if (!fs.existsSync(this.eventsPath)) {
17
+ console.warn(`[EventLoader] Events folder not found at ${this.eventsPath}`);
18
+ return;
19
+ }
20
+
21
+ const files = fs.readdirSync(this.eventsPath).filter((f) => f.endsWith(".js"));
22
+
23
+ for (const file of files) {
24
+ const event = require(path.join(this.eventsPath, file));
25
+ if (!event.name || !event.execute) continue;
26
+
27
+ if (event.once) {
28
+ this.client.once(event.name, (...args) => event.execute(...args));
29
+ } else {
30
+ this.client.on(event.name, (...args) => event.execute(...args));
31
+ }
32
+
33
+ console.log(`[EventLoader] Loaded event: ${event.name}`);
34
+ }
35
+ }
36
+ }
37
+
38
+ module.exports = EventLoader;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * src/music/MusicManager.js
3
+ * Handles all Lavalink events and wires in the song marker system.
4
+ */
5
+
6
+ const { EmbedBuilder } = require("discord.js");
7
+ const {
8
+ applyStartMarker,
9
+ startEndMarkerWatcher,
10
+ stopEndMarkerWatcher,
11
+ getMarkers,
12
+ } = require("./songMarkers");
13
+
14
+ class MusicManager {
15
+ constructor(client) {
16
+ this.client = client;
17
+ this._registerEvents();
18
+ }
19
+
20
+ _registerEvents() {
21
+ const lavalink = this.client.lavalink;
22
+
23
+ // ── trackStart ────────────────────────────────────────────────────────
24
+ lavalink.on("trackStart", async (player, track) => {
25
+ const channel = this.client.channels.cache.get(player.textChannelId);
26
+ if (channel) {
27
+ channel.send({
28
+ embeds: [
29
+ new EmbedBuilder()
30
+ .setColor("#5865F2")
31
+ .setTitle("🎵 Now Playing")
32
+ .setDescription(`**[${track.info.title}](${track.info.uri})**`)
33
+ .addFields(
34
+ { name: "Author", value: track.info.author, inline: true },
35
+ { name: "Duration", value: this._msToTime(track.info.duration), inline: true },
36
+ { name: "Requested by", value: `<@${track.info.requester?.id || "Unknown"}>`, inline: true }
37
+ )
38
+ .setThumbnail(track.info.artworkUrl)
39
+ .setTimestamp(),
40
+ ],
41
+ }).catch(() => {});
42
+ }
43
+
44
+ // Song marker system
45
+ const m = getMarkers(track);
46
+ if (!m) return;
47
+ if (m.start != null) await applyStartMarker(player, track);
48
+ if (m.end != null) startEndMarkerWatcher(player, track, m.loop ?? false);
49
+ });
50
+
51
+ // ── trackEnd ──────────────────────────────────────────────────────────
52
+ lavalink.on("trackEnd", (player) => {
53
+ stopEndMarkerWatcher(player.guildId);
54
+ });
55
+
56
+ // ── queueEnd ──────────────────────────────────────────────────────────
57
+ lavalink.on("queueEnd", (player) => {
58
+ stopEndMarkerWatcher(player.guildId);
59
+ const channel = this.client.channels.cache.get(player.textChannelId);
60
+ if (channel) channel.send("✅ Queue finished! Add more songs to keep the vibe going.").catch(() => {});
61
+ });
62
+
63
+ // ── playerDestroy ─────────────────────────────────────────────────────
64
+ lavalink.on("playerDestroy", (player) => {
65
+ stopEndMarkerWatcher(player.guildId);
66
+ });
67
+ }
68
+
69
+ _msToTime(ms) {
70
+ const s = Math.floor((ms / 1000) % 60);
71
+ const m = Math.floor((ms / (1000 * 60)) % 60);
72
+ const h = Math.floor(ms / (1000 * 60 * 60));
73
+ if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
74
+ return `${m}:${String(s).padStart(2, "0")}`;
75
+ }
76
+ }
77
+
78
+ module.exports = MusicManager;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * src/modules/XPSystem.js
3
+ * Built-in XP/leveling system.
4
+ * Requires Database module to be enabled.
5
+ */
6
+
7
+ class XPSystem {
8
+ constructor(client) {
9
+ this.client = client;
10
+ this.cooldowns = new Map(); // userId -> timestamp
11
+ this.cooldownMs = 60000; // 1 minute between XP gains
12
+
13
+ this._registerEvent();
14
+ }
15
+
16
+ _registerEvent() {
17
+ this.client.on("messageCreate", async (message) => {
18
+ if (message.author.bot || !message.guild) return;
19
+ if (!this.client.db) return;
20
+
21
+ const userId = BigInt(message.author.id);
22
+ const guildId = BigInt(message.guild.id);
23
+ const now = Date.now();
24
+
25
+ // Cooldown check
26
+ const key = `${userId}-${guildId}`;
27
+ if (this.cooldowns.has(key) && now - this.cooldowns.get(key) < this.cooldownMs) return;
28
+ this.cooldowns.set(key, now);
29
+
30
+ const xpGain = Math.floor(Math.random() * 10) + 5; // 5-15 XP per message
31
+
32
+ await this.client.db.query(`
33
+ INSERT INTO chrx_user_xp (user_id, guild_id, xp, level)
34
+ VALUES ($1, $2, $3, 0)
35
+ ON CONFLICT (user_id, guild_id)
36
+ DO UPDATE SET xp = chrx_user_xp.xp + $3
37
+ `, [userId, guildId, xpGain]);
38
+
39
+ // Check for level up
40
+ const result = await this.client.db.query(
41
+ `SELECT xp, level FROM chrx_user_xp WHERE user_id = $1 AND guild_id = $2`,
42
+ [userId, guildId]
43
+ );
44
+
45
+ if (!result.rows.length) return;
46
+ const { xp, level } = result.rows[0];
47
+ const xpNeeded = this._xpForLevel(level + 1);
48
+
49
+ if (xp >= xpNeeded) {
50
+ const newLevel = level + 1;
51
+ await this.client.db.query(
52
+ `UPDATE chrx_user_xp SET level = $1, xp = 0 WHERE user_id = $2 AND guild_id = $3`,
53
+ [newLevel, userId, guildId]
54
+ );
55
+ message.channel.send(`🎉 ${message.author} leveled up to **level ${newLevel}**!`).catch(() => {});
56
+ }
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Get XP and level for a user in a guild.
62
+ */
63
+ async getUser(userId, guildId) {
64
+ if (!this.client.db) throw new Error("[XPSystem] Database module is not enabled.");
65
+ const result = await this.client.db.query(
66
+ `SELECT xp, level FROM chrx_user_xp WHERE user_id = $1 AND guild_id = $2`,
67
+ [BigInt(userId), BigInt(guildId)]
68
+ );
69
+ return result.rows[0] ?? { xp: 0, level: 0 };
70
+ }
71
+
72
+ /**
73
+ * XP required to reach a given level.
74
+ */
75
+ _xpForLevel(level) {
76
+ return 100 * level * level;
77
+ }
78
+ }
79
+
80
+ module.exports = XPSystem;
package/core/client.js ADDED
@@ -0,0 +1,163 @@
1
+ /**
2
+ * src/core/Client.js
3
+ * The main ChrxClient class — wraps discord.js Client with
4
+ * auto loaders, Lavalink, and optional modules.
5
+ */
6
+
7
+ const { Client, GatewayIntentBits, Collection, Partials } = require("discord.js");
8
+ const { LavalinkManager } = require("lavalink-client");
9
+ const CommandLoader = require("./CommandLoader");
10
+ const EventLoader = require("./EventLoader");
11
+ const MusicManager = require("./MusicManager");
12
+ const Database = require("./Database");
13
+ const XPSystem = require("./XPSystem");
14
+ const AIWrapper = require("./AIWrapper");
15
+
16
+ class ChrxClient {
17
+ /**
18
+ * @param {object} options
19
+ * @param {string} options.token - Bot token
20
+ * @param {string} [options.commandsPath] - Path to commands folder (default: ./commands)
21
+ * @param {string} [options.eventsPath] - Path to events folder (default: ./events)
22
+ * @param {object} [options.lavalink] - Lavalink config
23
+ * @param {string} options.lavalink.host
24
+ * @param {number} [options.lavalink.port]
25
+ * @param {string} options.lavalink.password
26
+ * @param {boolean} [options.lavalink.secure]
27
+ * @param {object} [options.modules] - Optional modules to enable
28
+ * @param {boolean} [options.modules.music] - Enable music system
29
+ * @param {boolean} [options.modules.xp] - Enable XP system
30
+ * @param {boolean} [options.modules.ai] - Enable AI wrapper
31
+ * @param {string} [options.modules.database] - Postgres connection string
32
+ * @param {number[]} [options.intents] - Override default intents
33
+ */
34
+ constructor(options = {}) {
35
+ if (!options.token) throw new Error("[ChrxClient] token is required.");
36
+
37
+ this._options = options;
38
+
39
+ // ── Discord client ───────────────────────────────────────────────────
40
+ this.client = new Client({
41
+ intents: options.intents ?? [
42
+ GatewayIntentBits.Guilds,
43
+ GatewayIntentBits.GuildMessages,
44
+ GatewayIntentBits.MessageContent,
45
+ GatewayIntentBits.GuildMembers,
46
+ GatewayIntentBits.GuildVoiceStates,
47
+ GatewayIntentBits.DirectMessages,
48
+ ],
49
+ partials: [Partials.Channel, Partials.Message],
50
+ });
51
+
52
+ this.client.commands = new Collection();
53
+ this.client.chrx = this; // expose framework on client for use in commands/events
54
+
55
+ // ── Lavalink ─────────────────────────────────────────────────────────
56
+ if (options.lavalink && options.modules?.music !== false) {
57
+ this._setupLavalink(options.lavalink);
58
+ }
59
+
60
+ // ── Optional modules ─────────────────────────────────────────────────
61
+ if (options.modules?.database) {
62
+ this.db = new Database(options.modules.database);
63
+ this.client.db = this.db;
64
+ }
65
+
66
+ if (options.modules?.xp) {
67
+ this.xp = new XPSystem(this.client);
68
+ }
69
+
70
+ if (options.modules?.ai) {
71
+ this.ai = new AIWrapper(options.modules.ai);
72
+ this.client.ai = this.ai;
73
+ }
74
+
75
+ // ── Loaders ───────────────────────────────────────────────────────────
76
+ this._commandLoader = new CommandLoader(this.client, options.commandsPath);
77
+ this._eventLoader = new EventLoader(this.client, options.eventsPath);
78
+
79
+ // ── Ready event ───────────────────────────────────────────────────────
80
+ this.client.once("ready", async () => {
81
+ if (this.client.lavalink) {
82
+ await this.client.lavalink.init({
83
+ id: this.client.user.id,
84
+ username: this.client.user.username,
85
+ });
86
+ console.log("[ChrxClient] Lavalink initialized!");
87
+ }
88
+
89
+ if (this.db) {
90
+ await this.db.init();
91
+ console.log("[ChrxClient] Database connected!");
92
+ }
93
+
94
+ console.log(`[ChrxClient] Ready! Logged in as ${this.client.user.tag}`);
95
+ });
96
+
97
+ // ── Voice state forwarding for Lavalink ───────────────────────────────
98
+ this.client.on("raw", (d) => {
99
+ if (d.t === "VOICE_SERVER_UPDATE" || d.t === "VOICE_STATE_UPDATE") {
100
+ this.client.lavalink?.sendRawData(d);
101
+ }
102
+ });
103
+
104
+ // ── Global error handlers ─────────────────────────────────────────────
105
+ process.on("unhandledRejection", (reason) => {
106
+ console.error("[ChrxClient] Unhandled Rejection:", reason);
107
+ });
108
+ process.on("uncaughtException", (err) => {
109
+ console.error("[ChrxClient] Uncaught Exception:", err);
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Set up the LavalinkManager and attach it to the client.
115
+ */
116
+ _setupLavalink(cfg) {
117
+ this.client.lavalink = new LavalinkManager({
118
+ nodes: [
119
+ {
120
+ host: cfg.host,
121
+ port: cfg.port ?? 2333,
122
+ authorization: cfg.password,
123
+ secure: cfg.secure ?? false,
124
+ id: "main",
125
+ },
126
+ ],
127
+ sendToShard: (guildId, payload) => {
128
+ const guild = this.client.guilds.cache.get(guildId);
129
+ if (guild) guild.shard.send(payload);
130
+ },
131
+ client: {
132
+ id: process.env.CLIENT_ID,
133
+ username: "ChrxBot",
134
+ },
135
+ playerOptions: {
136
+ defaultSearchPlatform: "ytsearch",
137
+ onDisconnect: { destroyPlayer: true },
138
+ onEmptyQueue: { destroyAfterMs: 30000 },
139
+ },
140
+ });
141
+
142
+ // Attach music manager (handles events + marker system)
143
+ this.music = new MusicManager(this.client);
144
+
145
+ this.client.lavalink.on("nodeConnect", (node) =>
146
+ console.log(`[ChrxClient] Lavalink node "${node.id}" connected!`)
147
+ );
148
+ this.client.lavalink.on("nodeError", (node, err) =>
149
+ console.error(`[ChrxClient] Lavalink node "${node.id}" error:`, err.message)
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Load commands and events then log in.
155
+ */
156
+ async start() {
157
+ this._commandLoader.load();
158
+ this._eventLoader.load();
159
+ await this.client.login(this._options.token);
160
+ }
161
+ }
162
+
163
+ module.exports = { ChrxClient };
package/core/index.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * src/index.js
3
+ * Main entry point — exports everything the user needs.
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");
11
+ const {
12
+ parseTime,
13
+ formatTime,
14
+ setMarker,
15
+ getMarkers,
16
+ clearMarker,
17
+ applyStartMarker,
18
+ startEndMarkerWatcher,
19
+ stopEndMarkerWatcher,
20
+ } = require("./songMarkers");
21
+
22
+ module.exports = {
23
+ // Core
24
+ ChrxClient,
25
+
26
+ // Modules
27
+ Database,
28
+ XPSystem,
29
+ AIWrapper,
30
+ MusicManager,
31
+
32
+ // Song marker utilities (for advanced users)
33
+ parseTime,
34
+ formatTime,
35
+ setMarker,
36
+ getMarkers,
37
+ clearMarker,
38
+ applyStartMarker,
39
+ startEndMarkerWatcher,
40
+ stopEndMarkerWatcher,
41
+ };
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Song marker system for Discord Music Bots.
3
+ * Per-track start/end marker system — lavalink-client v2 compatible.
4
+ *
5
+ * Markers are stored in memory. They persist as long as the bot is running
6
+ * but reset on restart. Swap the Map for Postgres if you want them permanent.
7
+ */
8
+
9
+ // Key: track URI (or title fallback) → { start?: ms, end?: ms, loop?: bool }
10
+ const markers = new Map();
11
+
12
+ // Active end-marker watcher per guild: guildId → intervalId
13
+ const endIntervals = new Map();
14
+
15
+ // ── Time utilities ────────────────────────────────────────────────────────
16
+
17
+ /**
18
+ * Parse "mm:ss" or "hh:mm:ss" into milliseconds.
19
+ * Returns null if the format is invalid.
20
+ */
21
+ function parseTime(timeStr) {
22
+ const parts = timeStr.trim().split(":").map(Number);
23
+ if (parts.some(isNaN)) return null;
24
+ if (parts.length === 2) return (parts[0] * 60 + parts[1]) * 1000;
25
+ if (parts.length === 3) return (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1000;
26
+ return null;
27
+ }
28
+
29
+ /**
30
+ * Format milliseconds into a readable "m:ss" string.
31
+ */
32
+ function formatTime(ms) {
33
+ const totalSec = Math.floor(ms / 1000);
34
+ const m = Math.floor(totalSec / 60);
35
+ const s = totalSec % 60;
36
+ return `${m}:${s.toString().padStart(2, "0")}`;
37
+ }
38
+
39
+ // ── Key resolution ────────────────────────────────────────────────────────
40
+
41
+ /**
42
+ * Get a stable unique key for a track.
43
+ * lavalink-client v2 stores info under track.info.*
44
+ */
45
+ function getKey(track) {
46
+ return track?.info?.uri || track?.info?.title || track?.uri || track?.title || null;
47
+ }
48
+
49
+ // ── Marker CRUD ───────────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Set a start or end marker on a track.
53
+ * @param {object} track
54
+ * @param {"start"|"end"} type
55
+ * @param {number} ms
56
+ * @param {boolean} [loop=false] Only relevant for end markers
57
+ */
58
+ function setMarker(track, type, ms, loop = false) {
59
+ const key = getKey(track);
60
+ if (!key) return;
61
+ const existing = markers.get(key) || {};
62
+ markers.set(key, {
63
+ ...existing,
64
+ [type]: ms,
65
+ ...(type === "end" ? { loop } : {}),
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Get the markers object for a track ({ start?, end?, loop? }) or null.
71
+ */
72
+ function getMarkers(track) {
73
+ const key = getKey(track);
74
+ if (!key) return null;
75
+ return markers.get(key) || null;
76
+ }
77
+
78
+ /**
79
+ * Clear a start, end, or both markers from a track.
80
+ * @param {object} track
81
+ * @param {"start"|"end"|"both"} type
82
+ */
83
+ function clearMarker(track, type) {
84
+ const key = getKey(track);
85
+ if (!key) return;
86
+ const existing = markers.get(key);
87
+ if (!existing) return;
88
+
89
+ if (type === "both") {
90
+ markers.delete(key);
91
+ return;
92
+ }
93
+
94
+ delete existing[type];
95
+ if (type === "end") delete existing.loop;
96
+
97
+ if (existing.start == null && existing.end == null) {
98
+ markers.delete(key);
99
+ } else {
100
+ markers.set(key, existing);
101
+ }
102
+ }
103
+
104
+ // ── Marker application ────────────────────────────────────────────────────
105
+
106
+ /**
107
+ * Seek to the start marker when a track begins.
108
+ * Called inside the trackStart event.
109
+ *
110
+ * lavalink-client v2: player.seek(ms) accepts a number.
111
+ */
112
+ async function applyStartMarker(player, track) {
113
+ const m = getMarkers(track);
114
+ if (!m?.start) return;
115
+
116
+ // Brief delay so Lavalink has buffered enough to accept a seek
117
+ await sleep(350);
118
+
119
+ try {
120
+ await player.seek(m.start);
121
+ } catch (err) {
122
+ console.error("[SongMarkers] applyStartMarker failed:", err.message);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Start a 500ms polling interval that enforces the end marker.
128
+ * When position >= end marker:
129
+ * - If loop is true → seek back to start marker (or 0)
130
+ * - If loop is false → skip to the next track
131
+ *
132
+ * @param {object} player
133
+ * @param {object} track
134
+ * @param {boolean} [loop=false]
135
+ */
136
+ function startEndMarkerWatcher(player, track, loop = false) {
137
+ stopEndMarkerWatcher(player.guildId); // always clear before starting fresh
138
+
139
+ const m = getMarkers(track);
140
+ if (!m?.end) return;
141
+
142
+ const intervalId = setInterval(async () => {
143
+ try {
144
+ // If the player moved on to a different track, stop watching
145
+ const current = player.queue?.current;
146
+ if (!current || getKey(current) !== getKey(track)) {
147
+ stopEndMarkerWatcher(player.guildId);
148
+ return;
149
+ }
150
+
151
+ // lavalink-client v2: player.position is a number in ms
152
+ if (player.position >= m.end) {
153
+ if (loop) {
154
+ const seekTo = m.start ?? 0;
155
+ await player.seek(seekTo);
156
+ } else {
157
+ // Skip to next track (or stop if queue is empty)
158
+ await player.skip().catch(() => player.stopTrack());
159
+ stopEndMarkerWatcher(player.guildId);
160
+ }
161
+ }
162
+ } catch (err) {
163
+ console.error("[SongMarkers] endMarkerWatcher tick failed:", err.message);
164
+ }
165
+ }, 500);
166
+
167
+ endIntervals.set(player.guildId, intervalId);
168
+ }
169
+
170
+ /**
171
+ * Stop the end marker watcher for a guild.
172
+ */
173
+ function stopEndMarkerWatcher(guildId) {
174
+ if (endIntervals.has(guildId)) {
175
+ clearInterval(endIntervals.get(guildId));
176
+ endIntervals.delete(guildId);
177
+ }
178
+ }
179
+
180
+ // ── Internal ──────────────────────────────────────────────────────────────
181
+
182
+ function sleep(ms) {
183
+ return new Promise((res) => setTimeout(res, ms));
184
+ }
185
+
186
+ module.exports = {
187
+ parseTime,
188
+ formatTime,
189
+ getKey,
190
+ setMarker,
191
+ getMarkers,
192
+ clearMarker,
193
+ applyStartMarker,
194
+ startEndMarkerWatcher,
195
+ stopEndMarkerWatcher,
196
+ };
package/index.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * src/index.js
3
+ * Main entry point — exports everything the user needs.
4
+ */
5
+
6
+ const { ChrxClient } = require("./core/Client");
7
+ const Database = require("./core/Database");
8
+ const XPSystem = require("./core/XPSystem");
9
+ const AIWrapper = require("./core/AIWrapper");
10
+ const MusicManager = require("./core/MusicManager");
11
+ const {
12
+ parseTime,
13
+ formatTime,
14
+ setMarker,
15
+ getMarkers,
16
+ clearMarker,
17
+ applyStartMarker,
18
+ startEndMarkerWatcher,
19
+ stopEndMarkerWatcher,
20
+ } = require("./core/songMarkers");
21
+
22
+ module.exports = {
23
+ // Core
24
+ ChrxClient,
25
+
26
+ // Modules
27
+ Database,
28
+ XPSystem,
29
+ AIWrapper,
30
+ MusicManager,
31
+
32
+ // Song marker utilities (for advanced users)
33
+ parseTime,
34
+ formatTime,
35
+ setMarker,
36
+ getMarkers,
37
+ clearMarker,
38
+ applyStartMarker,
39
+ startEndMarkerWatcher,
40
+ stopEndMarkerWatcher,
41
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "chrxmaticc-framework",
3
+ "version": "1.0.2",
4
+ "description": "A batteries-included Discord bot framework with music, AI, XP and database support.",
5
+ "main": "index.js",
6
+ "keywords": [
7
+ "discord",
8
+ "discord.js",
9
+ "lavalink",
10
+ "music",
11
+ "bot",
12
+ "framework",
13
+ "ai",
14
+ "xp"
15
+ ],
16
+ "author": "Chrxmee-Bits",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/Chrxmee-Bits/Chrxmaticc-Framework.git"
21
+ },
22
+ "peerDependencies": {
23
+ "discord.js": "^14.0.0"
24
+ },
25
+ "dependencies": {
26
+ "lavalink-client": "^2.0.0",
27
+ "pg": "^8.11.0",
28
+ "dotenv": "^16.0.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=16.0.0"
32
+ },
33
+ "scripts": {
34
+ "test": "node -e \"console.log('No tests yet')\"",
35
+ "prepublishOnly": "npm test"
36
+ }
37
+ }