Files
RhythmWorks/src/main.rs
T
brotoskyj 6169e6065d Added Telemetry
closes #8
2026-01-15 14:32:28 -05:00

310 lines
12 KiB
Rust

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<ChannelId> {
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:?}");
}
}