Skip to main content

chat_system/
messenger.rs

1//! The [`Messenger`] trait and [`MessengerManager`].
2
3use crate::message::{Message, SendOptions};
4use anyhow::Result;
5use async_trait::async_trait;
6use serde::{Deserialize, Serialize};
7
8/// The presence/availability status of a messenger account or bot.
9///
10/// Not every platform supports every variant; unsupported values fall back to
11/// the closest equivalent or are silently ignored via the default no-op
12/// implementation of [`Messenger::set_status`].
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum PresenceStatus {
16    /// Fully available and accepting messages.
17    Online,
18    /// Temporarily away (e.g. idle, away message set).
19    Away,
20    /// Do-not-disturb / busy — notifications may be suppressed.
21    Busy,
22    /// Signed in but appearing as offline to other users.
23    Invisible,
24    /// Fully offline / disconnected.
25    Offline,
26}
27
28/// Structured query for searching messages.
29///
30/// All fields are optional; only the fields provided are used as filters.
31/// Platforms that do not support a particular filter silently ignore it.
32///
33/// The struct is serde-serializable so it can be loaded from config files or
34/// forwarded over APIs.
35#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36pub struct SearchQuery {
37    /// Free-text search string (empty string matches all messages).
38    #[serde(default)]
39    pub text: String,
40    /// Restrict the search to a particular channel or conversation ID.
41    #[serde(default)]
42    pub channel: Option<String>,
43    /// Restrict to messages from a specific sender ID / username.
44    #[serde(default)]
45    pub from: Option<String>,
46    /// Maximum number of results to return.
47    #[serde(default)]
48    pub limit: Option<usize>,
49    /// Return only messages sent before this Unix timestamp (exclusive).
50    #[serde(default)]
51    pub before_timestamp: Option<i64>,
52    /// Return only messages sent after this Unix timestamp (exclusive).
53    #[serde(default)]
54    pub after_timestamp: Option<i64>,
55}
56
57/// A unified interface for chat platform clients.
58#[async_trait]
59pub trait Messenger: Send + Sync {
60    fn name(&self) -> &str;
61    fn messenger_type(&self) -> &str;
62    async fn initialize(&mut self) -> Result<()>;
63    async fn send_message(&self, recipient: &str, content: &str) -> Result<String>;
64    async fn send_message_with_options(&self, opts: SendOptions<'_>) -> Result<String> {
65        self.send_message(opts.recipient, opts.content).await
66    }
67    async fn receive_messages(&self) -> Result<Vec<Message>>;
68    fn is_connected(&self) -> bool;
69    async fn disconnect(&mut self) -> Result<()>;
70    async fn set_typing(&self, _channel: &str, _typing: bool) -> Result<()> {
71        Ok(())
72    }
73    /// Set the bot's own presence/availability status.
74    ///
75    /// Platforms that do not support a particular [`PresenceStatus`] value, or
76    /// that have no presence API at all, may ignore this call.  The default
77    /// implementation is a no-op so that existing messenger implementations
78    /// are unaffected.
79    async fn set_status(&self, _status: PresenceStatus) -> Result<()> {
80        Ok(())
81    }
82
83    /// Add an emoji reaction to a message.
84    ///
85    /// `message_id` is the platform message ID, `channel` is the channel or
86    /// conversation it belongs to, and `emoji` is the reaction emoji (Unicode
87    /// character or platform shortcode).
88    ///
89    /// Platforms that do not support reactions return `Ok(())` silently via
90    /// this default implementation.
91    async fn add_reaction(&self, _message_id: &str, _channel: &str, _emoji: &str) -> Result<()> {
92        Ok(())
93    }
94
95    /// Remove an emoji reaction from a message.
96    ///
97    /// Has the same signature as [`add_reaction`](Messenger::add_reaction).
98    /// Platforms that do not support reactions return `Ok(())` silently.
99    async fn remove_reaction(&self, _message_id: &str, _channel: &str, _emoji: &str) -> Result<()> {
100        Ok(())
101    }
102
103    /// Retrieve the profile-picture URL for a user.
104    ///
105    /// Returns `Ok(None)` on platforms that do not expose profile pictures or
106    /// when the user has no picture set.
107    async fn get_profile_picture(&self, _user_id: &str) -> Result<Option<String>> {
108        Ok(None)
109    }
110
111    /// Update the bot's own profile picture.
112    ///
113    /// `url` may be an HTTP URL or a `file://` path depending on what the
114    /// platform accepts.  Platforms that do not support this operation silently
115    /// return `Ok(())`.
116    async fn set_profile_picture(&self, _url: &str) -> Result<()> {
117        Ok(())
118    }
119
120    /// Set the bot's text status / custom status message.
121    ///
122    /// This is distinct from [`set_status`](Messenger::set_status), which
123    /// controls the presence indicator (online/away/busy/…).  A text status is
124    /// a short human-readable string displayed next to the user's name on
125    /// platforms that support it (e.g. Slack, Discord).
126    ///
127    /// Platforms that do not support text statuses silently return `Ok(())`.
128    async fn set_text_status(&self, _text: &str) -> Result<()> {
129        Ok(())
130    }
131
132    /// Search for messages matching `query`.
133    ///
134    /// Returns an empty `Vec` on platforms that do not support server-side
135    /// search.  Results are returned in an unspecified order unless the
136    /// platform guarantees one.
137    async fn search_messages(&self, _query: SearchQuery) -> Result<Vec<Message>> {
138        Ok(Vec::new())
139    }
140}
141
142/// Manages multiple [`Messenger`] instances.
143pub struct MessengerManager {
144    messengers: Vec<Box<dyn Messenger>>,
145}
146
147impl MessengerManager {
148    pub fn new() -> Self {
149        Self {
150            messengers: Vec::new(),
151        }
152    }
153
154    #[allow(clippy::should_implement_trait)]
155    pub fn add(mut self, messenger: impl Messenger + 'static) -> Self {
156        self.messengers.push(Box::new(messenger));
157        self
158    }
159
160    pub fn add_boxed(mut self, messenger: Box<dyn Messenger>) -> Self {
161        self.messengers.push(messenger);
162        self
163    }
164
165    pub async fn initialize_all(&mut self) -> Result<()> {
166        for m in &mut self.messengers {
167            m.initialize().await?;
168        }
169        Ok(())
170    }
171
172    pub async fn disconnect_all(&mut self) -> Result<()> {
173        for m in &mut self.messengers {
174            m.disconnect().await?;
175        }
176        Ok(())
177    }
178
179    pub async fn receive_all(&self) -> Result<Vec<Message>> {
180        let mut all = Vec::new();
181        for m in &self.messengers {
182            match m.receive_messages().await {
183                Ok(mut msgs) => all.append(&mut msgs),
184                Err(e) => tracing::warn!(messenger = %m.name(), "receive error: {e}"),
185            }
186        }
187        Ok(all)
188    }
189
190    pub async fn broadcast(
191        &self,
192        recipient: impl AsRef<str>,
193        content: impl AsRef<str>,
194    ) -> Vec<Result<String>> {
195        let mut results = Vec::new();
196        for m in &self.messengers {
197            results.push(m.send_message(recipient.as_ref(), content.as_ref()).await);
198        }
199        results
200    }
201
202    pub fn messengers(&self) -> &[Box<dyn Messenger>] {
203        &self.messengers
204    }
205
206    pub fn get(&self, name: impl AsRef<str>) -> Option<&dyn Messenger> {
207        self.messengers
208            .iter()
209            .find(|m| m.name() == name.as_ref())
210            .map(|b| b.as_ref())
211    }
212}
213
214impl Default for MessengerManager {
215    fn default() -> Self {
216        Self::new()
217    }
218}