Initial project setup with basic structure, including environment configuration, command handling, and Lavalink integration.
This commit is contained in:
parent
322434afbd
commit
05fec6747d
4
.env.example
Normal file
4
.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
DISCORD_TOKEN=your_token_here
|
||||||
|
LAVALINK_HOST=127.0.0.1
|
||||||
|
LAVALINK_PORT=2333
|
||||||
|
LAVALINK_PASSWORD=your_password_here
|
||||||
11
.gitignore
vendored
11
.gitignore
vendored
@ -2,12 +2,21 @@
|
|||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
debug/
|
debug/
|
||||||
target/
|
/target/
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
|
||||||
|
# dotenv environment variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
# VSCode settings
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
# These are backup files generated by rustfmt
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
||||||
|
|||||||
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "discord-music-bot"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serenity = { version = "0.12", features = ["client", "gateway", "cache", "model", "http", "builder", "voice", "rustls_backend", "application"] }
|
||||||
|
lavalink-rs = { version = "0.14", features = ["serenity", "tungstenite-rustls-native-roots"] }
|
||||||
|
tokio = { version = "1.28", features = ["macros", "rt-multi-thread"] }
|
||||||
|
anyhow = "1.0"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||||||
|
dotenv = "0.15"
|
||||||
|
futures = "0.3"
|
||||||
|
inventory = { version = "0.3", features = ["proc-macro"] }
|
||||||
|
url = "2.4"
|
||||||
56
README.md
56
README.md
@ -1,3 +1,57 @@
|
|||||||
# discord-music-bot
|
# discord-music-bot
|
||||||
|
|
||||||
Discord bot made in Rust using serenity and lavalink-rs
|
Discord music bot template written in Rust using `serenity` and `lavalink-rs`.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- 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))
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Copy `.env.example` to `.env` and fill in your credentials:
|
||||||
|
```
|
||||||
|
DISCORD_TOKEN=your_discord_bot_token
|
||||||
|
LAVALINK_HOST=127.0.0.1
|
||||||
|
LAVALINK_PORT=2333
|
||||||
|
LAVALINK_PASSWORD=your_lavalink_password
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Build the project:
|
||||||
|
```sh
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the bot:
|
||||||
|
```sh
|
||||||
|
cargo run --release
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT
|
||||||
16
src/commands/join.rs
Normal file
16
src/commands/join.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use super::SlashCommand;
|
||||||
|
use serenity::builder::CreateCommand;
|
||||||
|
use serenity::prelude::Context;
|
||||||
|
use serenity::model::application::interaction::Interaction;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
inventory::submit! {
|
||||||
|
SlashCommand {
|
||||||
|
name: "join",
|
||||||
|
register: |c| c.name("join").description("Joins your voice channel"),
|
||||||
|
handler: |ctx: Context, interaction: Interaction| Box::pin(async move {
|
||||||
|
// TODO: Implement join logic (e.g., move bot to user voice channel)
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/commands/leave.rs
Normal file
16
src/commands/leave.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use super::SlashCommand;
|
||||||
|
use serenity::builder::CreateApplicationCommand;
|
||||||
|
use serenity::prelude::Context;
|
||||||
|
use serenity::model::interactions::Interaction;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
inventory::submit! {
|
||||||
|
SlashCommand {
|
||||||
|
name: "leave",
|
||||||
|
register: |c| c.name("leave").description("Leaves the voice channel"),
|
||||||
|
handler: |ctx: Context, interaction: Interaction| Box::pin(async move {
|
||||||
|
// TODO: Implement leave logic (e.g., disconnect from voice channel)
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/commands/mod.rs
Normal file
26
src/commands/mod.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use serenity::builder::CreateCommand;
|
||||||
|
use serenity::prelude::Context;
|
||||||
|
use serenity::model::interactions::Interaction;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
/// Defines a slash command with its registration and handler.
|
||||||
|
pub struct SlashCommand {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub register: fn(&mut CreateCommand) -> &mut CreateCommand,
|
||||||
|
pub handler: fn(Context, Interaction) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::collect!(SlashCommand);
|
||||||
|
|
||||||
|
/// Returns all registered slash commands.
|
||||||
|
pub fn get_slash_commands() -> Vec<&'static SlashCommand> {
|
||||||
|
inventory::iter::<SlashCommand>
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register individual command modules
|
||||||
|
pub mod ping;
|
||||||
|
pub mod join;
|
||||||
|
pub mod play;
|
||||||
|
pub mod leave;
|
||||||
16
src/commands/ping.rs
Normal file
16
src/commands/ping.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use super::SlashCommand;
|
||||||
|
use serenity::builder::CreateCommand;
|
||||||
|
use serenity::prelude::Context;
|
||||||
|
use serenity::model::application::interaction::Interaction;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
inventory::submit! {
|
||||||
|
SlashCommand {
|
||||||
|
name: "ping",
|
||||||
|
register: |c: &mut CreateCommand| c.name("ping").description("Replies with Pong!"),
|
||||||
|
handler: |ctx: Context, interaction: Interaction| Box::pin(async move {
|
||||||
|
// TODO: Implement ping logic (e.g., respond with "Pong!")
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/commands/play.rs
Normal file
26
src/commands/play.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use super::SlashCommand;
|
||||||
|
use serenity::builder::CreateApplicationCommand;
|
||||||
|
use serenity::model::interactions::application_command::ApplicationCommandOptionType;
|
||||||
|
use serenity::prelude::Context;
|
||||||
|
use serenity::model::interactions::Interaction;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
inventory::submit! {
|
||||||
|
SlashCommand {
|
||||||
|
name: "play",
|
||||||
|
register: |c| {
|
||||||
|
c.name("play")
|
||||||
|
.description("Plays audio from a given URL")
|
||||||
|
.create_option(|o| {
|
||||||
|
o.name("url")
|
||||||
|
.description("Track URL to play")
|
||||||
|
.kind(ApplicationCommandOptionType::String)
|
||||||
|
.required(true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handler: |ctx: Context, interaction: Interaction| Box::pin(async move {
|
||||||
|
// TODO: Implement play logic (e.g., fetch URL argument and instruct Lavalink to play)
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/handler.rs
Normal file
49
src/handler.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use serenity::async_trait;
|
||||||
|
use serenity::model::gateway::Ready;
|
||||||
|
use serenity::model::application::interaction::{Interaction, InteractionResponseType};
|
||||||
|
use serenity::model::channel::VoiceState;
|
||||||
|
use serenity::prelude::*;
|
||||||
|
use anyhow::Result;
|
||||||
|
use tracing::info;
|
||||||
|
use crate::commands::get_slash_commands;
|
||||||
|
use crate::state::LavalinkClientKey;
|
||||||
|
|
||||||
|
pub struct Handler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventHandler for Handler {
|
||||||
|
async fn ready(&self, ctx: Context, ready: Ready) {
|
||||||
|
info!("{} is connected!", ready.user.name);
|
||||||
|
let commands = get_slash_commands();
|
||||||
|
for cmd in commands {
|
||||||
|
match ctx.http.create_global_application_command(|c| (cmd.register)(c)).await {
|
||||||
|
Ok(_) => info!("Registered slash command: {}", cmd.name),
|
||||||
|
Err(err) => info!("Failed to register {}: {:?}", cmd.name, err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||||
|
if let Interaction::ApplicationCommand(ref command) = interaction {
|
||||||
|
if let Err(err) = command.defer(&ctx.http).await {
|
||||||
|
info!("Failed to defer response: {:?}", err);
|
||||||
|
}
|
||||||
|
for cmd in get_slash_commands() {
|
||||||
|
if cmd.name == command.data.name {
|
||||||
|
if let Err(err) = (cmd.handler)(ctx.clone(), interaction.clone()).await {
|
||||||
|
info!("Command error {}: {:?}", cmd.name, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn voice_state_update(&self, ctx: Context, old: Option<VoiceState>, new: VoiceState) {
|
||||||
|
if let Some(guild) = new.guild_id {
|
||||||
|
let data_read = ctx.data.read().await;
|
||||||
|
if let Some(lavalink) = data_read.get::<LavalinkClientKey>() {
|
||||||
|
lavalink.on_voice_state_update(&new).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/lavalink_handler.rs
Normal file
37
src/lavalink_handler.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// src/lavalink_handler.rs
|
||||||
|
use anyhow::Result;
|
||||||
|
use tracing::info;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use lavalink_rs::prelude::{LavalinkClient, LavalinkClientBuilder, EventHandler, TrackStartEvent, TrackEndEvent};
|
||||||
|
use crate::utils::env_var;
|
||||||
|
|
||||||
|
/// Handles Lavalink events such as track start and end.
|
||||||
|
pub struct LavalinkHandler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventHandler for LavalinkHandler {
|
||||||
|
async fn track_start(&self, _client: &LavalinkClient, event: &TrackStartEvent) {
|
||||||
|
info!("Track started: {}", event.track);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn track_end(&self, _client: &LavalinkClient, event: &TrackEndEvent) {
|
||||||
|
info!("Track ended: {}", event.track);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add more event handlers as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes and returns a Lavalink client.
|
||||||
|
pub async fn create_lavalink_client() -> Result<LavalinkClient> {
|
||||||
|
let host = env_var("LAVALINK_HOST");
|
||||||
|
let port = env_var("LAVALINK_PORT").parse::<u16>()?;
|
||||||
|
let password = env_var("LAVALINK_PASSWORD");
|
||||||
|
let url = format!("ws://{}:{}", host, port);
|
||||||
|
|
||||||
|
let builder = LavalinkClientBuilder::new(&url)
|
||||||
|
.password(&password)
|
||||||
|
.event_handler(LavalinkHandler);
|
||||||
|
|
||||||
|
let client = builder.build().await?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
38
src/main.rs
Normal file
38
src/main.rs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// src/main.rs
|
||||||
|
mod handler;
|
||||||
|
mod lavalink_handler;
|
||||||
|
mod state;
|
||||||
|
mod utils;
|
||||||
|
mod commands;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use dotenv::dotenv;
|
||||||
|
use std::env;
|
||||||
|
use tracing_subscriber;
|
||||||
|
use serenity::prelude::TypeMapKey;
|
||||||
|
use serenity::Client;
|
||||||
|
use crate::lavalink_handler::create_lavalink_client;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
dotenv().ok();
|
||||||
|
tracing_subscriber::fmt().with_env_filter("info").init();
|
||||||
|
|
||||||
|
let token = env::var("DISCORD_TOKEN")?;
|
||||||
|
|
||||||
|
// Initialize Lavalink client
|
||||||
|
let lavalink_client = create_lavalink_client().await?;
|
||||||
|
|
||||||
|
let mut client = Client::builder(&token)
|
||||||
|
.event_handler(handler::Handler)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut data = client.data.write().await;
|
||||||
|
data.insert::<state::LavalinkClientKey>(lavalink_client);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.start().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
8
src/state.rs
Normal file
8
src/state.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// src/state.rs
|
||||||
|
use serenity::prelude::TypeMapKey;
|
||||||
|
use lavalink_rs::prelude::LavalinkClient;
|
||||||
|
|
||||||
|
pub struct LavalinkClientKey;
|
||||||
|
impl TypeMapKey for LavalinkClientKey {
|
||||||
|
type Value = LavalinkClient;
|
||||||
|
}
|
||||||
7
src/utils.rs
Normal file
7
src/utils.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// src/utils.rs
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
/// Retrieves an environment variable or panics if it's not set.
|
||||||
|
pub fn env_var(key: &str) -> String {
|
||||||
|
env::var(key).expect(&format!("Environment variable {} not set", key))
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user