23 Commits

Author SHA1 Message Date
mysticmomba 5fa83ba60d fix(codebase): Changed git URL in updator to reflect new git name 2026-03-04 09:24:25 -05:00
brotoskyj 3d7af18dc4 feat(UI/Codebase): Made menu persistent. After starting the bot, the menu will remain on the screen and be interactable for the user while the bot is running 2026-01-20 22:17:28 -05:00
brotoskyj 4924b7e505 Changed Cargo.toml 2026-01-20 21:55:00 -05:00
brotoskyj d9a7bb7fed fix(codebase): Added Proper --no-playlist to command 2026-01-20 21:35:39 -05:00
brotoskyj 8fefbddffd Added --no-playlist to yt-dlp command. This seems to fix the bot randomly skipping songs 2026-01-20 21:30:39 -05:00
brotoskyj 7dea731cbf Added Extra Telemetry for Queue 2026-01-19 14:54:13 -05:00
brotoskyj ff9a46c05c Feature Changes
Added a Token Credential Manager - Closes #4
Added TUI Menu - Closes #2
2026-01-16 10:26:17 -05:00
brotoskyj 6169e6065d Added Telemetry
closes #8
2026-01-15 14:32:28 -05:00
James Brotosky 0ae38b0e01 Merge pull request 'Closes #1' (#10) from feature/queue into master
Reviewed-on: brotoskyj/RhythmWorks#10
2026-01-12 12:37:26 -05:00
brotoskyj b2059a76c6 Closes #1 2026-01-12 12:36:15 -05:00
brotoskyj fcbe163d18 Merge branch 'feature/FixURLS' 2026-01-12 10:18:41 -05:00
brotoskyj 6a3f75af71 Closes #5 2026-01-12 10:16:10 -05:00
brotoskyj a4d3146076 Fixed typo in Readme 2025-12-31 12:36:00 -05:00
brotoskyj eaaba5a053 Updated Readme File 2025-12-31 12:20:54 -05:00
brotoskyj 464d7f6fab Updated Readme File 2025-12-31 12:20:28 -05:00
brotoskyj 547a067e54 Edited Project Roadmap 2025-12-30 15:05:48 -05:00
brotoskyj acce38cd44 Converted issues to project roadmap 2025-12-30 15:03:31 -05:00
brotoskyj 29fefc118b Added issues to roadmap 2025-12-30 15:02:35 -05:00
brotoskyj 8feab199fe Updated Readme for easier setup 2025-12-30 14:57:23 -05:00
brotoskyj 97f4a4374d Updated Readme again 2025-12-29 15:50:07 -05:00
brotoskyj f27376f8eb Updated Readme again 2025-12-29 15:49:53 -05:00
brotoskyj 9b19be2a55 Updated Readme with assets 2025-12-29 15:41:17 -05:00
brotoskyj 1512018a4f Added readme and license 2025-12-29 15:39:45 -05:00
19 changed files with 1836 additions and 96 deletions
+1
View File
@@ -1 +1,2 @@
/target /target
cliff.toml
Generated
+1311 -23
View File
File diff suppressed because it is too large Load Diff
+30 -2
View File
@@ -1,9 +1,21 @@
[package] [package]
name = "discord_music" name = "RhythmWorks"
version = "0.1.0" version = "0.7.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-appender-log = "0.27.0"
opentelemetry-appender-tracing = "0.27.0"
opentelemetry-otlp = { version = "0.27.0", features = ["trace", "metrics", "grpc-tonic", "http-proto", "tls", "reqwest-client", "reqwest-rustls", "logs"] }
opentelemetry-proto = "0.27.0"
opentelemetry-semantic-conventions = "0.27.0"
opentelemetry_sdk = { version = "0.27.0", features = ["logs", "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,4 +24,20 @@ 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"
[profile.release]
opt-level = "z"
lto = "fat"
debug = false
codegen-units = 1
panic = 'abort'
strip = true
debug-assertions = false
overflow-checks = true
+30
View File
@@ -0,0 +1,30 @@
## Welcome to RhythmWorks
RhythmWorks is a lightweight Discord music bot designed for simple, direct audio playback from YouTube links. It focuses on manual control, minimal complexity, and local execution, making it ideal for personal servers or small communities.
Unlike many modern Discord bots, RhythmWorks does not rely on hosted services or third-party dashboards. You run it yourself, connect it to your Discord server, and control playback using straightforward text commands.
### Features
- Play Audio directly from YouTube
- Basic Playback Controls
- Runs locally on your machine
- No external accounts or external bot hosting required
### How It Works
RhythmWorks connects to Discord using a custom bot that you generate through the Discord Developer Portal
### Roadmap
[Project Roadmap](https://git.racooncity.org/brotoskyj/RhythmWorks/projects/4)
### Getting Started
Please follow the [Getting Started](https://git.racooncity.org/brotoskyj/RhythmWorks/wiki/Getting-Started) Documentation for details on how to get RhythmWorks up and running
### Discord Commands
Please review the [Commands](https://git.racooncity.org/brotoskyj/RhythmWorks/wiki/Commands) for usage
### Wiki
[RhythmWorks Wiki](https://git.racooncity.org/brotoskyj/RhythmWorks/wiki/?action=_pages) - Please review this all for all documentation
## License
[AGPLv3](https://choosealicense.com/licenses/agpl-3.0/)
Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+6
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
## [0.5.2] - 2026-01-21
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

+353 -58
View File
@@ -1,25 +1,77 @@
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 tracing::instrument;
use std::collections::HashMap;
use std::env; use std::env;
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;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::Layer;
use tracing_subscriber::filter::Targets;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
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 +90,51 @@ 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, tracing::info!("Received Guild ID");
}; cx.span().add_event(
let channel_id = match get_user_voice_channel(&ctx, &msg) { "Received Guild ID",
Some(c) => c, 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 => { 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 +142,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 +150,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,58 +171,168 @@ impl EventHandler for Handler {
} }
} }
if msg.content.starts_with("!play") { if msg.content.starts_with("!play") {
let 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();
if let Some(offset) = yt_url.find('&') {
if url.is_empty() { yt_url.truncate(offset);
}
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" {
@@ -136,8 +340,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" {
@@ -145,8 +351,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();
} }
} }
} }
@@ -154,25 +362,112 @@ 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;
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
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, logger_provider) = telemetry::init_telemetry();
global::set_tracer_provider(tracer_provider.clone());
let filter = Targets::new()
.with_target("RhythmWorks", LevelFilter::INFO)
.with_default(LevelFilter::OFF);
let otel_log_layer =
opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(&logger_provider)
.with_filter(filter);
tracing_subscriber::registry().with(otel_log_layer).init();
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"),
}
}
} }
+37
View File
@@ -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();
}
View File
+2 -1
View File
@@ -1,2 +1,3 @@
pub mod credentialmanager;
pub mod telemetry;
pub mod updator; pub mod updator;
pub mod downloadyt;
+44
View File
@@ -0,0 +1,44 @@
use opentelemetry::KeyValue;
use opentelemetry::global;
use opentelemetry_otlp::{WithExportConfig, WithTonicConfig};
use opentelemetry_sdk::{Resource, logs, runtime};
use tonic::transport::{Channel, ClientTlsConfig};
pub fn init_telemetry() -> (
opentelemetry_sdk::trace::TracerProvider,
opentelemetry_sdk::logs::LoggerProvider,
) {
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.clone())
.build()
.expect("Failed to Build Exporter");
let log_exporter = opentelemetry_otlp::LogExporter::builder()
.with_tonic()
.with_endpoint(endpoint.clone())
.with_channel(channel.clone())
.build()
.expect("Failed to build log exporter");
let logger_provider = logs::LoggerProvider::builder()
.with_resource(Resource::new(vec![KeyValue::new(
"service.name",
"RhythmWorks",
)]))
.with_batch_exporter(log_exporter, runtime::Tokio)
.build();
let tracer_provider = opentelemetry_sdk::trace::TracerProvider::builder()
.with_batch_exporter(exporter, runtime::Tokio)
.with_resource(Resource::new(vec![KeyValue::new(
"service.name",
"RhythmWorks",
)]))
.build();
(tracer_provider, logger_provider)
}
+16 -7
View File
@@ -1,20 +1,29 @@
use serde::Deserialize; use std::thread;
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/mysticmomba/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();
if latest_version > current_version { if latest_version > current_version {
println!("Update Available") println!("Update Available! Please download the latest version");
} println!("URL: https://git.racooncity.org/mysticmomba/RhythmWorks/releases");
else { thread::sleep(std::time::Duration::from_secs(10));
println!("Already on current version"); std::process::exit(1);
} }
} }