diff --git a/Cargo.lock b/Cargo.lock index 225b0b1..47033b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -17,6 +26,56 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -68,7 +127,10 @@ name = "bingus-bot" version = "0.1.0" dependencies = [ "anyhow", + "brotli", + "colog", "fastrand", + "log", "rmp-serde", "rustls", "serde", @@ -87,6 +149,17 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + [[package]] name = "brotli-decompressor" version = "5.0.0" @@ -136,6 +209,32 @@ dependencies = [ "cc", ] +[[package]] +name = "colog" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df62599ba6adc9c6c04a54278c8209125343dc4775f57b9d76c9a4287e58f2bd" +dependencies = [ + "colored", + "env_logger", + "log", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "combine" version = "4.6.7" @@ -197,12 +296,45 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -427,12 +559,42 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jni" version = "0.21.1" @@ -524,6 +686,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -576,6 +744,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -615,6 +798,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "ring" version = "0.17.14" @@ -846,6 +1058,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -951,6 +1173,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -1200,6 +1423,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index ad0e9ae..1a6cb31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,19 @@ edition = "2024" [dependencies] anyhow = "1.0.102" +brotli = "8.0.2" +colog = "1.4.0" fastrand = "2.3.0" +log = "0.4.29" rmp-serde = "1.3.1" rustls = "0.23.37" serde = { version = "1.0.228", features = ["derive"] } -tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.50.0", features = [ + "macros", + "rt-multi-thread", + "fs", + "signal", +] } twilight-cache-inmemory = "0.17.1" twilight-gateway = "0.17.1" twilight-http = "0.17.1" diff --git a/src/brain.rs b/src/brain.rs index c87b8f3..5a7510c 100644 --- a/src/brain.rs +++ b/src/brain.rs @@ -1,6 +1,10 @@ +#![allow(unused)] + use std::collections::HashMap; +use log::debug; use serde::{Deserialize, Serialize}; +use tokio::sync::oneshot; /// Some = Word, None = End Message pub type Token = Option; @@ -12,6 +16,9 @@ pub struct Edges(HashMap, u64); #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct Brain(HashMap); +pub type TypingSender = oneshot::Sender; +pub type TypingReceiver = oneshot::Receiver; + pub fn format_token(tok: &Token) -> String { if let Some(w) = tok { w.clone() @@ -60,6 +67,8 @@ impl Edges { } } +const FORCE_REPLIES: bool = cfg!(test) || (option_env!("BINGUS_FORCE_REPLY").is_some()); + impl Brain { fn normalize_token(word: &str) -> Token { let w = if word.starts_with("http://") || word.starts_with("https://") { @@ -84,10 +93,10 @@ impl Brain { } fn should_reply(rand: &mut fastrand::Rng, is_self: bool) -> bool { - let chance = if is_self { 80 } else { 45 }; + let chance = if is_self { 45 } else { 80 }; let roll = rand.u8(0..=100); - cfg!(test) || roll <= chance + (FORCE_REPLIES && !is_self) || roll <= chance } fn extract_final_token(msg: &str) -> Option { @@ -106,19 +115,22 @@ impl Brain { } } - pub fn ingest(&mut self, msg: &str) { + pub fn ingest(&mut self, msg: &str) -> bool { + let mut learned_new_word = false; // This is a silly way to do windows rust ppl :sob: let _ = Self::parse(msg) .map_windows(|[from, to]| { - eprintln!("{from:?} {to:?}"); if let Some(edge) = self.0.get_mut(from) { edge.increment_token(to); } else { let new = Edges(HashMap::from_iter([(to.clone(), 1)]), 1); self.0.insert(from.clone(), new); + learned_new_word = true; } }) .collect::>(); + + learned_new_word } pub fn merge_from(&mut self, other: Self) { @@ -131,13 +143,19 @@ impl Brain { } } - pub fn respond(&self, msg: &str, is_self: bool) -> Option { + pub fn respond( + &self, + msg: &str, + is_self: bool, + mut typing_oneshot: Option, + ) -> Option { const MAX_TOKENS: usize = 20; let mut rng = fastrand::Rng::new(); // Roll if we should reply if !Self::should_reply(&mut rng, is_self) { + debug!("Failed roll"); return None; } @@ -147,6 +165,7 @@ impl Brain { Self::extract_final_token(msg).or_else(|| self.random_token(&mut rng))?; let mut chain = Vec::with_capacity(MAX_TOKENS); + let mut has_triggered_typing = false; while let Some(tok) = current_token && chain.len() <= MAX_TOKENS @@ -155,6 +174,9 @@ impl Brain { let next = edges.sample(&mut rng).flatten(); if let Some(ref s) = next { chain.push(s.clone()); + if !has_triggered_typing && let Some(typ) = typing_oneshot.take() { + typ.send(true).ok(); + } } current_token = next; } else { @@ -162,9 +184,17 @@ impl Brain { } } + if let Some(typ) = typing_oneshot.take() { + typ.send(false).ok(); + } + Some(chain.join(" ")) } + pub fn word_count(&self) -> usize { + self.0.len() + } + pub fn get_weights(&self, tok: &str) -> Option<&Edges> { self.0.get(&Self::normalize_token(tok)) } @@ -211,7 +241,7 @@ mod tests { hello_edges.0, HashMap::from_iter([(Some("world".to_string()), 1)]) ); - let reply = brain.respond("hello", false); + let reply = brain.respond("hello", false, None); assert_eq!(reply, Some("world".to_string())); } @@ -225,7 +255,7 @@ mod tests { .join(" "); let mut brain = Brain::default(); brain.ingest(&msg); - let reply = brain.respond("a", false); + let reply = brain.respond("a", false, None); let expected = LETTERS .chars() .skip(1) diff --git a/src/main.rs b/src/main.rs index f3c47cb..7ca861a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ #![feature(iter_map_windows)] -#![allow(unused)] mod brain; +mod on_message; +mod status; pub mod prelude { pub use anyhow::Context; @@ -9,50 +10,125 @@ pub mod prelude { pub type Result = StdResult; } -use std::{collections::HashSet, sync::Arc}; +use std::{ + collections::HashSet, + fs::File, + path::{Path, PathBuf}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, +}; +use brotli::enc::BrotliEncoderParams; +use log::{debug, error, info, warn}; use prelude::*; +use tokio::{ + sync::Mutex, + time::{self, Duration}, +}; use twilight_cache_inmemory::{DefaultInMemoryCache, ResourceType}; -use twilight_gateway::{Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt}; +use twilight_gateway::{ + CloseFrame, Event, EventTypeFlags, Intents, MessageSender, Shard, ShardId, StreamExt, +}; use twilight_http::Client as HttpClient; +use twilight_model::id::{Id, marker::UserMarker}; + +use crate::{brain::Brain, on_message::handle_discord_message, status::update_status}; + +pub type BrainHandle = Mutex; #[derive(Debug)] -struct BotContext { +pub struct BotContext { http: HttpClient, - reply_channels: HashSet, + self_id: Id, + brain_file_path: PathBuf, + reply_channels: HashSet, + brain_handle: BrainHandle, + shard_sender: MessageSender, + pending_save: AtomicBool, } -async fn handle_discord_event(event: Event, _ctx: Arc) -> Result { +async fn handle_discord_event(event: Event, ctx: Arc) -> Result { match event { - Event::MessageCreate(msg) => { - let channel_id = msg.channel_id.to_string(); - eprintln!("id: {channel_id}"); - } + Event::MessageCreate(msg) => handle_discord_message(msg, ctx).await?, Event::Ready(ev) => { - eprintln!("Connected to gateway as {}", ev.user.name); + info!("Connected to gateway as {}", ev.user.name); + let brain = ctx.brain_handle.lock().await; + update_status(&*brain, &ctx.shard_sender).context("Failed to update status")?; + } + _ => { + debug!("Ev: {event:?}"); } - _ => {} } Ok(()) } +fn load_brain(path: &Path) -> Result> { + if path.exists() { + let mut file = File::open(path).context("Failed to open brain file")?; + let mut brotli_stream = brotli::Decompressor::new(&mut file, 4096); + rmp_serde::from_read(&mut brotli_stream) + .map(|b| Some(b)) + .context("Failed to decode brain file") + } else { + Ok(None) + } +} + +async fn save_brain(ctx: Arc) -> Result { + let mut file = File::create(&ctx.brain_file_path).context("Failed to open brain file")?; + let params = BrotliEncoderParams::default(); + let mut brotli_writer = brotli::CompressorWriter::with_params(&mut file, 4096, ¶ms); + let brain = ctx.brain_handle.lock().await; + rmp_serde::encode::write(&mut brotli_writer, &*brain) + .context("Failed to write serialized brain")?; + debug!("Saved brain file"); + Ok(()) +} + #[tokio::main] async fn main() -> Result { + let mut clog = colog::default_builder(); + clog.filter( + None, + if cfg!(debug_assertions) { + log::LevelFilter::Debug + } else { + log::LevelFilter::Info + }, + ); + clog.try_init().context("Failed to initialize colog")?; + + info!("Start of bingus-bot {}", env!("CARGO_PKG_VERSION")); + // Config let token_file = std::env::var("TOKEN_FILE").context("Missing TOKEN_FILE env var")?; - let reply_channels: HashSet = HashSet::from_iter( - std::env::var("REPLY_CHANNELS") - .context("Missing REPLY_CHANNELS env var")? - .split(",") - .map(|s| s.trim().to_string()), - ); + let reply_channels: HashSet = std::env::var("REPLY_CHANNELS") + .context("Missing REPLY_CHANNELS env var")? + .split(",") + .map(|s| s.trim().parse::()) + .collect::>() + .context("Invalid channel IDs for REPLY_CHANNELS")?; + let brain_file_path = + PathBuf::from(std::env::var("BRAIN_FILE").unwrap_or_else(|_| "brain.msgpackz".to_string())); let intents = Intents::GUILD_MESSAGES | Intents::MESSAGE_CONTENT; // Read token let token = std::fs::read_to_string(token_file).context("Failed to read bot token")?; let token = token.trim(); + // Read Brain + let brain = if let Some(brain) = load_brain(&brain_file_path)? { + info!("Loading brain from {brain_file_path:?}"); + brain + } else { + info!("Creating new brain file at {brain_file_path:?}"); + Brain::default() + }; + let brain_handle = Mutex::new(brain); + // Init let mut shard = Shard::new(ShardId::ONE, token.to_string(), intents); let http = HttpClient::new(token.to_string()); @@ -65,23 +141,86 @@ async fn main() -> Result { ) .build(); + let self_id = http + .current_user_application() + .await + .context("Failed to get current App")? + .model() + .await + .context("Failed to deserialize")? + .bot + .context("App is not a bot!")? + .id; + let context = Arc::new(BotContext { http, + self_id, reply_channels, + brain_file_path, + brain_handle, + shard_sender: shard.sender(), + pending_save: AtomicBool::new(false), }); - // Event Loop - while let Some(res) = shard.next_event(EventTypeFlags::all()).await { - match res { - Ok(event) => { - cache.update(&event); - tokio::spawn(handle_discord_event(event, Arc::clone(&context))); + info!("Ensuring brain is writable..."); + save_brain(context.clone()) + .await + .context("Brain file is not writable")?; + info!("Brain file saved"); + + let mut interval = time::interval(Duration::from_secs(60)); + interval.tick().await; + tokio::pin!(interval); + + info!("Connecting to gateway..."); + + loop { + tokio::select! { + Ok(()) = tokio::signal::ctrl_c() => { + info!("SIGINT: Closing connection and saving"); + shard.close(CloseFrame::NORMAL); + break; } - Err(why) => { - eprintln!("Failed to receive event: {why:?}"); + _ = interval.tick() => { + debug!("Save Interval"); + if context.pending_save.load(Ordering::Relaxed) { + let ctx = context.clone(); + tokio::spawn(async move { + if let Err(why) = save_brain(ctx.clone()).await { + error!("Failed to save brain file:\n{why:?}"); + } + ctx.pending_save.store(true, Ordering::Relaxed); + }); + } + }, + opt = shard.next_event(EventTypeFlags::all()) => { + match opt { + Some(Ok(event)) => { + cache.update(&event); + let ctx = context.clone(); + tokio::spawn(async move { + if let Err(why) = handle_discord_event(event, ctx).await { + error!("Error while processing Discord event:\n{why:?}"); + } + }); + } + Some(Err(why)) => { + warn!("Failed to receive event:\n{why:?}"); + } + None => { + info!("Disconnected from Discord: Saving brain and exiting"); + break; + } + } } } } + save_brain(context) + .await + .context("Failed to write brain file on exit")?; + + info!("Save Complete, Exiting"); + Ok(()) } diff --git a/src/on_message.rs b/src/on_message.rs new file mode 100644 index 0000000..5032c2f --- /dev/null +++ b/src/on_message.rs @@ -0,0 +1,72 @@ +use std::{ + boxed::Box, + sync::{Arc, atomic::Ordering}, +}; + +use log::warn; +use twilight_model::{ + channel::message::{AllowedMentions, MessageFlags, MessageType}, + gateway::payload::incoming::MessageCreate, +}; + +use crate::{BotContext, prelude::*, status::update_status}; + +pub async fn handle_discord_message(msg: Box, ctx: Arc) -> Result { + let channel_id = msg.channel_id.get(); + let is_self = msg.author.id == ctx.self_id; + let is_normal_message = matches!(msg.kind, MessageType::Regular | MessageType::Reply); + let is_ephemeral = msg + .flags + .is_some_and(|flags| flags.contains(MessageFlags::EPHEMERAL)); + let is_dm = msg.guild_id.is_none(); + + // Should Ingest Message? + if is_self || !is_normal_message || is_ephemeral || is_dm { + return Ok(()); + } + + let mut brain = ctx.brain_handle.lock().await; + let learned_new_word = brain.ingest(&msg.content); + ctx.pending_save.store(true, Ordering::Relaxed); + + if learned_new_word { + update_status(&*brain, &ctx.shard_sender).context("Failed to update status")?; + } + + // Should Reply to Message? + if !ctx.reply_channels.contains(&channel_id) { + return Ok(()); + } + + let (typ_tx, typ_rx) = tokio::sync::oneshot::channel(); + let (done_tx, done_rx) = tokio::sync::oneshot::channel(); + + let ctx_typ = ctx.clone(); + let typ_id = msg.channel_id; + tokio::spawn(async move { + if typ_rx.await.ok().is_some_and(|start| start) { + if let Err(why) = ctx_typ.http.create_typing_trigger(typ_id).await { + warn!("Failed to set typing indicator:\n{why:?}"); + } + } + done_tx.send(()).ok(); + }); + + if let Some(reply_text) = brain + .respond(&msg.content, is_self, Some(typ_tx)) + .filter(|s| !s.trim().is_empty()) + { + drop(brain); + done_rx.await.ok(); + ctx.http + .create_message(msg.channel_id) + .content(&reply_text) + .reply(msg.id) + .fail_if_not_exists(false) + .allowed_mentions(Some(&AllowedMentions::default())) + .await + .context("Failed to send message")?; + } + + Ok(()) +} diff --git a/src/status.rs b/src/status.rs new file mode 100644 index 0000000..e94a138 --- /dev/null +++ b/src/status.rs @@ -0,0 +1,41 @@ +use log::debug; +use twilight_gateway::MessageSender; +use twilight_model::gateway::{ + payload::outgoing::UpdatePresence, + presence::{Activity, ActivityType, Status}, +}; + +use crate::{brain::Brain, prelude::*}; + +pub fn update_status(brain: &Brain, sender: &MessageSender) -> Result { + let words = brain.word_count(); + + let activity = Activity { + application_id: None, + assets: None, + buttons: Vec::new(), + created_at: None, + details: None, + emoji: None, + flags: None, + id: None, + instance: None, + kind: ActivityType::Custom, + name: "Bingus".to_string(), + party: None, + secrets: None, + state: Some(format!("I know {words} words!")), + timestamps: None, + url: None, + }; + + let status = UpdatePresence::new(vec![activity], false, None, Status::Online) + .context("Failed to make status")?; + + sender + .command(&status) + .context("Failed to send to gateway")?; + + debug!("Sent status update"); + Ok(()) +}