use std::sync::Arc; use std::time::Instant; use mlua::{Lua, Result as LuaResult, UserData, UserDataMethods}; use tokio::sync::{mpsc, Mutex}; use sqlx::PgPool; use crate::irc::parser::IrcMessage; #[derive(Clone)] pub struct IrcApi { pub write_tx: mpsc::Sender, } impl UserData for IrcApi { fn add_methods>(methods: &mut M) { methods.add_method("send", |_, this, (target, text): (String, String)| { let msg = IrcMessage::privmsg(&target, &text); let tx = this.write_tx.clone(); tokio::spawn(async move { let _ = tx.send(msg).await; }); Ok(()) }); methods.add_method("join", |_, this, channel: String| { let msg = IrcMessage::join(&channel); let tx = this.write_tx.clone(); tokio::spawn(async move { let _ = tx.send(msg).await; }); Ok(()) }); methods.add_method("part", |_, this, channel: String| { let msg = IrcMessage::part(&channel); let tx = this.write_tx.clone(); tokio::spawn(async move { let _ = tx.send(msg).await; }); Ok(()) }); methods.add_method("nick", |_, this, nick: String| { let msg = IrcMessage::nick(&nick); let tx = this.write_tx.clone(); tokio::spawn(async move { let _ = tx.send(msg).await; }); Ok(()) }); } } #[derive(Clone)] pub struct KvApi { pub db: PgPool, pub bot_id: String, } impl UserData for KvApi { fn add_methods>(methods: &mut M) { methods.add_async_method("get", |_, this, key: String| async move { let result = crate::db::kv_get(&this.db, &this.bot_id, &key).await .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?; Ok(result.map(|kv| kv.value)) }); methods.add_async_method("set", |_, this, (key, value): (String, String)| async move { crate::db::kv_set(&this.db, &this.bot_id, &key, &value).await .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?; Ok(()) }); methods.add_async_method("delete", |_, this, key: String| async move { crate::db::kv_delete(&this.db, &this.bot_id, &key).await .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?; Ok(()) }); methods.add_async_method("list", |lua, this, ()| async move { let items = crate::db::kv_list(&this.db, &this.bot_id).await .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?; let table = lua.create_table()?; for item in items { table.set(item.key, item.value)?; } Ok(table) }); } } #[derive(Clone)] pub struct LogApi { pub db: PgPool, pub bot_id: String, pub log_tx: tokio::sync::broadcast::Sender, } #[derive(Clone, Debug, serde::Serialize)] pub struct LogEntry { pub bot_id: String, pub level: String, pub message: String, pub timestamp: String, } impl UserData for LogApi { fn add_methods>(methods: &mut M) { methods.add_async_method("info", |_, this, msg: String| async move { log_impl(&this, "info", &msg).await }); methods.add_async_method("warn", |_, this, msg: String| async move { log_impl(&this, "warn", &msg).await }); methods.add_async_method("error", |_, this, msg: String| async move { log_impl(&this, "error", &msg).await }); } } async fn log_impl(api: &LogApi, level: &str, message: &str) -> LuaResult<()> { let _ = crate::db::insert_log(&api.db, &api.bot_id, level, message).await; let entry = LogEntry { bot_id: api.bot_id.clone(), level: level.to_string(), message: message.to_string(), timestamp: time::OffsetDateTime::now_utc().to_string(), }; let _ = api.log_tx.send(entry); Ok(()) } #[derive(Clone)] pub struct HttpApi { pub client: reqwest::Client, pub rate_limiter: Arc>, } pub struct HttpRateLimiter { pub requests: Vec, pub max_per_minute: usize, } impl HttpRateLimiter { pub fn new(max_per_minute: usize) -> Self { Self { requests: Vec::new(), max_per_minute, } } pub fn check(&mut self) -> bool { let cutoff = Instant::now() - std::time::Duration::from_secs(60); self.requests.retain(|t| *t > cutoff); if self.requests.len() >= self.max_per_minute { return false; } self.requests.push(Instant::now()); true } } impl UserData for HttpApi { fn add_methods>(methods: &mut M) { methods.add_async_method("get", |_, this, url: String| async move { { let mut limiter = this.rate_limiter.lock().await; if !limiter.check() { return Err(mlua::Error::RuntimeError( "http rate limit exceeded (10 req/min)".to_string(), )); } } let resp = this.client.get(&url) .timeout(std::time::Duration::from_secs(10)) .send() .await .map_err(|e| mlua::Error::RuntimeError(format!("http request failed: {e}")))?; let body = resp.text().await .map_err(|e| mlua::Error::RuntimeError(format!("http read body failed: {e}")))?; Ok(body) }); } } pub struct TimerEvent { pub registry_key: mlua::RegistryKey, pub repeating: bool, pub interval_secs: f64, } #[derive(Clone)] pub struct TimerApi { pub timer_tx: mpsc::Sender, } impl UserData for TimerApi { fn add_methods>(methods: &mut M) { methods.add_method("after", |lua, this, (secs, func): (f64, mlua::Function)| { let key = lua.create_registry_value(func)?; let tx = this.timer_tx.clone(); let millis = (secs * 1000.0) as u64; tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(millis)).await; let _ = tx.send(TimerEvent { registry_key: key, repeating: false, interval_secs: 0.0, }).await; }); Ok(()) }); methods.add_method("every", |lua, this, (secs, func): (f64, mlua::Function)| { let key = lua.create_registry_value(func)?; let tx = this.timer_tx.clone(); tokio::spawn(async move { let _ = tx.send(TimerEvent { registry_key: key, repeating: true, interval_secs: secs, }).await; }); Ok(()) }); } } pub fn register_apis( lua: &Lua, irc_api: IrcApi, kv_api: KvApi, log_api: LogApi, http_api: HttpApi, timer_api: TimerApi, ) -> LuaResult<()> { lua.globals().set("irc", irc_api)?; lua.globals().set("kv", kv_api)?; lua.globals().set("log", log_api)?; lua.globals().set("http", http_api)?; lua.globals().set("timer", timer_api)?; Ok(()) }