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.
- package/.github/workflows/npm-publish.yml +33 -0
- package/README.md +176 -0
- package/core/AIWrapper.js +73 -0
- package/core/CommandLoader.js +51 -0
- package/core/Database.js +48 -0
- package/core/EventLoader.js +38 -0
- package/core/MusicManager.js +78 -0
- package/core/XPSystem.js +80 -0
- package/core/client.js +163 -0
- package/core/index.js +41 -0
- package/core/songMarkers.js +196 -0
- package/index.js +41 -0
- package/package.json +37 -0
|
@@ -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;
|
package/core/Database.js
ADDED
|
@@ -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;
|
package/core/XPSystem.js
ADDED
|
@@ -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
|
+
}
|