Skip to main content

chat_system/
config.rs

1//! Config-driven generic client and server types.
2//!
3//! [`MessengerConfig`] and [`ServerConfig`] are serde-tagged enums whose `protocol`
4//! field selects the backend.  They can be deserialized directly from TOML, JSON,
5//! or any other serde-compatible format, making them suitable for config files.
6//!
7//! # Client example (TOML)
8//!
9//! ```toml
10//! protocol = "irc"
11//! name     = "my-bot"
12//! server   = "irc.libera.chat"
13//! port     = 6697
14//! nick     = "my-bot"
15//! channels = ["#rust"]
16//! tls      = true
17//! ```
18//!
19//! ```rust,no_run
20//! use chat_system::config::{IrcConfig, MessengerConfig};
21//! use chat_system::{GenericMessenger, Messenger};
22//!
23//! #[tokio::main]
24//! async fn main() -> anyhow::Result<()> {
25//!     let config = MessengerConfig::Irc(IrcConfig {
26//!         name: "bot".into(),
27//!         server: "irc.libera.chat".into(),
28//!         port: 6697,
29//!         nick: "my-bot".into(),
30//!         channels: vec!["#rust".into()],
31//!         tls: true,
32//!     });
33//!     let mut client = GenericMessenger::new(config);
34//!     client.initialize().await?;
35//!     client.send_message("#rust", "Hello!").await?;
36//!     client.disconnect().await?;
37//!     Ok(())
38//! }
39//! ```
40
41use crate::message::{Message, SendOptions};
42use crate::messenger::{Messenger, PresenceStatus, SearchQuery};
43use crate::server::ChatServer;
44use anyhow::Result;
45use async_trait::async_trait;
46use serde::{Deserialize, Serialize};
47
48// ── per-protocol client config structs ────────────────────────────────────────
49
50/// Configuration for an IRC client connection.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct IrcConfig {
53    pub name: String,
54    pub server: String,
55    #[serde(default = "default_irc_port")]
56    pub port: u16,
57    pub nick: String,
58    #[serde(default)]
59    pub channels: Vec<String>,
60    #[serde(default)]
61    pub tls: bool,
62}
63fn default_irc_port() -> u16 {
64    6667
65}
66
67/// Configuration for a Discord bot.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct DiscordConfig {
70    pub name: String,
71    pub token: String,
72}
73
74/// Configuration for a Telegram bot.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct TelegramConfig {
77    pub name: String,
78    pub token: String,
79}
80
81/// Configuration for a Slack bot.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SlackConfig {
84    pub name: String,
85    pub token: String,
86}
87
88/// Configuration for Microsoft Teams.
89///
90/// Use `webhook_url` for legacy incoming-webhook mode, or `token` + `team_id`
91/// + `channel_id` for Microsoft Graph mode with inbound polling support.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct TeamsConfig {
94    pub name: String,
95    #[serde(default)]
96    pub webhook_url: Option<String>,
97    #[serde(default)]
98    pub token: Option<String>,
99    #[serde(default)]
100    pub team_id: Option<String>,
101    #[serde(default)]
102    pub channel_id: Option<String>,
103}
104
105/// Configuration for Google Chat.
106///
107/// Use `webhook_url` for incoming-webhook mode, or `token` + `space_id` for
108/// authenticated Google Chat API mode with inbound polling support.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct GoogleChatConfig {
111    pub name: String,
112    #[serde(default)]
113    pub webhook_url: Option<String>,
114    #[serde(default)]
115    pub token: Option<String>,
116    #[serde(default)]
117    pub space_id: Option<String>,
118}
119
120/// Configuration for the console (stdin/stdout) messenger.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ConsoleConfig {
123    pub name: String,
124}
125
126/// Configuration for an outbound HTTP webhook.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct WebhookConfig {
129    pub name: String,
130    pub url: String,
131}
132
133/// Configuration for the iMessage messenger (macOS only).
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct IMessageConfig {
136    pub name: String,
137}
138
139#[cfg(feature = "matrix")]
140/// Configuration for a Matrix client.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct MatrixConfig {
143    pub name: String,
144    pub homeserver: String,
145    pub username: String,
146    pub password: String,
147}
148
149#[cfg(feature = "signal-cli")]
150/// Configuration for a Signal CLI messenger.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct SignalCliConfig {
153    pub name: String,
154    pub phone_number: String,
155    #[serde(default = "default_signal_cli_path")]
156    pub cli_path: String,
157}
158#[cfg(feature = "signal-cli")]
159fn default_signal_cli_path() -> String {
160    "signal-cli".to_string()
161}
162
163#[cfg(feature = "whatsapp")]
164/// Configuration for a WhatsApp messenger.
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct WhatsAppConfig {
167    pub name: String,
168    /// Path to the SQLite session database (e.g. `"whatsapp.db"`).
169    pub db_path: String,
170}
171
172// ── MessengerConfig ────────────────────────────────────────────────────────────
173
174/// Protocol-selecting messenger configuration.
175///
176/// The `protocol` field (the serde tag) identifies the backend.  Deserializing
177/// from a config file that contains `protocol = "irc"` will produce
178/// `MessengerConfig::Irc(IrcConfig { … })`.
179///
180/// Call [`MessengerConfig::build`] to obtain a concrete [`Messenger`], or wrap it
181/// in a [`GenericMessenger`] which itself implements [`Messenger`].
182#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(tag = "protocol", rename_all = "lowercase")]
184pub enum MessengerConfig {
185    Irc(IrcConfig),
186    Discord(DiscordConfig),
187    Telegram(TelegramConfig),
188    Slack(SlackConfig),
189    Teams(TeamsConfig),
190    #[serde(rename = "googlechat")]
191    GoogleChat(GoogleChatConfig),
192    Console(ConsoleConfig),
193    Webhook(WebhookConfig),
194    #[serde(rename = "imessage")]
195    IMessage(IMessageConfig),
196    #[cfg(feature = "matrix")]
197    Matrix(MatrixConfig),
198    #[cfg(feature = "signal-cli")]
199    #[serde(rename = "signal")]
200    SignalCli(SignalCliConfig),
201    #[cfg(feature = "whatsapp")]
202    WhatsApp(WhatsAppConfig),
203}
204
205impl MessengerConfig {
206    /// The human-readable name for this messenger instance.
207    pub fn name(&self) -> &str {
208        match self {
209            Self::Irc(c) => &c.name,
210            Self::Discord(c) => &c.name,
211            Self::Telegram(c) => &c.name,
212            Self::Slack(c) => &c.name,
213            Self::Teams(c) => &c.name,
214            Self::GoogleChat(c) => &c.name,
215            Self::Console(c) => &c.name,
216            Self::Webhook(c) => &c.name,
217            Self::IMessage(c) => &c.name,
218            #[cfg(feature = "matrix")]
219            Self::Matrix(c) => &c.name,
220            #[cfg(feature = "signal-cli")]
221            Self::SignalCli(c) => &c.name,
222            #[cfg(feature = "whatsapp")]
223            Self::WhatsApp(c) => &c.name,
224        }
225    }
226
227    /// The protocol identifier string (matches the serde tag value).
228    pub fn protocol_name(&self) -> &'static str {
229        match self {
230            Self::Irc(_) => "irc",
231            Self::Discord(_) => "discord",
232            Self::Telegram(_) => "telegram",
233            Self::Slack(_) => "slack",
234            Self::Teams(_) => "teams",
235            Self::GoogleChat(_) => "googlechat",
236            Self::Console(_) => "console",
237            Self::Webhook(_) => "webhook",
238            Self::IMessage(_) => "imessage",
239            #[cfg(feature = "matrix")]
240            Self::Matrix(_) => "matrix",
241            #[cfg(feature = "signal-cli")]
242            Self::SignalCli(_) => "signal",
243            #[cfg(feature = "whatsapp")]
244            Self::WhatsApp(_) => "whatsapp",
245        }
246    }
247
248    /// Construct a concrete [`Messenger`] from this config.
249    ///
250    /// The returned messenger has **not** been initialized; call
251    /// [`Messenger::initialize`] before use, or use [`GenericMessenger`] which
252    /// does this automatically.
253    pub fn build(&self) -> Result<Box<dyn Messenger>> {
254        use crate::messengers::*;
255        let m: Box<dyn Messenger> = match self {
256            Self::Irc(c) => Box::new(
257                IrcMessenger::new(&c.name, &c.server, c.port, &c.nick)
258                    .with_channels(c.channels.clone())
259                    .with_tls(c.tls),
260            ),
261            Self::Discord(c) => Box::new(DiscordMessenger::new(&c.name, &c.token)),
262            Self::Telegram(c) => Box::new(TelegramMessenger::new(&c.name, &c.token)),
263            Self::Slack(c) => Box::new(SlackMessenger::new(&c.name, &c.token)),
264            Self::Teams(c) => match (&c.webhook_url, &c.token, &c.team_id, &c.channel_id) {
265                (_, Some(token), Some(team_id), Some(channel_id)) => Box::new(
266                    TeamsMessenger::new_graph(&c.name, token, team_id, channel_id),
267                ),
268                (Some(webhook_url), _, _, _) => Box::new(TeamsMessenger::new(&c.name, webhook_url)),
269                _ => anyhow::bail!(
270                    "Teams config requires either webhook_url or token + team_id + channel_id"
271                ),
272            },
273            Self::GoogleChat(c) => match (&c.webhook_url, &c.token, &c.space_id) {
274                (_, Some(token), Some(space_id)) => {
275                    Box::new(GoogleChatMessenger::new_api(&c.name, token, space_id))
276                }
277                (Some(webhook_url), _, _) => {
278                    Box::new(GoogleChatMessenger::new(&c.name, webhook_url))
279                }
280                _ => anyhow::bail!(
281                    "Google Chat config requires either webhook_url or token + space_id"
282                ),
283            },
284            Self::Console(c) => Box::new(ConsoleMessenger::new(&c.name)),
285            Self::Webhook(c) => Box::new(WebhookMessenger::new(&c.name, &c.url)),
286            Self::IMessage(c) => Box::new(IMessageMessenger::new(&c.name)),
287            #[cfg(feature = "matrix")]
288            Self::Matrix(c) => Box::new(MatrixMessenger::new(
289                &c.name,
290                &c.homeserver,
291                &c.username,
292                &c.password,
293            )),
294            #[cfg(feature = "signal-cli")]
295            Self::SignalCli(c) => Box::new(
296                SignalCliMessenger::new(&c.name, &c.phone_number).with_cli_path(&c.cli_path),
297            ),
298            #[cfg(feature = "whatsapp")]
299            Self::WhatsApp(c) => Box::new(WhatsAppMessenger::new(&c.name, &c.db_path)),
300        };
301        Ok(m)
302    }
303}
304
305// ── per-protocol server config structs ────────────────────────────────────────
306
307/// Configuration for an IRC listener.
308///
309/// Each `IrcListenerConfig` represents a single TCP address the server will
310/// accept IRC connections on.
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct IrcListenerConfig {
313    /// TCP address to bind (e.g. `"0.0.0.0:6667"`).
314    pub address: String,
315}
316
317#[typetag::serde(name = "irc")]
318impl ListenerConfig for IrcListenerConfig {
319    fn protocol(&self) -> &str {
320        "irc"
321    }
322
323    fn address(&self) -> &str {
324        &self.address
325    }
326
327    fn build(&self) -> Box<dyn crate::server::ChatListener> {
328        Box::new(crate::servers::IrcListener::new(&self.address))
329    }
330
331    fn clone_box(&self) -> Box<dyn ListenerConfig> {
332        Box::new(self.clone())
333    }
334
335    fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336        std::fmt::Debug::fmt(self, f)
337    }
338}
339
340// ── ListenerConfig ────────────────────────────────────────────────────────────
341
342/// Per-listener configuration trait.
343///
344/// Each protocol provides its own config struct that implements this trait.
345/// The trait is serde-compatible via [`typetag`], so `Vec<Box<dyn
346/// ListenerConfig>>` can be serialized and deserialized from JSON, TOML, etc.
347///
348/// # Implementing a custom listener config
349///
350/// ```rust,ignore
351/// use chat_system::config::ListenerConfig;
352///
353/// #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
354/// pub struct MyListenerConfig { pub address: String }
355///
356/// #[typetag::serde(name = "my-protocol")]
357/// impl ListenerConfig for MyListenerConfig {
358///     fn protocol(&self) -> &str { "my-protocol" }
359///     fn address(&self) -> &str { &self.address }
360///     fn build(&self) -> Box<dyn chat_system::server::ChatListener> { todo!() }
361///     fn clone_box(&self) -> Box<dyn ListenerConfig> { Box::new(self.clone()) }
362///     fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363///         std::fmt::Debug::fmt(self, f)
364///     }
365/// }
366/// ```
367#[typetag::serde(tag = "protocol")]
368pub trait ListenerConfig: Send + Sync {
369    /// The wire protocol this listener speaks (e.g. `"irc"`).
370    fn protocol(&self) -> &str;
371
372    /// The address this listener will bind.
373    fn address(&self) -> &str;
374
375    /// Construct a concrete [`ChatListener`](crate::server::ChatListener) from
376    /// this config.
377    fn build(&self) -> Box<dyn crate::server::ChatListener>;
378
379    /// Clone this config into a new boxed trait object.
380    fn clone_box(&self) -> Box<dyn ListenerConfig>;
381
382    /// Format this config for debug output.
383    fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
384}
385
386impl Clone for Box<dyn ListenerConfig> {
387    fn clone(&self) -> Self {
388        self.clone_box()
389    }
390}
391
392impl std::fmt::Debug for dyn ListenerConfig {
393    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394        self.debug_fmt(f)
395    }
396}
397
398// ── ServerConfig ───────────────────────────────────────────────────────────────
399
400/// Server configuration.
401///
402/// A server has a `name` and a list of listener configs.  Each listener may use
403/// a different protocol; the server itself is protocol-agnostic.
404///
405/// # Example (JSON)
406///
407/// ```json
408/// {
409///   "name": "my-server",
410///   "listeners": [
411///     { "protocol": "irc", "address": "0.0.0.0:6667" },
412///     { "protocol": "irc", "address": "0.0.0.0:6697" }
413///   ]
414/// }
415/// ```
416#[derive(Clone, Debug, Serialize, Deserialize)]
417pub struct ServerConfig {
418    pub name: String,
419    pub listeners: Vec<Box<dyn ListenerConfig>>,
420}
421
422impl ServerConfig {
423    /// The human-readable name for this server instance.
424    pub fn name(&self) -> &str {
425        &self.name
426    }
427
428    /// The listener configurations.
429    pub fn listener_configs(&self) -> &[Box<dyn ListenerConfig>] {
430        &self.listeners
431    }
432}
433
434// ── GenericMessenger ───────────────────────────────────────────────────────────
435
436/// A [`Messenger`] whose protocol is determined at runtime by a [`MessengerConfig`].
437///
438/// Construct with a config, call [`Messenger::initialize`] to establish the
439/// connection (which also builds the inner backend), then use it like any other
440/// [`Messenger`].
441///
442/// Because [`GenericMessenger`] implements [`Messenger`] it is a drop-in
443/// replacement everywhere a `Box<dyn Messenger>` is accepted, including
444/// [`MessengerManager`](crate::MessengerManager).
445pub struct GenericMessenger {
446    config: MessengerConfig,
447    inner: Option<Box<dyn Messenger>>,
448}
449
450impl GenericMessenger {
451    /// Create a new uninitialized [`GenericMessenger`] from a config.
452    pub fn new(config: MessengerConfig) -> Self {
453        Self {
454            config,
455            inner: None,
456        }
457    }
458
459    /// Access the underlying config.
460    pub fn config(&self) -> &MessengerConfig {
461        &self.config
462    }
463}
464
465#[async_trait]
466impl Messenger for GenericMessenger {
467    fn name(&self) -> &str {
468        self.inner
469            .as_ref()
470            .map(|m| m.name())
471            .unwrap_or_else(|| self.config.name())
472    }
473
474    fn messenger_type(&self) -> &str {
475        self.inner
476            .as_ref()
477            .map(|m| m.messenger_type())
478            .unwrap_or_else(|| self.config.protocol_name())
479    }
480
481    async fn initialize(&mut self) -> Result<()> {
482        let mut built = self.config.build()?;
483        built.initialize().await?;
484        self.inner = Some(built);
485        Ok(())
486    }
487
488    async fn send_message(&self, recipient: &str, content: &str) -> Result<String> {
489        self.inner
490            .as_ref()
491            .ok_or_else(|| anyhow::anyhow!("GenericMessenger not initialized"))?
492            .send_message(recipient, content)
493            .await
494    }
495
496    async fn send_message_with_options(&self, opts: SendOptions<'_>) -> Result<String> {
497        self.inner
498            .as_ref()
499            .ok_or_else(|| anyhow::anyhow!("GenericMessenger not initialized"))?
500            .send_message_with_options(opts)
501            .await
502    }
503
504    async fn receive_messages(&self) -> Result<Vec<Message>> {
505        self.inner
506            .as_ref()
507            .ok_or_else(|| anyhow::anyhow!("GenericMessenger not initialized"))?
508            .receive_messages()
509            .await
510    }
511
512    fn is_connected(&self) -> bool {
513        self.inner
514            .as_ref()
515            .map(|m| m.is_connected())
516            .unwrap_or(false)
517    }
518
519    async fn disconnect(&mut self) -> Result<()> {
520        if let Some(inner) = &mut self.inner {
521            inner.disconnect().await?;
522        }
523        Ok(())
524    }
525
526    async fn set_typing(&self, channel: &str, typing: bool) -> Result<()> {
527        if let Some(inner) = &self.inner {
528            inner.set_typing(channel, typing).await
529        } else {
530            Ok(())
531        }
532    }
533
534    async fn set_status(&self, status: PresenceStatus) -> Result<()> {
535        if let Some(inner) = &self.inner {
536            inner.set_status(status).await
537        } else {
538            Ok(())
539        }
540    }
541
542    async fn add_reaction(&self, message_id: &str, channel: &str, emoji: &str) -> Result<()> {
543        if let Some(inner) = &self.inner {
544            inner.add_reaction(message_id, channel, emoji).await
545        } else {
546            Ok(())
547        }
548    }
549
550    async fn remove_reaction(&self, message_id: &str, channel: &str, emoji: &str) -> Result<()> {
551        if let Some(inner) = &self.inner {
552            inner.remove_reaction(message_id, channel, emoji).await
553        } else {
554            Ok(())
555        }
556    }
557
558    async fn get_profile_picture(&self, user_id: &str) -> Result<Option<String>> {
559        if let Some(inner) = &self.inner {
560            inner.get_profile_picture(user_id).await
561        } else {
562            Ok(None)
563        }
564    }
565
566    async fn set_profile_picture(&self, url: &str) -> Result<()> {
567        if let Some(inner) = &self.inner {
568            inner.set_profile_picture(url).await
569        } else {
570            Ok(())
571        }
572    }
573
574    async fn set_text_status(&self, text: &str) -> Result<()> {
575        if let Some(inner) = &self.inner {
576            inner.set_text_status(text).await
577        } else {
578            Ok(())
579        }
580    }
581
582    async fn search_messages(&self, query: SearchQuery) -> Result<Vec<Message>> {
583        if let Some(inner) = &self.inner {
584            inner.search_messages(query).await
585        } else {
586            Ok(Vec::new())
587        }
588    }
589
590    async fn edit_message(&self, message_id: &str, channel: &str, new_content: &str) -> Result<()> {
591        if let Some(inner) = &self.inner {
592            inner.edit_message(message_id, channel, new_content).await
593        } else {
594            Ok(())
595        }
596    }
597
598    async fn delete_message(&self, message_id: &str, channel: &str) -> Result<()> {
599        if let Some(inner) = &self.inner {
600            inner.delete_message(message_id, channel).await
601        } else {
602            Ok(())
603        }
604    }
605
606    async fn pin_message(&self, message_id: &str, channel: &str) -> Result<()> {
607        if let Some(inner) = &self.inner {
608            inner.pin_message(message_id, channel).await
609        } else {
610            Ok(())
611        }
612    }
613
614    async fn unpin_message(&self, message_id: &str, channel: &str) -> Result<()> {
615        if let Some(inner) = &self.inner {
616            inner.unpin_message(message_id, channel).await
617        } else {
618            Ok(())
619        }
620    }
621
622    async fn get_channel_members(&self, channel: &str) -> Result<Vec<String>> {
623        if let Some(inner) = &self.inner {
624            inner.get_channel_members(channel).await
625        } else {
626            Ok(Vec::new())
627        }
628    }
629}
630
631// ── GenericServer ──────────────────────────────────────────────────────────────
632
633/// A config-driven server.
634///
635/// Builds a [`Server`](crate::server::Server) from a [`ServerConfig`],
636/// constructing the appropriate listeners for each entry in the config's
637/// `listeners` list.
638pub struct GenericServer {
639    config: ServerConfig,
640    inner: Option<crate::server::Server>,
641}
642
643impl GenericServer {
644    /// Create a new [`GenericServer`] from a config.
645    pub fn new(config: ServerConfig) -> Self {
646        Self {
647            config,
648            inner: None,
649        }
650    }
651
652    /// Access the underlying config.
653    pub fn config(&self) -> &ServerConfig {
654        &self.config
655    }
656
657    fn build_inner(&self) -> crate::server::Server {
658        let mut server = crate::server::Server::new(&self.config.name);
659        for lc in &self.config.listeners {
660            server = server.add_boxed_listener(lc.build());
661        }
662        server
663    }
664}
665
666#[async_trait]
667impl ChatServer for GenericServer {
668    fn name(&self) -> &str {
669        match &self.inner {
670            Some(s) => s.name(),
671            None => &self.config.name,
672        }
673    }
674
675    fn listeners(&self) -> Vec<&dyn crate::server::ChatListener> {
676        match &self.inner {
677            Some(s) => s.listeners(),
678            None => Vec::new(),
679        }
680    }
681
682    async fn run<F, Fut>(&mut self, handler: F) -> Result<()>
683    where
684        F: Fn(Message) -> Fut + Send + Sync + 'static,
685        Fut: std::future::Future<Output = Result<Option<String>>> + Send + 'static,
686    {
687        if self.inner.is_none() {
688            self.inner = Some(self.build_inner());
689        }
690        self.inner.as_mut().unwrap().run(handler).await
691    }
692
693    async fn shutdown(&mut self) -> Result<()> {
694        if let Some(s) = &mut self.inner {
695            s.shutdown().await
696        } else {
697            Ok(())
698        }
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705
706    #[test]
707    fn messenger_config_roundtrip_json() {
708        let cfg = MessengerConfig::Irc(IrcConfig {
709            name: "bot".into(),
710            server: "irc.libera.chat".into(),
711            port: 6697,
712            nick: "bot".into(),
713            channels: vec!["#rust".into()],
714            tls: true,
715        });
716        let json = serde_json::to_string(&cfg).unwrap();
717        let decoded: MessengerConfig = serde_json::from_str(&json).unwrap();
718        assert_eq!(decoded.protocol_name(), "irc");
719        assert_eq!(decoded.name(), "bot");
720    }
721
722    #[test]
723    fn messenger_config_deserialize_protocol_tag() {
724        let json = r#"{"protocol":"discord","name":"d-bot","token":"tok123"}"#;
725        let cfg: MessengerConfig = serde_json::from_str(json).unwrap();
726        assert_eq!(cfg.protocol_name(), "discord");
727        assert_eq!(cfg.name(), "d-bot");
728    }
729
730    #[test]
731    fn server_config_roundtrip_json() {
732        let cfg = ServerConfig {
733            name: "srv".into(),
734            listeners: vec![Box::new(IrcListenerConfig {
735                address: "0.0.0.0:6667".into(),
736            })],
737        };
738        let json = serde_json::to_string(&cfg).unwrap();
739        let decoded: ServerConfig = serde_json::from_str(&json).unwrap();
740        assert_eq!(decoded.name(), "srv");
741        assert_eq!(decoded.listeners.len(), 1);
742        assert_eq!(decoded.listeners[0].address(), "0.0.0.0:6667");
743        assert_eq!(decoded.listeners[0].protocol(), "irc");
744    }
745
746    #[test]
747    fn server_config_multi_listener_roundtrip_json() {
748        let cfg = ServerConfig {
749            name: "srv".into(),
750            listeners: vec![
751                Box::new(IrcListenerConfig {
752                    address: "0.0.0.0:6667".into(),
753                }),
754                Box::new(IrcListenerConfig {
755                    address: "0.0.0.0:6697".into(),
756                }),
757            ],
758        };
759        let json = serde_json::to_string(&cfg).unwrap();
760        let decoded: ServerConfig = serde_json::from_str(&json).unwrap();
761        assert_eq!(decoded.listeners.len(), 2);
762        assert_eq!(decoded.listeners[0].address(), "0.0.0.0:6667");
763        assert_eq!(decoded.listeners[1].address(), "0.0.0.0:6697");
764    }
765
766    #[test]
767    fn generic_messenger_name_before_init() {
768        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
769        let gm = GenericMessenger::new(cfg);
770        assert_eq!(gm.name(), "con");
771        assert_eq!(gm.messenger_type(), "console");
772        assert!(!gm.is_connected());
773    }
774
775    #[test]
776    fn generic_server_name_before_run() {
777        let cfg = ServerConfig {
778            name: "srv".into(),
779            listeners: vec![Box::new(IrcListenerConfig {
780                address: "127.0.0.1:7777".into(),
781            })],
782        };
783        let gs = GenericServer::new(cfg);
784        assert_eq!(gs.name(), "srv");
785    }
786
787    #[tokio::test]
788    async fn generic_messenger_set_typing_before_init_is_ok() {
789        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
790        let gm = GenericMessenger::new(cfg);
791        // Should be a no-op (not initialized yet), not an error.
792        gm.set_typing("#general", true).await.unwrap();
793        gm.set_typing("#general", false).await.unwrap();
794    }
795
796    #[tokio::test]
797    async fn generic_messenger_set_typing_after_init_is_ok() {
798        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
799        let mut gm = GenericMessenger::new(cfg);
800        gm.initialize().await.unwrap();
801        gm.set_typing("#general", true).await.unwrap();
802        gm.set_typing("#general", false).await.unwrap();
803        gm.disconnect().await.unwrap();
804    }
805
806    #[tokio::test]
807    async fn generic_messenger_set_status_before_init_is_ok() {
808        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
809        let gm = GenericMessenger::new(cfg);
810        gm.set_status(PresenceStatus::Online).await.unwrap();
811        gm.set_status(PresenceStatus::Away).await.unwrap();
812        gm.set_status(PresenceStatus::Busy).await.unwrap();
813        gm.set_status(PresenceStatus::Invisible).await.unwrap();
814        gm.set_status(PresenceStatus::Offline).await.unwrap();
815    }
816
817    #[tokio::test]
818    async fn generic_messenger_set_status_after_init_is_ok() {
819        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
820        let mut gm = GenericMessenger::new(cfg);
821        gm.initialize().await.unwrap();
822        gm.set_status(PresenceStatus::Online).await.unwrap();
823        gm.set_status(PresenceStatus::Away).await.unwrap();
824        gm.disconnect().await.unwrap();
825    }
826
827    #[test]
828    fn presence_status_serde_roundtrip() {
829        for status in [
830            PresenceStatus::Online,
831            PresenceStatus::Away,
832            PresenceStatus::Busy,
833            PresenceStatus::Invisible,
834            PresenceStatus::Offline,
835        ] {
836            let json = serde_json::to_string(&status).unwrap();
837            let decoded: PresenceStatus = serde_json::from_str(&json).unwrap();
838            assert_eq!(decoded, status);
839        }
840    }
841
842    #[test]
843    fn presence_status_json_values() {
844        assert_eq!(
845            serde_json::to_string(&PresenceStatus::Online).unwrap(),
846            r#""online""#
847        );
848        assert_eq!(
849            serde_json::to_string(&PresenceStatus::Away).unwrap(),
850            r#""away""#
851        );
852        assert_eq!(
853            serde_json::to_string(&PresenceStatus::Busy).unwrap(),
854            r#""busy""#
855        );
856        assert_eq!(
857            serde_json::to_string(&PresenceStatus::Invisible).unwrap(),
858            r#""invisible""#
859        );
860        assert_eq!(
861            serde_json::to_string(&PresenceStatus::Offline).unwrap(),
862            r#""offline""#
863        );
864    }
865
866    #[tokio::test]
867    async fn generic_messenger_add_reaction_before_init_is_ok() {
868        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
869        let gm = GenericMessenger::new(cfg);
870        gm.add_reaction("msg-1", "#general", "👍").await.unwrap();
871    }
872
873    #[tokio::test]
874    async fn generic_messenger_add_reaction_after_init_is_ok() {
875        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
876        let mut gm = GenericMessenger::new(cfg);
877        gm.initialize().await.unwrap();
878        gm.add_reaction("msg-1", "#general", "👍").await.unwrap();
879        gm.disconnect().await.unwrap();
880    }
881
882    #[tokio::test]
883    async fn generic_messenger_remove_reaction_before_init_is_ok() {
884        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
885        let gm = GenericMessenger::new(cfg);
886        gm.remove_reaction("msg-1", "#general", "👍").await.unwrap();
887    }
888
889    #[tokio::test]
890    async fn generic_messenger_remove_reaction_after_init_is_ok() {
891        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
892        let mut gm = GenericMessenger::new(cfg);
893        gm.initialize().await.unwrap();
894        gm.remove_reaction("msg-1", "#general", "❤️").await.unwrap();
895        gm.disconnect().await.unwrap();
896    }
897
898    #[tokio::test]
899    async fn generic_messenger_get_profile_picture_before_init_returns_none() {
900        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
901        let gm = GenericMessenger::new(cfg);
902        let pic = gm.get_profile_picture("alice").await.unwrap();
903        assert!(pic.is_none());
904    }
905
906    #[tokio::test]
907    async fn generic_messenger_get_profile_picture_after_init_returns_none() {
908        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
909        let mut gm = GenericMessenger::new(cfg);
910        gm.initialize().await.unwrap();
911        let pic = gm.get_profile_picture("bob").await.unwrap();
912        assert!(pic.is_none());
913        gm.disconnect().await.unwrap();
914    }
915
916    #[tokio::test]
917    async fn generic_messenger_set_profile_picture_before_init_is_ok() {
918        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
919        let gm = GenericMessenger::new(cfg);
920        gm.set_profile_picture("https://example.com/avatar.png")
921            .await
922            .unwrap();
923    }
924
925    #[tokio::test]
926    async fn generic_messenger_set_profile_picture_after_init_is_ok() {
927        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
928        let mut gm = GenericMessenger::new(cfg);
929        gm.initialize().await.unwrap();
930        gm.set_profile_picture("https://example.com/avatar.png")
931            .await
932            .unwrap();
933        gm.disconnect().await.unwrap();
934    }
935
936    #[tokio::test]
937    async fn generic_messenger_set_text_status_before_init_is_ok() {
938        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
939        let gm = GenericMessenger::new(cfg);
940        gm.set_text_status("Working from home 🏠").await.unwrap();
941    }
942
943    #[tokio::test]
944    async fn generic_messenger_set_text_status_after_init_is_ok() {
945        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
946        let mut gm = GenericMessenger::new(cfg);
947        gm.initialize().await.unwrap();
948        gm.set_text_status("In a meeting").await.unwrap();
949        gm.disconnect().await.unwrap();
950    }
951
952    #[tokio::test]
953    async fn generic_messenger_search_messages_before_init_returns_empty() {
954        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
955        let gm = GenericMessenger::new(cfg);
956        let results = gm
957            .search_messages(SearchQuery {
958                text: "hello".into(),
959                ..Default::default()
960            })
961            .await
962            .unwrap();
963        assert!(results.is_empty());
964    }
965
966    #[tokio::test]
967    async fn generic_messenger_search_messages_after_init_returns_empty() {
968        let cfg = MessengerConfig::Console(ConsoleConfig { name: "con".into() });
969        let mut gm = GenericMessenger::new(cfg);
970        gm.initialize().await.unwrap();
971        let results = gm
972            .search_messages(SearchQuery {
973                text: "rust".into(),
974                channel: Some("#general".into()),
975                limit: Some(10),
976                ..Default::default()
977            })
978            .await
979            .unwrap();
980        assert!(results.is_empty());
981        gm.disconnect().await.unwrap();
982    }
983
984    #[test]
985    fn search_query_serde_roundtrip() {
986        let q = SearchQuery {
987            text: "hello world".into(),
988            channel: Some("#rust".into()),
989            from: Some("alice".into()),
990            limit: Some(50),
991            before_timestamp: Some(9_999_999),
992            after_timestamp: Some(1_000_000),
993        };
994        let json = serde_json::to_string(&q).unwrap();
995        let de: SearchQuery = serde_json::from_str(&json).unwrap();
996        assert_eq!(de.text, q.text);
997        assert_eq!(de.channel, q.channel);
998        assert_eq!(de.from, q.from);
999        assert_eq!(de.limit, q.limit);
1000        assert_eq!(de.before_timestamp, q.before_timestamp);
1001        assert_eq!(de.after_timestamp, q.after_timestamp);
1002    }
1003
1004    #[test]
1005    fn search_query_defaults() {
1006        let q: SearchQuery = serde_json::from_str(r#"{"text":"hi"}"#).unwrap();
1007        assert_eq!(q.text, "hi");
1008        assert!(q.channel.is_none());
1009        assert!(q.from.is_none());
1010        assert!(q.limit.is_none());
1011        assert!(q.before_timestamp.is_none());
1012        assert!(q.after_timestamp.is_none());
1013    }
1014}