summary refs log tree commit diff
diff options
context:
space:
mode:
authorAshelyn Rose <git@ashen.earth>2025-03-01 14:03:56 -0700
committerAshelyn Rose <git@ashen.earth>2025-03-01 14:03:56 -0700
commit89bf5a8b4f85583795b9211eaca485d6fc633389 (patch)
tree18375298e6906f0644b9ed5b4d9c974d8e609cfe
parent99e5f7e3ff51aebc6796d1b7cf852367eb35d8d5 (diff)
Refactor logging
-rw-r--r--src/main.rs4
-rw-r--r--src/system/aggregator.rs34
-rw-r--r--src/system/log.rs57
-rw-r--r--src/system/mod.rs29
-rw-r--r--src/system/plugin.rs8
-rw-r--r--src/system/plugin/autoproxy.rs16
-rw-r--r--src/system/plugin/prefixes.rs16
7 files changed, 129 insertions, 35 deletions
diff --git a/src/main.rs b/src/main.rs
index c795e1c..98d4754 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -63,8 +63,8 @@ fn main() {
         join_handles.push((system_name.clone(), handle));
     }
 
-    // crossterm::execute!(io::stdout(), EnterAlternateScreen).unwrap();
-    // crossterm::execute!(io::stdout(), DisableLineWrap).unwrap();
+    crossterm::execute!(io::stdout(), EnterAlternateScreen).unwrap();
+    crossterm::execute!(io::stdout(), DisableLineWrap).unwrap();
 
     loop {
         // Wait for an event from one of the threads
diff --git a/src/system/aggregator.rs b/src/system/aggregator.rs
index 822c698..552ea8e 100644
--- a/src/system/aggregator.rs
+++ b/src/system/aggregator.rs
@@ -13,26 +13,44 @@ enum IncomingMessage {
     Partial {message: MessageUpdate, timestamp: Timestamp, seen_by: DiscordToken},
 }
 
+pub enum MemberEvent {
+    Message(Message, DiscordToken),
+    GatewayConnect(DiscordToken),
+    GatewayDisconnect(DiscordToken),
+    GatewayError(DiscordToken),
+}
+
 impl MessageAggregator {
-    pub fn start(system: &System) -> Receiver<(Message, DiscordToken)> {
+    pub fn start(system: &System) -> Receiver<MemberEvent> {
         let incoming_channel = channel::<IncomingMessage>(32);
-        let outgoing_channel = channel::<(Message, DiscordToken)>(32);
+        let outgoing_channel = channel::<MemberEvent>(32);
 
         // Start incoming task for each member
         for member in system.members.iter() {
             let followed_user = system.followed_user.clone();
             let sender = incoming_channel.0.clone();
             let member = member.clone();
+            let outgoing_channel = outgoing_channel.0.clone();
 
             tokio::spawn(async move {
                 loop {
                     let next_event = {member.shard.lock().await.next_event().await};
 
                     match next_event {
-                        Err(err) => println!("Error"),
+                        Err(source) => {
+                            if source.is_fatal() {
+                                let _ = outgoing_channel.send(MemberEvent::GatewayDisconnect(member.discord_token.clone())).await;
+                                return
+                            } else {
+                                let _ = outgoing_channel.send(MemberEvent::GatewayError(member.discord_token.clone())).await;
+                            }
+                        },
+                        Ok(twilight_gateway::Event::Ready(ready)) => {
+                            let _ = outgoing_channel.send(MemberEvent::GatewayConnect(member.discord_token.clone())).await;
+                        },
                         Ok(twilight_gateway::Event::MessageCreate(message)) => {
                             if message.author.id == followed_user {
-                                sender.send(IncomingMessage::Complete {
+                                let _ = sender.send(IncomingMessage::Complete {
                                     message: message.0.clone(),
                                     timestamp: message.timestamp.clone(),
                                     seen_by: member.discord_token.clone()
@@ -41,7 +59,7 @@ impl MessageAggregator {
                         },
                         Ok(twilight_gateway::Event::MessageUpdate(message)) => {
                             if message.author.is_some() && {message.author.as_ref().unwrap().id == followed_user} {
-                                sender.send(IncomingMessage::Partial {
+                                let _ = sender.send(IncomingMessage::Partial {
                                     message: (*message).clone(),
                                     timestamp: message.edited_timestamp.unwrap_or(message.timestamp.expect("No message timestamp")),
                                     seen_by: member.discord_token.clone()
@@ -66,7 +84,7 @@ impl MessageAggregator {
                     Some(IncomingMessage::Complete { message, timestamp, seen_by }) => {
                         if let None = message_cache.get(&message.id) {
                             message_cache.put(message.id.clone(), (message.clone(), timestamp, seen_by.clone()));
-                            sender.send((message, seen_by)).await;
+                            sender.send(MemberEvent::Message(message, seen_by)).await;
                         }
                         
                     },
@@ -76,13 +94,13 @@ impl MessageAggregator {
                                 let mut updated_message = previous_message.clone();
                                 updated_message.content = message.content.unwrap_or(updated_message.content);
                                 message_cache.put(message.id.clone(), (updated_message.clone(), timestamp, seen_by.clone()));
-                                sender.send((updated_message, seen_by)).await;
+                                sender.send(MemberEvent::Message(updated_message, seen_by)).await;
                             }
                         } else {
                             let client = system.members.iter().find(|m| m.discord_token == seen_by).map(|m| m.client.clone()).expect("Could not find client");
                             if let Ok(updated_message) = client.lock().await.message(message.channel_id, message.id).await.unwrap().model().await.map(|r|r.clone()) {
                                 message_cache.put(message.id.clone(), (updated_message.clone(), timestamp, seen_by.clone()));
-                                sender.send((updated_message, seen_by)).await;
+                                sender.send(MemberEvent::Message(updated_message, seen_by)).await;
                             };
                         }
 
diff --git a/src/system/log.rs b/src/system/log.rs
new file mode 100644
index 0000000..c80963f
--- /dev/null
+++ b/src/system/log.rs
@@ -0,0 +1,57 @@
+use crate::SystemUiEvent;
+use std::{collections::HashMap, sync::{mpsc::Sender, Arc}};
+
+#[derive(Clone)]
+pub struct Logger {
+    system_name: String,
+    ui_sender : Sender<(String, SystemUiEvent)>,
+    name_lookup: Arc<HashMap<String, String>>,
+}
+
+impl Logger {
+    pub fn new(system_name: String, system_config: crate::config::System, ui_sender: Sender<(String, SystemUiEvent)>) -> Self {
+        Self {
+            system_name,
+            ui_sender,
+            name_lookup: Arc::new(system_config.members.iter().map(|member| (member.discord_token.clone(), member.name.clone())).collect::<HashMap<_,_>>()),
+        }
+    }
+
+    pub async fn log_connect(&self, member_token: String) {
+        let member_name = self.name_lookup.get(&member_token).expect("Member not found").clone();
+        let _ = self.ui_sender.send((self.system_name.clone(), SystemUiEvent::GatewayConnect(member_name)));
+
+    }
+
+    pub async fn log_disconnect(&self, member_token: String) {
+        let member_name = self.name_lookup.get(&member_token).expect("Member not found").clone();
+        let _ = self.ui_sender.send((self.system_name.clone(), SystemUiEvent::GatewayDisconnect(member_name)));
+    }
+
+    pub async fn log_err(&self, member_token: Option<String>, message: String) {
+        let member = member_token.map(|token| self.name_lookup.get(&token).cloned()).flatten();
+
+        let _ = self.ui_sender.send((
+            self.system_name.clone(),
+            SystemUiEvent::LogLine(if let Some(member_name) = member {
+                format!("{member_name}: Error: {message}")
+            } else {
+                format!("{message}")
+            })
+        ));
+    }
+
+    pub async fn log_line(&self, member_token: Option<String>, message: String) {
+        let member = member_token.map(|token| self.name_lookup.get(&token).cloned()).flatten();
+
+        let _ = self.ui_sender.send((
+            self.system_name.clone(),
+            SystemUiEvent::LogLine(if let Some(member_name) = member {
+                format!("{member_name}: {message}")
+            } else {
+                format!("{message}")
+            })
+        ));
+    }
+}
+
diff --git a/src/system/mod.rs b/src/system/mod.rs
index f5327e3..8e642ea 100644
--- a/src/system/mod.rs
+++ b/src/system/mod.rs
@@ -2,10 +2,12 @@ mod aggregator;
 mod types;
 mod plugin;
 mod util;
+mod log;
 
+use aggregator::MemberEvent;
+use log::Logger;
 use twilight_gateway::{Intents, Shard, ShardId};
 use twilight_http::Client;
-use twilight_model::channel::Message;
 pub use types::SystemThreadCommand;
 use crate::SystemUiEvent;
 use std::{num::NonZeroU64, sync::Arc};
@@ -35,6 +37,7 @@ impl Manager {
         };
 
         let mut message_receiver = aggregator::MessageAggregator::start(&system);
+        let logger = Logger::new(system_name.clone(), system_config.clone(), ui_sender.clone());
 
         let mut plugins : Vec<Box<dyn SeancePlugin>> = vec![
             Box::new(plugin::ProxyPrefixes),
@@ -44,15 +47,24 @@ impl Manager {
         loop {
             match message_receiver.recv().await {
                 None => (),
-                Some((message, seen_by)) => {
-                    println!("Checking message: {}", message.content.clone());
+                Some(MemberEvent::GatewayConnect(member_token)) => {
+                    logger.log_connect(member_token).await;
+                },
+                Some(MemberEvent::GatewayError(member_token)) => {
+                    logger.log_err(Some(member_token), "Non-fatal gateway error".to_string()).await;
+                },
+                Some(MemberEvent::GatewayDisconnect(member_token)) => {
+                    logger.log_disconnect(member_token).await;
+                },
+                Some(MemberEvent::Message(message, seen_by)) => {
                     if message.content.starts_with(&system.command_prefix) {
+                        logger.log_line(None, format!("Handling command: {}", message.content)).await;
                         for plugin in &mut plugins {
-                            match plugin.handle_command(&system, &message).await {
+                            match plugin.handle_command(&logger, &system, &message).await {
                                 plugin::CommandOutcome::Skipped => continue,
                                 plugin::CommandOutcome::Handled => break,
                                 plugin::CommandOutcome::Errored {message} => {
-                                    println!("Error: {message}");
+                                    logger.log_err(None, message).await;
                                     break
                                 },
                             }
@@ -60,13 +72,12 @@ impl Manager {
                     } else {
                         let mut message_response = Response::Noop { delete_source: false };
                         for plugin in &plugins {
-                            plugin.handle_message(&system, &message, &mut message_response).await;
+                            plugin.handle_message(&logger, &system, &message, &mut message_response).await;
                         }
 
                         match message_response.clone() {
                             Response::Noop { delete_source } => {
                                 if delete_source {
-                                    println!("Deleting source message");
                                     let client = system.members.iter().find(|m| m.discord_token == seen_by).map(|m| m.client.clone())
                                         .expect("No such client");
 
@@ -77,12 +88,12 @@ impl Manager {
                             Response::Proxy { member, content } => {
                                 if let Ok(new_message) = util::duplicate_message(&member.client, &message, content.as_str()).await {
                                     if let Err(err) = {member.client.lock().await.delete_message(message.channel_id, message.id).await.map(|_| ()).map_err(|err| err.to_string()).clone() } {
-                                        println!("Error proxying message: {err}");
+                                        logger.log_err(Some(member.discord_token), format!("Could not proxy message: {err}")).await;
                                         {let _ = member.client.lock().await.delete_message(new_message.channel_id, new_message.id).await;}
                                     }
 
                                     for plugin in &plugins {
-                                        plugin.post_response(&system, &new_message, message.channel_id, &message_response).await;
+                                        plugin.post_response(&logger, &system, &new_message, message.channel_id, &message_response).await;
                                     }
                                 }
                             },
diff --git a/src/system/plugin.rs b/src/system/plugin.rs
index c458fd4..d3c6764 100644
--- a/src/system/plugin.rs
+++ b/src/system/plugin.rs
@@ -4,6 +4,8 @@ mod prefixes;
 use async_trait::async_trait;
 
 use twilight_model::{channel::{Channel, Message}, id::{marker::ChannelMarker, Id}};
+
+use super::log::Logger;
 use crate::system::types::{System, Response};
 
 pub use prefixes::ProxyPrefixes;
@@ -11,11 +13,11 @@ pub use autoproxy::Autoproxy;
 
 #[async_trait]
 pub trait SeancePlugin {
-    async fn handle_command(&self, system: &System, message: &Message) -> CommandOutcome;
+    async fn handle_command(&self, logger: &Logger, system: &System, message: &Message) -> CommandOutcome;
 
-    async fn handle_message(&self, system: &System, message: &Message, response: &mut Response);
+    async fn handle_message(&self, logger: &Logger, system: &System, message: &Message, response: &mut Response);
 
-    async fn post_response(&self, system: &System, message: &Message, channel: Id<ChannelMarker>, response: &Response);
+    async fn post_response(&self, logger: &Logger, system: &System, message: &Message, channel: Id<ChannelMarker>, response: &Response);
 }
 
 pub enum CommandOutcome {
diff --git a/src/system/plugin/autoproxy.rs b/src/system/plugin/autoproxy.rs
index cd24e0b..bba25a5 100644
--- a/src/system/plugin/autoproxy.rs
+++ b/src/system/plugin/autoproxy.rs
@@ -6,6 +6,7 @@ use crate::system::types::{System, Member, Response};
 use super::{CommandOutcome, SeancePlugin};
 use tokio::time::sleep;
 use std::time::Duration;
+use super::Logger;
 
 pub struct Autoproxy {
     current_state: Arc<Mutex<InnerState>>,
@@ -29,7 +30,7 @@ impl Autoproxy {
 
 #[async_trait]
 impl SeancePlugin for Autoproxy {
-    async fn handle_command(&self, system: &System, message: &Message) -> CommandOutcome {
+    async fn handle_command(&self, logger: &Logger, system: &System, message: &Message) -> CommandOutcome {
         if message.content.starts_with(format!("{}auto ", system.command_prefix).as_str()) {
             let args = message.content.replace(format!("{}auto ", system.command_prefix).as_str(), "");
             let mut remainder = args.split_whitespace();
@@ -51,7 +52,7 @@ impl SeancePlugin for Autoproxy {
         }
     }
 
-    async fn handle_message(&self, system: &System, message: &Message, response: &mut Response) {
+    async fn handle_message(&self, logger: &Logger, system: &System, message: &Message, response: &mut Response) {
         if message.content.starts_with("\\") {
             if message.content.starts_with("\\\\") {
                 if let InnerState::LatchActive {current_member: _, last_message: _} = {self.current_state.lock().await.clone()} {
@@ -69,12 +70,14 @@ impl SeancePlugin for Autoproxy {
                 InnerState::Off => return,
                 InnerState::LatchInactive => return,
                 InnerState::Member { current_member } => {
+                    logger.log_line(Some(current_member.discord_token.clone()), "Proxying via member mode autoproxy".to_string()).await;
                     *response = Response::Proxy {
                         member: current_member.clone(),
                         content: message.content.clone(),
                     }
                 },
                 InnerState::LatchActive { current_member, last_message: _ } => {
+                    logger.log_line(Some(current_member.discord_token.clone()), "Proxying via autoproxy latch".to_string()).await;
                     *response = Response::Proxy {
                         member: current_member.clone(),
                         content: message.content.clone(),
@@ -85,7 +88,7 @@ impl SeancePlugin for Autoproxy {
         }
     }
 
-    async fn post_response(&self, system: &System, message: &Message, channel: Id<ChannelMarker>, response: &Response) {
+    async fn post_response(&self, logger: &Logger, system: &System, message: &Message, channel: Id<ChannelMarker>, response: &Response) {
         match response {
             Response::Noop { delete_source } => return,
             Response::Proxy { member, content } => {
@@ -94,6 +97,7 @@ impl SeancePlugin for Autoproxy {
                     InnerState::Off => return,
                     InnerState::Member { current_member } => return,
                     InnerState::LatchInactive => {
+                        logger.log_line(Some(member.discord_token.clone()), "Setting autoproxy latch".to_string()).await;
                         {*self.current_state.lock().await = InnerState::LatchActive {
                            current_member: member.clone(),
                            last_message: message.timestamp,
@@ -115,6 +119,7 @@ impl SeancePlugin for Autoproxy {
                         });
                     },
                     InnerState::LatchActive { current_member: _, last_message: _ } => {
+                        logger.log_line(Some(member.discord_token.clone()), "Setting autoproxy latch".to_string()).await;
                         {*self.current_state.lock().await = InnerState::LatchActive {
                             current_member: member.clone(),
                             last_message: message.timestamp,
@@ -123,14 +128,19 @@ impl SeancePlugin for Autoproxy {
                         let state_arc = self.current_state.clone();
                         let sent_member = member.clone();
                         let sent_timestamp = message.timestamp.clone();
+                        let logger = logger.clone();
 
                         tokio::spawn(async move {
                             sleep(Duration::from_secs(15 * 60)).await;
                             let current_state = {state_arc.lock().await.clone()};
+                            logger.log_line(Some(sent_member.discord_token.clone()), "Latch timeout".to_string()).await;
 
                             if let InnerState::LatchActive { current_member, last_message } = current_state {
                                 if sent_member.discord_token == current_member.discord_token && sent_timestamp.as_micros() == last_message.as_micros() {
+                                    logger.log_line(Some(sent_member.discord_token.clone()), "Latch expired".to_string()).await;
                                     {*state_arc.lock().await = InnerState::LatchInactive};
+                                } else {
+                                    logger.log_line(Some(sent_member.discord_token.clone()), "Timeout no longer valid".to_string()).await;
                                 }
                             }
                         });
diff --git a/src/system/plugin/prefixes.rs b/src/system/plugin/prefixes.rs
index a513573..609dafd 100644
--- a/src/system/plugin/prefixes.rs
+++ b/src/system/plugin/prefixes.rs
@@ -1,6 +1,6 @@
 use async_trait::async_trait;
 use twilight_model::id::{marker::ChannelMarker, Id};
-use crate::system::types::Response;
+use crate::system::{log::Logger, types::Response};
 
 use super::{CommandOutcome, SeancePlugin};
 
@@ -8,23 +8,19 @@ pub struct ProxyPrefixes;
 
 #[async_trait]
 impl SeancePlugin for ProxyPrefixes {
-    async fn handle_command(&self, _system: &crate::system::types::System, _message: &twilight_model::channel::Message) -> CommandOutcome {
+    async fn handle_command(&self, _logger: &Logger, _system: &crate::system::types::System, _message: &twilight_model::channel::Message) -> CommandOutcome {
         CommandOutcome::Skipped
     }
 
-    async fn handle_message(&self, system: &crate::system::types::System, message: &twilight_model::channel::Message, response: &mut crate::system::types::Response) {
+    async fn handle_message(&self, logger: &Logger, system: &crate::system::types::System, message: &twilight_model::channel::Message, response: &mut crate::system::types::Response) {
         if let Response::Noop { delete_source: _ } = response {
             for member in &system.members {
-                println!("Checking member prefix: {:?}", member.message_pattern);
                 match member.message_pattern.captures(message.content.as_str()) {
-                    None => {
-                        println!("Nope");
-                        continue;
-                    },
+                    None => continue,
                     Some(captures) => match captures.name("content") {
                         None => continue,
                         Some(matched_content) => {
-                            println!("Matched member prefix: {:?}", member.message_pattern);
+                            logger.log_line(Some(member.discord_token.clone()), "Matched prefix".to_string()).await;
                             *response = Response::Proxy { member: member.clone(), content: matched_content.as_str().to_string() };
                             return
                         },
@@ -34,7 +30,7 @@ impl SeancePlugin for ProxyPrefixes {
         }
     }
 
-    async fn post_response(&self, _system: &crate::system::types::System, _message: &twilight_model::channel::Message, _channel: Id<ChannelMarker>, _response: &crate::system::types::Response) {
+    async fn post_response(&self, _logger: &Logger, _system: &crate::system::types::System, _message: &twilight_model::channel::Message, _channel: Id<ChannelMarker>, _response: &crate::system::types::Response) {
         return
     }
 }