1use 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct DiscordConfig {
70 pub name: String,
71 pub token: String,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct TelegramConfig {
77 pub name: String,
78 pub token: String,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SlackConfig {
84 pub name: String,
85 pub token: String,
86}
87
88#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ConsoleConfig {
123 pub name: String,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct WebhookConfig {
129 pub name: String,
130 pub url: String,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct IMessageConfig {
136 pub name: String,
137}
138
139#[cfg(feature = "matrix")]
140#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct WhatsAppConfig {
167 pub name: String,
168 pub db_path: String,
170}
171
172#[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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct IrcListenerConfig {
313 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#[typetag::serde(tag = "protocol")]
368pub trait ListenerConfig: Send + Sync {
369 fn protocol(&self) -> &str;
371
372 fn address(&self) -> &str;
374
375 fn build(&self) -> Box<dyn crate::server::ChatListener>;
378
379 fn clone_box(&self) -> Box<dyn ListenerConfig>;
381
382 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#[derive(Clone, Debug, Serialize, Deserialize)]
417pub struct ServerConfig {
418 pub name: String,
419 pub listeners: Vec<Box<dyn ListenerConfig>>,
420}
421
422impl ServerConfig {
423 pub fn name(&self) -> &str {
425 &self.name
426 }
427
428 pub fn listener_configs(&self) -> &[Box<dyn ListenerConfig>] {
430 &self.listeners
431 }
432}
433
434pub struct GenericMessenger {
446 config: MessengerConfig,
447 inner: Option<Box<dyn Messenger>>,
448}
449
450impl GenericMessenger {
451 pub fn new(config: MessengerConfig) -> Self {
453 Self {
454 config,
455 inner: None,
456 }
457 }
458
459 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
631pub struct GenericServer {
639 config: ServerConfig,
640 inner: Option<crate::server::Server>,
641}
642
643impl GenericServer {
644 pub fn new(config: ServerConfig) -> Self {
646 Self {
647 config,
648 inner: None,
649 }
650 }
651
652 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 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}