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}