This commit is contained in:
Patryk Koreń
2025-12-23 19:38:20 +01:00
commit 7b72b15caa
12 changed files with 3846 additions and 0 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
target
.git
.gitignore
Dockerfile

1
.env.example Normal file
View File

@@ -0,0 +1 @@
DISCORD_TOKEN

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/target
/data
.env

3633
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[package]
name = "dbot"
version = "0.1.0"
edition = "2024"
[dependencies]
serenity = "0.12.5"
songbird = { version="0.5.0", features = ["builtin-queue"] }
tokio = { version = "1.21.1", features = ["rt-multi-thread", "macros"] }

22
Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM rust:1.91-alpine as builder
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /bot
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM alpine:latest
RUN apk add --no-cache \
ca-certificates \
libgcc \
libstdc++
WORKDIR /app
COPY --from=builder /dbot/target/x86_64-unknown-linux-musl/release/dbot /usr/local/bin/dbot
RUN wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux /usr/local/bin/dbot
CMD ["dbot"]

5
docker-compose.yml Normal file
View File

@@ -0,0 +1,5 @@
services:
bot:
build: .
env_file:
- .env

28
src/audio/downloader.rs Normal file
View File

@@ -0,0 +1,28 @@
use std::env;
use std::process::Command;
use std::error::Error;
pub fn get_audio(url: String) -> Result<String, Box<dyn Error>> {
let yt_dlp_bin = env::var("YT_DLP_BIN_PATH").unwrap_or("yt-dlp_linux".to_string());
let data_dir = env::var("DATA_DIR").unwrap_or("./data".to_string());
let output = Command::new(&yt_dlp_bin)
.args([
"-x",
"--audio-format", "opus",
"--audio-quality", "0",
"--no-playlist",
"--paths", &data_dir,
&url,
])
.output();
let cmd = format!("find ./data -type f -iname \"*$({} -x --print id \"{}\")*.opus\" -exec realpath {{}} \\;", &yt_dlp_bin, &url);
println!("{}", cmd);
let res = Command::new("sh")
.arg("-c")
.arg(cmd)
.output()?;
let path = String::from_utf8_lossy(&res.stdout).to_string();
Ok(path)
}

1
src/audio/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod downloader;

1
src/commands/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod play;

46
src/commands/play.rs Normal file
View File

@@ -0,0 +1,46 @@
use serenity::all::{Context, GuildId};
use serenity::builder::{CreateCommand, CreateCommandOption};
use serenity::model::application::{CommandOptionType, ResolvedOption, ResolvedValue};
use songbird::Call;
use songbird::driver::opus::ffi;
use songbird::input::{self, AudioStream, Input};
use std::env;
use std::process::Command;
use crate::audio::downloader::get_audio;
pub async fn run(options: &[ResolvedOption<'_>], ctx: Context) -> String {
if let Some(ResolvedOption {
value: ResolvedValue::String(url),
..
}) = options.first()
{
let path = get_audio(url.to_string());
let manager = songbird::get(&ctx).await.unwrap().clone();
let guild_id = GuildId::new(
env::var("GUILD_ID")
.expect("Expected GUILD_ID in environment")
.parse()
.expect("GUILD_ID must be an integer"),
);
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
let src = ffi
let _ = handler.play_input();
}
format!("{}", url)
} else {
"Please provide a valid url".to_string()
}
}
pub fn register() -> CreateCommand {
CreateCommand::new("play")
.description("A play command")
.add_option(
CreateCommandOption::new(CommandOptionType::String, "url", "song to look for")
.required(true),
)
}

92
src/main.rs Normal file
View File

@@ -0,0 +1,92 @@
mod audio;
mod commands;
use std::env;
use serenity::all::ChannelId;
use serenity::async_trait;
use serenity::builder::{CreateInteractionResponse, CreateInteractionResponseMessage};
use serenity::model::application::{Command, Interaction};
use serenity::model::gateway::Ready;
use serenity::model::id::GuildId;
use serenity::prelude::*;
use songbird::SerenityInit;
struct Handler;
#[async_trait]
impl EventHandler for Handler {
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
if let Interaction::Command(command) = interaction {
let clone_ctx = ctx.clone();
let manager = songbird::get(&clone_ctx).await.unwrap().clone();
let guild_id = command.guild_id.expect("Guild ID missing");
let user_id = command.user.id;
println!("Received command interaction: {command:#?}");
// let channel_id = command.channel_id;
let channel_id = ChannelId::new(515300847790325790);
let join_result = manager.join(guild_id, channel_id).await;
if join_result.is_ok() {
println!("Joined your voice channel!")
} else {
println!("Failed to join your voice channel")
}
let content = match command.data.name.as_str() {
"play" => Some(commands::play::run(&command.data.options(), ctx.clone()).await),
_ => Some("not implemented :(".to_string()),
};
if let Some(content) = content {
let data = CreateInteractionResponseMessage::new().content(content);
let builder = CreateInteractionResponse::Message(data);
if let Err(why) = command.create_response(&ctx.http.clone(), builder).await {
println!("Cannot respond to slash command: {why}");
}
}
}
}
async fn ready(&self, ctx: Context, ready: Ready) {
// println!("{} is connected!", ready.user.name);
let guild_id = GuildId::new(
env::var("GUILD_ID")
.expect("Expected GUILD_ID in environment")
.parse()
.expect("GUILD_ID must be an integer"),
);
let commands = guild_id
.set_commands(&ctx.http, vec![commands::play::register()])
.await;
// println!("I now have the following guild slash commands: {commands:#?}");
}
}
#[tokio::main]
async fn main() {
// Configure the client with your Discord bot token in the environment.
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
// Build our client.
let intents = GatewayIntents::GUILD_VOICE_STATES
| GatewayIntents::GUILD_MESSAGES
| GatewayIntents::MESSAGE_CONTENT;
let mut client = Client::builder(token, intents)
.event_handler(Handler)
.register_songbird()
.await
.expect("Error creating client");
// Finally, start a single shard, and start listening to events.
//
// Shards will automatically attempt to reconnect, and will perform exponential backoff until
// it reconnects.
if let Err(why) = client.start().await {
println!("Client error: {why:?}");
}
}