Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d7af18dc4 | |||
| 4924b7e505 | |||
| d9a7bb7fed | |||
| 8fefbddffd | |||
| 7dea731cbf | |||
| ff9a46c05c | |||
| 6169e6065d | |||
| 0ae38b0e01 | |||
| b2059a76c6 |
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
cliff.toml
|
||||||
Generated
+1273
-9
File diff suppressed because it is too large
Load Diff
+16
-1
@@ -1,9 +1,19 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "RhythmWorks"
|
name = "RhythmWorks"
|
||||||
version = "0.2.0"
|
version = "0.6.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
color-eyre = "0.6.5"
|
||||||
|
crossterm = "0.29.0"
|
||||||
|
dialoguer = "0.12.0"
|
||||||
|
keyring = { version = "3.6.3", features = ["linux-native", "windows-native"] }
|
||||||
|
opentelemetry = { version = "0.27.0", features = ["logs", "metrics", "trace"] }
|
||||||
|
opentelemetry-otlp = { version = "0.27.0", features = ["trace", "metrics", "grpc-tonic", "http-proto", "tls", "reqwest-client", "reqwest-rustls"] }
|
||||||
|
opentelemetry-proto = "0.27.0"
|
||||||
|
opentelemetry-semantic-conventions = "0.27.0"
|
||||||
|
opentelemetry_sdk = { version = "0.27.0", features = ["rt-tokio", "trace"] }
|
||||||
|
ratatui = { version = "0.30.0", features = ["all-widgets"] }
|
||||||
reqwest = { version = "0.12.28", features = ["blocking", "json"] }
|
reqwest = { version = "0.12.28", features = ["blocking", "json"] }
|
||||||
semver = "1.0.27"
|
semver = "1.0.27"
|
||||||
serde = "1.0.228"
|
serde = "1.0.228"
|
||||||
@@ -12,6 +22,11 @@ serenity = { version = "0.12.5", features = ["client", "gateway", "voice"] }
|
|||||||
songbird = { version = "0.5.0", features = ["builtin-queue", "driver", "serenity"] }
|
songbird = { version = "0.5.0", features = ["builtin-queue", "driver", "serenity"] }
|
||||||
symphonia = { version = "0.5.5", features = ["aac", "alac", "isomp4", "mp3"] }
|
symphonia = { version = "0.5.5", features = ["aac", "alac", "isomp4", "mp3"] }
|
||||||
tokio = "1.48.0"
|
tokio = "1.48.0"
|
||||||
|
tokio-util = "0.7.18"
|
||||||
|
tonic = { version = "0.12.3", features = ["tls-roots"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-opentelemetry = "0.32.0"
|
||||||
|
tracing-subscriber = "0.3.20"
|
||||||
yt-dlp = "1.4.7"
|
yt-dlp = "1.4.7"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cargo clean
|
||||||
|
cargo fmt
|
||||||
|
cargo clippy --fix --all-targets --allow-dirty
|
||||||
|
cargo build --release
|
||||||
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
## [0.5.2] - 2026-01-21
|
||||||
+339
-64
@@ -1,25 +1,71 @@
|
|||||||
|
use dialoguer::Select;
|
||||||
|
use dialoguer::theme::ColorfulTheme;
|
||||||
use modules::*;
|
use modules::*;
|
||||||
|
use opentelemetry::KeyValue;
|
||||||
|
use opentelemetry::global;
|
||||||
|
use opentelemetry::trace::Status;
|
||||||
|
use opentelemetry::trace::TraceContextExt;
|
||||||
|
use opentelemetry::trace::Tracer;
|
||||||
use serenity::all::ChannelId;
|
use serenity::all::ChannelId;
|
||||||
use serenity::async_trait;
|
use serenity::async_trait;
|
||||||
use serenity::model::channel::Message;
|
use serenity::model::channel::Message;
|
||||||
use serenity::model::gateway::Ready;
|
use serenity::model::gateway::Ready;
|
||||||
use serenity::prelude::*;
|
use serenity::prelude::*;
|
||||||
|
use songbird::Event;
|
||||||
|
use songbird::EventContext;
|
||||||
use songbird::SerenityInit;
|
use songbird::SerenityInit;
|
||||||
|
use songbird::events::EventHandler as VoiceEventHandler;
|
||||||
use songbird::get;
|
use songbird::get;
|
||||||
use songbird::input::HlsRequest;
|
use songbird::input::HlsRequest;
|
||||||
use songbird::input::Input;
|
use songbird::input::Input;
|
||||||
use songbird::tracks::TrackHandle;
|
use std::env;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::io;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::sync::Arc;
|
use tokio::task::JoinHandle;
|
||||||
pub mod modules;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
type TrackMap = Arc<Mutex<HashMap<u64, TrackHandle>>>;
|
pub mod modules;
|
||||||
struct Handler {
|
enum BotState {
|
||||||
track_map: TrackMap,
|
Stopped,
|
||||||
|
Running,
|
||||||
}
|
}
|
||||||
|
struct TrackTraceHandler {
|
||||||
|
otel_ctx: opentelemetry::Context,
|
||||||
|
guild_id: String,
|
||||||
|
track_url: String,
|
||||||
|
hls_url: String,
|
||||||
|
event_name: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl VoiceEventHandler for TrackTraceHandler {
|
||||||
|
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
||||||
|
let tracer = global::tracer("RhythmWorks");
|
||||||
|
let track_status = if let EventContext::Track(track_list) = ctx {
|
||||||
|
track_list.first().map(|state| format!("{:?}", state.0))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
tracer.in_span(self.event_name, |cx| {
|
||||||
|
cx.span()
|
||||||
|
.set_attribute(KeyValue::new("Guild_ID", self.guild_id.clone()));
|
||||||
|
cx.span()
|
||||||
|
.set_attribute(KeyValue::new("Youtube_URL", self.track_url.clone()));
|
||||||
|
cx.span()
|
||||||
|
.set_attribute(KeyValue::new("Stream_URL", self.hls_url.clone()));
|
||||||
|
cx.span().add_event(
|
||||||
|
self.event_name,
|
||||||
|
vec![KeyValue::new(
|
||||||
|
"track_status",
|
||||||
|
track_status.unwrap().to_string(),
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct Handler;
|
||||||
fn get_user_voice_channel(ctx: &Context, msg: &Message) -> Option<ChannelId> {
|
fn get_user_voice_channel(ctx: &Context, msg: &Message) -> Option<ChannelId> {
|
||||||
let guild_id = msg.guild_id?;
|
let guild_id = msg.guild_id?;
|
||||||
let guild = ctx.cache.guild(guild_id)?;
|
let guild = ctx.cache.guild(guild_id)?;
|
||||||
@@ -38,13 +84,50 @@ impl EventHandler for Handler {
|
|||||||
println!("Error sending message: {why:?}");
|
println!("Error sending message: {why:?}");
|
||||||
}
|
}
|
||||||
if msg.content == "!join" {
|
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 {
|
let guild_id = match msg.guild_id {
|
||||||
Some(g) => g,
|
Some(g) => {
|
||||||
None => return,
|
cx.span().add_event(
|
||||||
};
|
"Received Guild ID",
|
||||||
let channel_id = match get_user_voice_channel(&ctx, &msg) {
|
vec![KeyValue::new("Server ID", g.to_string())],
|
||||||
Some(c) => c,
|
);
|
||||||
|
cx.span().set_status(opentelemetry::trace::Status::Ok);
|
||||||
|
cx.span()
|
||||||
|
.set_attribute(KeyValue::new("Guild_ID", g.to_string()));
|
||||||
|
g
|
||||||
|
}
|
||||||
None => {
|
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
|
||||||
|
)));
|
||||||
let _ = msg
|
let _ = msg
|
||||||
.channel_id
|
.channel_id
|
||||||
.say(&ctx.http, "You must be in a voice channel")
|
.say(&ctx.http, "You must be in a voice channel")
|
||||||
@@ -52,7 +135,6 @@ impl EventHandler for Handler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let manager = get(&ctx).await.expect("Songbird not initialized").clone();
|
let manager = get(&ctx).await.expect("Songbird not initialized").clone();
|
||||||
|
|
||||||
let _ = manager.join(guild_id, channel_id).await;
|
let _ = manager.join(guild_id, channel_id).await;
|
||||||
@@ -61,6 +143,11 @@ impl EventHandler for Handler {
|
|||||||
.channel_id
|
.channel_id
|
||||||
.say(&ctx.http, "Joined your voice channel!")
|
.say(&ctx.http, "Joined your voice channel!")
|
||||||
.await;
|
.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" {
|
if msg.content == "!leave" {
|
||||||
let guild_id = match msg.guild_id {
|
let guild_id = match msg.guild_id {
|
||||||
@@ -77,61 +164,168 @@ impl EventHandler for Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if msg.content.starts_with("!play") {
|
if msg.content.starts_with("!play") {
|
||||||
let mut url = msg
|
let tracer = global::tracer("RhythmWorks");
|
||||||
|
let span = tracer.start("Play Message Received");
|
||||||
|
let mut yt_url = msg
|
||||||
.content
|
.content
|
||||||
.strip_prefix("!play ")
|
.strip_prefix("!play ")
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.trim()
|
.trim()
|
||||||
.to_string();
|
.to_string();
|
||||||
let separator = "&";
|
if let Some(offset) = yt_url.find('&') {
|
||||||
if let Some(offset) = url.find(separator) {
|
yt_url.truncate(offset);
|
||||||
url.truncate(offset);
|
|
||||||
}
|
}
|
||||||
if url.is_empty() {
|
if yt_url.is_empty() {
|
||||||
let _ = msg.reply(&ctx, "Please provide a YouTube URL.").await;
|
let _ = msg.reply(&ctx, "Please provide a YouTube URL.").await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let guild_id = msg.guild_id.unwrap();
|
let cx = opentelemetry::Context::current_with_span(span);
|
||||||
let manager = songbird::get(&ctx)
|
cx.span()
|
||||||
.await
|
.set_attribute(KeyValue::new("Youtube_URL", yt_url.to_string()));
|
||||||
.expect("Songbird Voice client placed in at initialisation.")
|
let guild_id = match msg.guild_id {
|
||||||
.clone();
|
Some(g) => {
|
||||||
let handler_lock = if let Some(handler) = manager.get(guild_id) {
|
cx.span().add_event(
|
||||||
handler
|
"Received Guild ID",
|
||||||
} else {
|
vec![KeyValue::new("Server ID", g.to_string())],
|
||||||
let channel_id = get_user_voice_channel(&ctx, &msg);
|
);
|
||||||
match channel_id {
|
cx.span().set_status(opentelemetry::trace::Status::Ok);
|
||||||
Some(c) => manager.join(guild_id, c).await.unwrap(),
|
cx.span()
|
||||||
|
.set_attribute(KeyValue::new("Guild_ID", g.to_string()));
|
||||||
|
g
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
let _ = msg.reply(&ctx, "Join a voice channel first!").await;
|
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;
|
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
|
||||||
|
)));
|
||||||
|
let _ = msg
|
||||||
|
.channel_id
|
||||||
|
.say(&ctx.http, "You must be in a voice channel")
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let output = Command::new("./yt-dlp")
|
manager.join(guild_id, channel_id).await.unwrap()
|
||||||
.args(["-f", "bestaudio", "-g", &url])
|
};
|
||||||
|
let output = match Command::new("./yt-dlp")
|
||||||
|
.args(["-f", "bestaudio", "-g", "--no-playlist", &yt_url])
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed");
|
{
|
||||||
let url_bytes = output.stdout;
|
Ok(output) => {
|
||||||
let url = String::from_utf8(url_bytes).expect("Failed");
|
cx.span().add_event(
|
||||||
let mut handler = handler_lock.lock().await;
|
"Retreiving Youtube Information",
|
||||||
let input = Input::from(HlsRequest::new(reqwest::Client::new(), url.to_string()));
|
vec![KeyValue::new("Success", yt_url.to_string())],
|
||||||
let track_handle = handler.play_input(input);
|
);
|
||||||
let _ = track_handle.play();
|
output
|
||||||
self.track_map
|
}
|
||||||
.lock()
|
Err(e) => {
|
||||||
.await
|
cx.span().add_event(
|
||||||
.insert(guild_id.into(), track_handle);
|
"Retreiving Youtube Information",
|
||||||
let _ = msg.channel_id.say(&ctx.http, "🎶 Playing...").await;
|
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 telemetry_stream_url = stream_url.clone();
|
||||||
|
let mut call = handler_lock.lock().await;
|
||||||
|
let input = Input::from(HlsRequest::new(reqwest::Client::new(), stream_url));
|
||||||
|
let track_handle = call.enqueue_input(input).await;
|
||||||
|
let otel_ctx = opentelemetry::Context::current();
|
||||||
|
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;
|
||||||
|
let _ = track_handle.add_event(
|
||||||
|
Event::Track(songbird::events::TrackEvent::Play),
|
||||||
|
TrackTraceHandler {
|
||||||
|
otel_ctx: otel_ctx.clone(),
|
||||||
|
guild_id: guild_id.to_string().clone(),
|
||||||
|
track_url: yt_url.clone(),
|
||||||
|
hls_url: telemetry_stream_url.clone(),
|
||||||
|
event_name: "Track Started Playing",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let _ = track_handle.add_event(
|
||||||
|
Event::Track(songbird::events::TrackEvent::End),
|
||||||
|
TrackTraceHandler {
|
||||||
|
otel_ctx: otel_ctx.clone(),
|
||||||
|
guild_id: guild_id.to_string().clone(),
|
||||||
|
track_url: yt_url.clone(),
|
||||||
|
hls_url: telemetry_stream_url.clone(),
|
||||||
|
event_name: "Track Finished",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let _ = track_handle.add_event(
|
||||||
|
Event::Track(songbird::events::TrackEvent::Error),
|
||||||
|
TrackTraceHandler {
|
||||||
|
otel_ctx: otel_ctx.clone(),
|
||||||
|
guild_id: guild_id.to_string().clone(),
|
||||||
|
track_url: yt_url.clone(),
|
||||||
|
hls_url: telemetry_stream_url.clone(),
|
||||||
|
event_name: "Playback had an error",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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" {
|
if msg.content == "!pause" {
|
||||||
let guild_id = match msg.guild_id {
|
let guild_id = match msg.guild_id {
|
||||||
Some(g) => g,
|
Some(g) => g,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
if let Some(handle) = self.track_map.lock().await.get(&guild_id.into()) {
|
let manager = songbird::get(&ctx).await.unwrap();
|
||||||
let _ = handle.pause();
|
if let Some(call) = manager.get(guild_id) {
|
||||||
|
let call = call.lock().await;
|
||||||
|
let _ = call.queue().pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if msg.content == "!resume" {
|
if msg.content == "!resume" {
|
||||||
@@ -139,8 +333,10 @@ impl EventHandler for Handler {
|
|||||||
Some(g) => g,
|
Some(g) => g,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
if let Some(handle) = self.track_map.lock().await.get(&guild_id.into()) {
|
let manager = songbird::get(&ctx).await.unwrap();
|
||||||
let _ = handle.play();
|
if let Some(call) = manager.get(guild_id) {
|
||||||
|
let call = call.lock().await;
|
||||||
|
let _ = call.queue().resume();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if msg.content == "!stop" {
|
if msg.content == "!stop" {
|
||||||
@@ -148,8 +344,10 @@ impl EventHandler for Handler {
|
|||||||
Some(g) => g,
|
Some(g) => g,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
if let Some(handle) = self.track_map.lock().await.get(&guild_id.into()) {
|
let manager = songbird::get(&ctx).await.unwrap();
|
||||||
let _ = handle.stop();
|
if let Some(call) = manager.get(guild_id) {
|
||||||
|
let call = call.lock().await;
|
||||||
|
let _ = call.queue().stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,28 +355,105 @@ impl EventHandler for Handler {
|
|||||||
println!("{} is connected!", ready.user.name);
|
println!("{} is connected!", ready.user.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[tokio::main]
|
async fn run_bot(token: String, shutdown: CancellationToken) {
|
||||||
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.");
|
|
||||||
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
|
let intents = GatewayIntents::GUILD_MESSAGES
|
||||||
| GatewayIntents::GUILDS
|
| GatewayIntents::GUILDS
|
||||||
| GatewayIntents::DIRECT_MESSAGES
|
| GatewayIntents::DIRECT_MESSAGES
|
||||||
| GatewayIntents::MESSAGE_CONTENT
|
| GatewayIntents::MESSAGE_CONTENT
|
||||||
| GatewayIntents::GUILD_VOICE_STATES;
|
| GatewayIntents::GUILD_VOICE_STATES;
|
||||||
let track_map: TrackMap = Arc::new(Mutex::new(HashMap::new()));
|
|
||||||
let mut client = Client::builder(&token, intents)
|
let mut client = Client::builder(&token, intents)
|
||||||
.event_handler(Handler { track_map })
|
.event_handler(Handler)
|
||||||
.register_songbird()
|
.register_songbird()
|
||||||
.await
|
.await
|
||||||
.expect("Err creating client");
|
.expect("Err creating client");
|
||||||
if let Err(why) = client.start().await {
|
|
||||||
|
tokio::select! {
|
||||||
|
result = client.start() => {
|
||||||
|
if let Err(why) = result {
|
||||||
println!("Client error: {why:?}");
|
println!("Client error: {why:?}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
_ = shutdown.cancelled() => {
|
||||||
|
println!("Shutting down bot...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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;
|
||||||
|
let service_name = "RhythmWorks";
|
||||||
|
let username = env::var("USER").or_else(|_| env::var("USERNAME")).unwrap();
|
||||||
|
let mut bot_task: Option<JoinHandle<()>> = None;
|
||||||
|
let mut shutdown_token: Option<CancellationToken> = None;
|
||||||
|
let mut bot_state = BotState::Stopped;
|
||||||
|
loop {
|
||||||
|
let token = credentialmanager::get_token(service_name, &username);
|
||||||
|
let options: Vec<&'static str> = vec![
|
||||||
|
"- Start Bot",
|
||||||
|
"- Stop Bot",
|
||||||
|
"- Change Token",
|
||||||
|
"- Delete Token",
|
||||||
|
"- Exit",
|
||||||
|
];
|
||||||
|
let selection = Select::with_theme(&ColorfulTheme::default())
|
||||||
|
.with_prompt("Choose an Option")
|
||||||
|
.default(0)
|
||||||
|
.items(&options)
|
||||||
|
.interact()
|
||||||
|
.unwrap();
|
||||||
|
match selection {
|
||||||
|
0 => {
|
||||||
|
if matches!(bot_state, BotState::Running) {
|
||||||
|
println!("[x] Bot is already running.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = credentialmanager::get_token(service_name, &username);
|
||||||
|
let shutdown = CancellationToken::new();
|
||||||
|
let shutdown_clone = shutdown.clone();
|
||||||
|
|
||||||
|
let task = tokio::spawn(run_bot(token, shutdown_clone));
|
||||||
|
|
||||||
|
bot_task = Some(task);
|
||||||
|
shutdown_token = Some(shutdown);
|
||||||
|
bot_state = BotState::Running;
|
||||||
|
|
||||||
|
println!("✅ Bot started.");
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
if let Some(token) = shutdown_token.take() {
|
||||||
|
token.cancel();
|
||||||
|
}
|
||||||
|
if let Some(task) = bot_task.take() {
|
||||||
|
let _ = task.await;
|
||||||
|
}
|
||||||
|
println!("Bot stopped.");
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
credentialmanager::change_token(service_name, &username);
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
credentialmanager::remove_token(service_name, &username);
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
if let Some(token) = shutdown_token {
|
||||||
|
token.cancel();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => println!("Invalid Option"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
use dialoguer::{Input, theme::ColorfulTheme};
|
||||||
|
use keyring::Entry;
|
||||||
|
|
||||||
|
pub fn get_token<'a>(service: &str, username: &str) -> String {
|
||||||
|
let entry = Entry::new(service, username).expect("Failed to get keyring entry");
|
||||||
|
match entry.get_password() {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => {
|
||||||
|
let token: String = Input::with_theme(&ColorfulTheme::default())
|
||||||
|
.with_prompt("Enter your bot token")
|
||||||
|
.interact_text()
|
||||||
|
.unwrap();
|
||||||
|
match entry.set_password(&token) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to set token: {}", e);
|
||||||
|
std::process::abort()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn change_token(service: &str, username: &str) {
|
||||||
|
let entry = Entry::new(service, username).expect("Failed to get keyring entry");
|
||||||
|
let new_token: String = Input::with_theme(&ColorfulTheme::default())
|
||||||
|
.with_prompt("New Token")
|
||||||
|
.interact_text()
|
||||||
|
.unwrap();
|
||||||
|
let _ = entry.set_password(&new_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_token(service: &str, username: &str) {
|
||||||
|
let entry = Entry::new(service, username).expect("Failed to get keyring entry");
|
||||||
|
let _ = entry.delete_credential();
|
||||||
|
}
|
||||||
@@ -1 +1,3 @@
|
|||||||
|
pub mod credentialmanager;
|
||||||
|
pub mod telemetry;
|
||||||
pub mod updator;
|
pub mod updator;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use opentelemetry::KeyValue;
|
||||||
|
use opentelemetry_otlp::{WithExportConfig, WithTonicConfig};
|
||||||
|
use opentelemetry_sdk::{Resource, runtime};
|
||||||
|
use tonic::transport::{Channel, ClientTlsConfig};
|
||||||
|
|
||||||
|
pub fn init_telemetry() -> opentelemetry_sdk::trace::TracerProvider {
|
||||||
|
let endpoint = "https://signoz.racooncity.org".to_string();
|
||||||
|
let channel = Channel::from_shared(endpoint.clone())
|
||||||
|
.unwrap()
|
||||||
|
.tls_config(ClientTlsConfig::new().with_native_roots())
|
||||||
|
.unwrap()
|
||||||
|
.connect_lazy();
|
||||||
|
let exporter = opentelemetry_otlp::SpanExporter::builder()
|
||||||
|
.with_tonic()
|
||||||
|
.with_endpoint(endpoint.clone())
|
||||||
|
.with_channel(channel)
|
||||||
|
.build()
|
||||||
|
.expect("Failed to Build Exporter");
|
||||||
|
opentelemetry_sdk::trace::TracerProvider::builder()
|
||||||
|
.with_batch_exporter(exporter, runtime::Tokio)
|
||||||
|
.with_resource(Resource::new(vec![KeyValue::new(
|
||||||
|
"service.name",
|
||||||
|
"RhythmWorks",
|
||||||
|
)]))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
+10
-7
@@ -1,15 +1,22 @@
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
use semver::Version;
|
|
||||||
use reqwest;
|
use reqwest;
|
||||||
|
use semver::Version;
|
||||||
|
use serde::Deserialize;
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct Release {
|
struct Release {
|
||||||
tag_name: String,
|
tag_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update() {
|
pub async fn update() {
|
||||||
let body = reqwest::get("https://git.racooncity.org/api/v1/repos/brotoskyj/RhythmWorks/releases/latest").await.unwrap().text().await.unwrap();
|
let body = reqwest::get(
|
||||||
|
"https://git.racooncity.org/api/v1/repos/brotoskyj/RhythmWorks/releases/latest",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let release_info: Release = serde_json::from_str(&body).unwrap();
|
let release_info: Release = serde_json::from_str(&body).unwrap();
|
||||||
let latest_version = Version::parse(&release_info.tag_name).unwrap();
|
let latest_version = Version::parse(&release_info.tag_name).unwrap();
|
||||||
let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
||||||
@@ -18,9 +25,5 @@ pub async fn update() {
|
|||||||
println!("URL: https://git.racooncity.org/brotoskyj/RhythmWorks/releases");
|
println!("URL: https://git.racooncity.org/brotoskyj/RhythmWorks/releases");
|
||||||
thread::sleep(std::time::Duration::from_secs(10));
|
thread::sleep(std::time::Duration::from_secs(10));
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user