Compare commits

..

5 Commits

7 changed files with 177 additions and 35 deletions

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
# Use official Node.js LTS image
FROM node:18-alpine
# Create app directory
WORKDIR /app
# Install build dependencies and copy manifest
RUN apk add --no-cache python3 make g++
COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Expose no ports (Discord bot)
# Define default environment variables (optional)
ENV NODE_ENV=production
# Start the bot
CMD ["node", "src/index.js"]

View File

@ -1,57 +1,69 @@
# discord-music-bot
Discord music bot template written in Rust using `serenity` and `lavalink-rs`.
Discord music bot template written in NodeJS using `discord.js` and `erela.js`, with Lavalink support.
## Features
- Slash commands: `/ping`, `/join`, `/play`, `/leave`
- LavaLink integration for audio playback
- Modular command handler structure
- Slash commands: `/ping`, `/join`, `/play`, `/leave`
- Lavalink integration for audio playback
- Modular command handler structure
## Prerequisites
- Rust (stable toolchain)
- A Discord application with bot token
- LavaLink server running (see [LavaLink](https://github.com/freyacodes/Lavalink))
- Node.js (>=14)
- pnpm or npm
- A Discord application with bot token
- LavaLink server for audio streaming
## Setup
1. Copy `.env.example` to `.env` and fill in your credentials:
```
```env
DISCORD_TOKEN=your_discord_bot_token
CLIENT_ID=your_discord_application_id
LAVALINK_HOST=127.0.0.1
LAVALINK_PORT=2333
LAVALINK_PASSWORD=your_lavalink_password
```
2. Build the project:
2. Install dependencies:
```sh
cargo build --release
pnpm install
```
3. Run tests:
```sh
npm test
```
4. Register slash commands:
```sh
npm start # or node deploy-commands.js
```
5. Start the bot:
```sh
npm start
```
3. Run the bot:
```sh
cargo run --release
```
## Docker
A `Dockerfile` and `docker-compose.yml` are provided for containerized deployment.
- Build and run with Docker Compose:
```sh
docker-compose up --build
```
- Environment variables are loaded from `.env`.
- Lavalink service is configured in `docker-compose.yml` alongside the bot.
## Project Structure
- `src/main.rs` - Bot entry point, initializes Serenity and LavaLink clients
- `src/handler.rs` - Serenity event handlers (ready, interaction, voice updates)
- `src/lavalink_handler.rs` - LavaLink event handlers
- `src/state.rs` - TypeMap keys for shared state
- `src/utils.rs` - Utility functions (env management)
- `src/commands/` - Modular slash command definitions and handlers
## Commands
- `/ping` - Replies with "Pong!"
- `/join` - Bot joins your voice channel
- `/play url:<URL>` - Plays audio from the given URL
- `/leave` - Bot leaves the voice channel
## TODO
- Implement actual command logic in `src/commands/*.rs`
- Add error handling and command concurrency management
- Expand LavaLink event handlers
- `src/index.js` — Entry point
- `src/commands/` — Slash command modules
- `src/events/` — Discord event handlers
- `src/structures/` — Erela.js (Lavalink) event wiring
- `src/utils/logger.js` — Logging setup
- `deploy-commands.js` — Slash command registration script
- `Dockerfile` — Bot container image
- `docker-compose.yml` — Multi-service setup (bot + Lavalink)
## License
MIT
MIT

24
docker-compose.yml Normal file
View File

@ -0,0 +1,24 @@
version: '3.8'
services:
lavalink:
image: jagrosh/lavalink:latest
container_name: lavalink
ports:
- "2333:2333"
environment:
- LAVALINK_PASSWORD=${LAVALINK_PASSWORD}
volumes:
# Optional: mount custom configuration if needed
# - ./application.yml:/opt/Lavalink/application.yml
bot:
build: .
container_name: discord-music-bot
env_file:
- .env
environment:
- LAVALINK_HOST=lavalink
- LAVALINK_PORT=2333
depends_on:
- lavalink

View File

@ -4,7 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"start": "node src/index.js",
"test": "jest"
},
"keywords": [],
"author": "",
@ -14,5 +15,9 @@
"dotenv": "^16.5.0",
"erela.js": "^2.4.0",
"winston": "^3.17.0"
},
"devDependencies": {
"jest": "^29.7.0",
"js-yaml": "^4.1.0"
}
}

View File

@ -0,0 +1,55 @@
jest.mock('discord.js', () => {
const original = jest.requireActual('discord.js');
const mockRest = {
put: jest.fn().mockResolvedValue([{ length: 1 }]),
setToken: jest.fn().mockReturnThis(),
};
return {
...original,
REST: jest.fn(() => mockRest),
Routes: {
applicationCommands: jest.fn().mockReturnValue('/fake-route'),
},
};
});
jest.mock('fs', () => ({
readdirSync: jest.fn(() => ['ping.js']),
}));
jest.mock('node:path', () => {
const actual = jest.requireActual('node:path');
return {
...actual,
join: (...args) => args.join('/'),
resolve: (...args) => args.join('/'),
};
});
describe('deploy-commands.js', () => {
let origEnv;
beforeAll(() => {
origEnv = { ...process.env };
process.env.CLIENT_ID = '12345';
process.env.DISCORD_TOKEN = 'token';
});
afterAll(() => {
process.env = origEnv;
jest.resetModules();
});
test('registers commands via REST API', async () => {
const mockLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
jest.mock('../src/utils/logger', () => mockLogger);
// Run the script
await require('../deploy-commands.js');
const { REST } = require('discord.js');
expect(REST).toHaveBeenCalled();
const restInstance = REST.mock.results[0].value;
expect(restInstance.setToken).toHaveBeenCalledWith('token');
expect(restInstance.put).toHaveBeenCalledWith('/fake-route', { body: expect.any(Array) });
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Started refreshing'));
});
});

View File

@ -0,0 +1,10 @@
const { spawnSync } = require('child_process');
describe('NPM Start Script', () => {
test('npm start exits without error when DISCORD_TOKEN is provided', () => {
const env = { ...process.env, DISCORD_TOKEN: 'dummy-token', CLIENT_ID: '123', LAVALINK_HOST: 'localhost', LAVALINK_PORT: '2333', LAVALINK_PASSWORD: 'pass' };
const result = spawnSync('pnpm', ['start'], { env, encoding: 'utf-8' });
// The script starts the bot; if it reaches login attempt, exit code is 0
expect(result.status).toBe(0);
});
});

13
tests/startup.test.js Normal file
View File

@ -0,0 +1,13 @@
const { spawnSync } = require('child_process');
describe('Bot Startup', () => {
test('exits with code 1 if DISCORD_TOKEN is missing', () => {
// Clear DISCORD_TOKEN
const env = { ...process.env };
delete env.DISCORD_TOKEN;
const result = spawnSync('node', ['src/index.js'], { env, encoding: 'utf-8' });
expect(result.status).toBe(1);
expect(result.stderr || result.stdout).toMatch(/DISCORD_TOKEN is missing/);
});
});