use modules::*; use opentelemetry::KeyValue; use opentelemetry::global; use opentelemetry::global::ObjectSafeSpan; use opentelemetry::trace::SpanKind; use opentelemetry::trace::Status; use opentelemetry::trace::TraceContextExt; use opentelemetry::trace::Tracer; use serenity::all::ChannelId; use serenity::async_trait; use serenity::futures::channel::oneshot::channel; use serenity::model::channel::Message; use serenity::model::gateway::Ready; use serenity::prelude::*; use songbird::SerenityInit; use songbird::get; use songbird::input::HlsRequest; use songbird::input::Input; use std::io; use std::process::Command; use std::process::Stdio; pub mod modules; struct Handler; fn get_user_voice_channel(ctx: &Context, msg: &Message) -> Option { let guild_id = msg.guild_id?; let guild = ctx.cache.guild(guild_id)?; guild .voice_states .get(&msg.author.id) .and_then(|vs| vs.channel_id) } #[async_trait] impl EventHandler for Handler { async fn message(&self, ctx: Context, msg: Message) { if msg.content == "!ping" && let Err(why) = msg.channel_id.say(&ctx.http, "Pong!").await { println!("Error sending message: {why:?}"); } if msg.content == "!join" { let tracer = global::tracer("RhythmWorks"); let span = tracer.start("Join Message Received"); let cx = opentelemetry::Context::current_with_span(span); let guild_id = match msg.guild_id { Some(g) => { cx.span().add_event( "Received Guild ID", vec![KeyValue::new("Server ID", g.to_string())], ); cx.span().set_status(opentelemetry::trace::Status::Ok); cx.span() .set_attribute(KeyValue::new("Guild_ID", g.to_string())); g } None => { cx.span().add_event( "Failed to Retreive Guild ID", vec![KeyValue::new("Server ID", "None")], ); cx.span() .set_status(opentelemetry::trace::Status::error("Failed")); return; } }; let channel_id = match get_user_voice_channel(&ctx, &msg) { Some(c) => { cx.span().add_event( "Received Channel ID", vec![KeyValue::new("Channel ID", c.to_string())], ); cx.span().set_status(Status::Ok); c } None => { cx.span().add_event( "Failed to Retreive Channel ID", vec![KeyValue::new("Channel ID", "None")], ); cx.span() .set_status(opentelemetry::trace::Status::error(format!( "{} not in Voice Channel", msg.author.to_string() ))); let _ = msg .channel_id .say(&ctx.http, "You must be in a voice channel") .await; return; } }; let manager = get(&ctx).await.expect("Songbird not initialized").clone(); let _ = manager.join(guild_id, channel_id).await; let _ = msg .channel_id .say(&ctx.http, "Joined your voice channel!") .await; cx.span().add_event( "Joined Voice Channel", vec![KeyValue::new("Voice Channel", msg.channel_id.to_string())], ); cx.span().set_status(Status::Ok); } if msg.content == "!leave" { let guild_id = match msg.guild_id { Some(g) => g, None => return, }; let manager = songbird::get(&ctx).await.unwrap(); if manager.get(guild_id).is_some() { let _ = manager.remove(guild_id).await; let _ = msg .channel_id .say(&ctx.http, "Left the voice channel") .await; } } if msg.content.starts_with("!play") { let tracer = global::tracer("RhythmWorks"); let span = tracer.start("Play Message Received"); let mut yt_url = msg .content .strip_prefix("!play ") .unwrap_or("") .trim() .to_string(); if let Some(offset) = yt_url.find('&') { yt_url.truncate(offset); } if yt_url.is_empty() { let _ = msg.reply(&ctx, "Please provide a YouTube URL.").await; return; } let cx = opentelemetry::Context::current_with_span(span); cx.span() .set_attribute(KeyValue::new("Youtube_URL", yt_url.to_string())); let guild_id = match msg.guild_id { Some(g) => { cx.span().add_event( "Received Guild ID", vec![KeyValue::new("Server ID", g.to_string())], ); cx.span().set_status(opentelemetry::trace::Status::Ok); cx.span() .set_attribute(KeyValue::new("Guild_ID", g.to_string())); g } None => { cx.span().add_event( "Failed to Retreive Guild ID", vec![KeyValue::new("Server ID", "None")], ); cx.span() .set_status(opentelemetry::trace::Status::error("Failed")); return; } }; let manager = songbird::get(&ctx).await.unwrap().clone(); let handler_lock = if let Some(call) = manager.get(guild_id) { call } else { let channel_id = match get_user_voice_channel(&ctx, &msg) { Some(c) => c, None => { cx.span().add_event( "Failed to Retreive Channel ID", vec![KeyValue::new("Channel ID", "None")], ); cx.span() .set_status(opentelemetry::trace::Status::error(format!( "{} not in Voice Channel", msg.author.to_string() ))); let _ = msg .channel_id .say(&ctx.http, "You must be in a voice channel") .await; return; } }; manager.join(guild_id, channel_id).await.unwrap() }; let output = match Command::new("./yt-dlp") .args(["-f", "bestaudio", "-g", &yt_url]) .stdout(Stdio::piped()) .output() { Ok(output) => { cx.span().add_event( "Retreiving Youtube Information", vec![KeyValue::new("Success", yt_url.to_string())], ); output } Err(e) => { cx.span().add_event( "Retreiving Youtube Information", vec![KeyValue::new("Failed", e.to_string())], ); cx.span().set_status(Status::error("Failed")); eprintln!("{}", e); return; } }; let stream_url = String::from_utf8(output.stdout) .expect("Invalid UTF-8") .trim() .to_string(); let mut call = handler_lock.lock().await; let input = Input::from(HlsRequest::new(reqwest::Client::new(), stream_url)); let _ = call.enqueue_input(input).await; let position = call.queue().len(); let response = if position == 1 { cx.span().add_event("Starting Playback", vec![KeyValue::new("Status", "Playing".to_string())]); "🎶 Now playing!".to_string() } else { cx.span().add_event("Starting Playback", vec![KeyValue::new("Status", "Added to Queue".to_string())]); format!("🎶 Added Song to Queue: #{}", position) }; let _ = msg.channel_id.say(&ctx.http, response).await; } if msg.content == "!skip" { let guild_id = match msg.guild_id { Some(g) => g, None => return, }; let manager = songbird::get(&ctx).await.unwrap(); if let Some(call) = manager.get(guild_id) { let call = call.lock().await; let _ = call.queue().skip(); let _ = msg.channel_id.say(&ctx.http, "Skipped").await; } } if msg.content == "!pause" { let guild_id = match msg.guild_id { Some(g) => g, None => return, }; let manager = songbird::get(&ctx).await.unwrap(); if let Some(call) = manager.get(guild_id) { let call = call.lock().await; let _ = call.queue().pause(); } } if msg.content == "!resume" { let guild_id = match msg.guild_id { Some(g) => g, None => return, }; let manager = songbird::get(&ctx).await.unwrap(); if let Some(call) = manager.get(guild_id) { let call = call.lock().await; let _ = call.queue().resume(); } } if msg.content == "!stop" { let guild_id = match msg.guild_id { Some(g) => g, None => return, }; let manager = songbird::get(&ctx).await.unwrap(); if let Some(call) = manager.get(guild_id) { let call = call.lock().await; let _ = call.queue().stop(); } } } async fn ready(&self, _: Context, ready: Ready) { println!("{} is connected!", ready.user.name); } } #[tokio::main] async fn main() { println!( "RhythmWorks Copyright (C) 2025 James Brotosky\n This program comes with ABSOLUTELY NO WARRANTY\n This is free software, and you are welcome to redistribute it under certain conditions." ); let tracer_provider = telemetry::init_telemetry(); global::set_tracer_provider(tracer_provider.clone()); let tracer: global::BoxedTracer = global::tracer("tracer"); tracer .in_span("Checking for Updates", |cx| updator::update()) .await; println!("Please paste in your bot token"); let mut input = String::new(); io::stdin() .read_line(&mut input) .expect("Failed to read line"); let token = input.trim(); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::GUILDS | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_VOICE_STATES; let mut client = Client::builder(&token, intents) .event_handler(Handler) .register_songbird() .await .expect("Err creating client"); if let Err(why) = client.start().await { println!("Client error: {why:?}"); } }