From cd8ed8bc5793985df2c1692072a932ceee517b95 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 8 Jul 2026 02:13:35 +0200 Subject: OPEN THE RUST GATES Signed-off-by: salmonik --- src/main.rs | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/main.rs (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4a89315 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,190 @@ +use std::collections::HashMap; + +use poise::serenity_prelude as serenity; +use songbird::SerenityInit; +use tracing_subscriber::filter::EnvFilter; + +mod commands; +mod config; +mod copyparty; +mod database; +mod error; +mod events; +mod lang; +mod openrouter; +mod tasks; +mod voice; + +use database::DbPool; + +pub struct Data { + pub db_pool: DbPool, + pub config: config::Config, + pub voice_manager: voice::VoiceManager, + pub copyparty: copyparty::CopypartyClient, + pub http_client: reqwest::Client, + pub volume: std::sync::atomic::AtomicU64, + pub triggers: tokio::sync::Mutex, Vec)>>, + /// Autocomplete lookup, keyed per-user to avoid cross-command collisions. + /// + /// Mirrors Python's per-cog `_cached_urls[user_id]` map: when two users + /// search the same filename simultaneously, each invocation must resolve its + /// own stashed `SearchHit`, so the cache is keyed by `(UserId, filename)`. + pub ac_cache: tokio::sync::Mutex>, + /// Active music track per guild (only one music source at a time). + pub music_tracks: tokio::sync::Mutex>, +} + +impl Data { + pub async fn reload_triggers(&self) { + let triggers = database::load_all_triggers_with_keywords_and_responses(&self.db_pool) + .await + .unwrap_or_default(); + *self.triggers.lock().await = triggers; + } + + pub async fn ac_stash( + &self, + user_id: serenity::UserId, + key: String, + hit: copyparty::SearchHit, + ) { + self.ac_cache.lock().await.insert((user_id, key), hit); + } + + pub async fn ac_lookup( + &self, + user_id: serenity::UserId, + key: &str, + ) -> Option { + self.ac_cache + .lock() + .await + .get(&(user_id, key.to_string())) + .cloned() + } +} + +#[allow(dead_code)] +type Error = error::Error; + +#[tokio::main] +async fn main() { + let _ = rustls::crypto::ring::default_provider().install_default(); + + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::builder() + .parse(std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into())) + .unwrap(), + ) + .init(); + + dotenvy::dotenv().ok(); + config::check_env_vars(); + let config = config::Config::from_env(); + + let db_pool = database::init_pool(&config.database_path) + .await + .expect("Failed to initialize database"); + + let http_client = reqwest::Client::new(); + + let copyparty = copyparty::CopypartyClient::new( + config.copyparty_url.clone(), + config.copyparty_url_upload.clone(), + config.copyparty_password.clone(), + ); + + let triggers = database::load_all_triggers_with_keywords_and_responses(&db_pool) + .await + .unwrap_or_default(); + + let commands_list = commands::all_commands(); + let config_for_client = config.clone(); + + let framework = poise::Framework::builder() + .options(poise::FrameworkOptions { + commands: commands_list, + event_handler: |ctx, event, _framework, user_data| { + Box::pin(async move { + handle_event(ctx, event, user_data).await; + Ok(()) + }) + }, + ..Default::default() + }) + .setup(move |ctx, _ready, framework| { + let config = config.clone(); + let db_pool = db_pool.clone(); + let http_client = http_client.clone(); + let copyparty = copyparty.clone(); + let triggers = triggers.clone(); + Box::pin(async move { + let vm = voice::VoiceManager::new(ctx).await; + + let data = Data { + config, + db_pool, + voice_manager: vm, + copyparty, + http_client, + volume: std::sync::atomic::AtomicU64::new(1.0f64.to_bits()), + triggers: tokio::sync::Mutex::new(triggers), + ac_cache: tokio::sync::Mutex::new(HashMap::new()), + music_tracks: tokio::sync::Mutex::new(HashMap::new()), + }; + + // Spawn cron scheduler loop. + { + let cron_ctx = crate::tasks::CronContext { + pool: data.db_pool.clone(), + http: ctx.http.clone(), + voice_manager: data.voice_manager.clone(), + copyparty: data.copyparty.clone(), + }; + tokio::spawn(async move { + crate::tasks::cron_scheduler_loop(cron_ctx).await; + }); + } + + poise::builtins::register_globally(ctx, &framework.options().commands).await?; + + events::handle_ready(ctx).await.map_err(|e| { + tracing::error!("on_ready handler: {e}"); + e + })?; + + Ok(data) + }) + }) + .build(); + + let mut client = + serenity::ClientBuilder::new(&config_for_client.token, serenity::GatewayIntents::all()) + .framework(framework) + .register_songbird() + .await + .expect("Error creating client"); + + client.start().await.expect("Error starting client"); +} + +async fn handle_event(ctx: &serenity::Context, event: &serenity::FullEvent, user_data: &Data) { + match event { + serenity::FullEvent::Ready { + data_about_bot: _, .. + } => {} + serenity::FullEvent::VoiceStateUpdate { old, new } => { + events::handle_voice_state_update(ctx, user_data, old.clone(), new) + .await + .unwrap_or_else(|e| tracing::error!("{}: {e}", lang::L_EV_VOICE_STATE_ERROR)); + } + serenity::FullEvent::Message { new_message } => { + events::handle_message(ctx, user_data, new_message) + .await + .unwrap_or_else(|e| tracing::error!("{}: {e}", lang::L_EV_MESSAGE_HANDLER_ERROR)); + } + _ => {} + } +} -- cgit v1.2.3