init
This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
target
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
|
||||
1
.env.example
Normal file
1
.env.example
Normal file
@@ -0,0 +1 @@
|
||||
DISCORD_TOKEN
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
/data
|
||||
.env
|
||||
3633
Cargo.lock
generated
Normal file
3633
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
Cargo.toml
Normal file
9
Cargo.toml
Normal 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
22
Dockerfile
Normal 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
5
docker-compose.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
services:
|
||||
bot:
|
||||
build: .
|
||||
env_file:
|
||||
- .env
|
||||
28
src/audio/downloader.rs
Normal file
28
src/audio/downloader.rs
Normal 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
1
src/audio/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod downloader;
|
||||
1
src/commands/mod.rs
Normal file
1
src/commands/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod play;
|
||||
46
src/commands/play.rs
Normal file
46
src/commands/play.rs
Normal 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
92
src/main.rs
Normal 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:?}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user