1use std::{future::Future, pin::Pin, sync::Arc};
9
10use tracing::{Instrument as _, debug, info};
11
12use crate::{
13 adapter::ChatAdapter,
14 emoji::EmojiValue,
15 error::ChatError,
16 event::{
17 ActionEvent, ChatEvent, ModalCloseEvent, ModalSubmitEvent, ReactionEvent, SlashCommandEvent,
18 },
19 message::IncomingMessage,
20 thread::ThreadHandle,
21};
22
23pub(crate) type MentionHandler = Box<
30 dyn Fn(ThreadHandle, IncomingMessage) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
31>;
32
33pub(crate) type MessageHandler = (
35 Option<String>,
36 Box<
37 dyn Fn(ThreadHandle, IncomingMessage) -> Pin<Box<dyn Future<Output = ()> + Send>>
38 + Send
39 + Sync,
40 >,
41);
42
43pub(crate) type SubscribedMessageHandler = Box<
45 dyn Fn(ThreadHandle, IncomingMessage) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
46>;
47
48pub(crate) type ActionHandler = (
50 Option<Vec<String>>,
51 Box<
52 dyn Fn(ActionEvent, ThreadHandle) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync,
53 >,
54);
55
56pub(crate) type ReactionHandler = (
58 Option<Vec<EmojiValue>>,
59 Box<dyn Fn(ReactionEvent) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>,
60);
61
62pub(crate) type SlashCommandHandler = (
64 Option<Vec<String>>,
65 Box<dyn Fn(SlashCommandEvent) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>,
66);
67
68pub(crate) type ModalSubmitHandler = (
70 Option<Vec<String>>,
71 Box<dyn Fn(ModalSubmitEvent) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>,
72);
73
74pub(crate) type ModalCloseHandler =
76 Box<dyn Fn(ModalCloseEvent) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
77
78#[derive(Debug, Clone)]
84pub struct ChatBotConfig {
85 pub streaming_update_interval_ms: u64,
87 pub fallback_streaming_placeholder: String,
89}
90
91impl Default for ChatBotConfig {
92 fn default() -> Self {
93 Self { streaming_update_interval_ms: 500, fallback_streaming_placeholder: "...".into() }
94 }
95}
96
97pub struct ChatBot {
113 pub(crate) adapters: Vec<Box<dyn ChatAdapter>>,
115 pub(crate) config: ChatBotConfig,
117
118 pub(crate) mention_handlers: Vec<MentionHandler>,
120 pub(crate) message_handlers: Vec<MessageHandler>,
121 pub(crate) subscribed_message_handlers: Vec<SubscribedMessageHandler>,
122 pub(crate) action_handlers: Vec<ActionHandler>,
123 pub(crate) reaction_handlers: Vec<ReactionHandler>,
124 pub(crate) slash_command_handlers: Vec<SlashCommandHandler>,
125 pub(crate) modal_submit_handlers: Vec<ModalSubmitHandler>,
126 pub(crate) modal_close_handlers: Vec<ModalCloseHandler>,
127
128 pub(crate) subscriptions: Arc<scc::HashMap<String, ()>>,
130}
131
132impl ChatBot {
133 #[must_use]
135 pub fn new(config: ChatBotConfig) -> Self {
136 Self {
137 adapters: Vec::new(),
138 config,
139 mention_handlers: Vec::new(),
140 message_handlers: Vec::new(),
141 subscribed_message_handlers: Vec::new(),
142 action_handlers: Vec::new(),
143 reaction_handlers: Vec::new(),
144 slash_command_handlers: Vec::new(),
145 modal_submit_handlers: Vec::new(),
146 modal_close_handlers: Vec::new(),
147 subscriptions: Arc::new(scc::HashMap::new()),
148 }
149 }
150
151 pub fn add_adapter(&mut self, adapter: impl ChatAdapter + 'static) {
157 self.adapters.push(Box::new(adapter));
158 }
159
160 pub fn on_mention<F, Fut>(&mut self, handler: F)
166 where
167 F: Fn(ThreadHandle, IncomingMessage) -> Fut + Send + Sync + 'static,
168 Fut: Future<Output = ()> + Send + 'static,
169 {
170 self.mention_handlers.push(Box::new(move |t, m| Box::pin(handler(t, m))));
171 }
172
173 pub fn on_message<F, Fut>(&mut self, pattern: Option<String>, handler: F)
178 where
179 F: Fn(ThreadHandle, IncomingMessage) -> Fut + Send + Sync + 'static,
180 Fut: Future<Output = ()> + Send + 'static,
181 {
182 self.message_handlers.push((pattern, Box::new(move |t, m| Box::pin(handler(t, m)))));
183 }
184
185 pub fn on_subscribed_message<F, Fut>(&mut self, handler: F)
187 where
188 F: Fn(ThreadHandle, IncomingMessage) -> Fut + Send + Sync + 'static,
189 Fut: Future<Output = ()> + Send + 'static,
190 {
191 self.subscribed_message_handlers.push(Box::new(move |t, m| Box::pin(handler(t, m))));
192 }
193
194 pub fn on_action<F, Fut>(&mut self, action_ids: Option<Vec<String>>, handler: F)
199 where
200 F: Fn(ActionEvent, ThreadHandle) -> Fut + Send + Sync + 'static,
201 Fut: Future<Output = ()> + Send + 'static,
202 {
203 self.action_handlers.push((action_ids, Box::new(move |a, t| Box::pin(handler(a, t)))));
204 }
205
206 pub fn on_reaction<F, Fut>(&mut self, emojis: Option<Vec<EmojiValue>>, handler: F)
211 where
212 F: Fn(ReactionEvent) -> Fut + Send + Sync + 'static,
213 Fut: Future<Output = ()> + Send + 'static,
214 {
215 self.reaction_handlers.push((emojis, Box::new(move |r| Box::pin(handler(r)))));
216 }
217
218 pub fn on_slash_command<F, Fut>(&mut self, commands: Option<Vec<String>>, handler: F)
223 where
224 F: Fn(SlashCommandEvent) -> Fut + Send + Sync + 'static,
225 Fut: Future<Output = ()> + Send + 'static,
226 {
227 self.slash_command_handlers.push((commands, Box::new(move |s| Box::pin(handler(s)))));
228 }
229
230 pub fn on_modal_submit<F, Fut>(&mut self, callback_ids: Option<Vec<String>>, handler: F)
235 where
236 F: Fn(ModalSubmitEvent) -> Fut + Send + Sync + 'static,
237 Fut: Future<Output = ()> + Send + 'static,
238 {
239 self.modal_submit_handlers.push((callback_ids, Box::new(move |ms| Box::pin(handler(ms)))));
240 }
241
242 pub fn on_modal_close<F, Fut>(&mut self, handler: F)
244 where
245 F: Fn(ModalCloseEvent) -> Fut + Send + Sync + 'static,
246 Fut: Future<Output = ()> + Send + 'static,
247 {
248 self.modal_close_handlers.push(Box::new(move |mc| Box::pin(handler(mc))));
249 }
250
251 pub async fn run(&mut self) -> Result<(), ChatError> {
269 use futures_util::stream::{self, StreamExt as _};
270
271 if self.adapters.is_empty() {
272 return Err(ChatError::Closed);
273 }
274
275 let adapters = std::mem::take(&mut self.adapters);
278
279 let streams: Vec<_> = adapters
283 .into_iter()
284 .map(|adapter| {
285 Box::pin(stream::unfold(adapter, |mut a| async move {
286 let event = a.recv_event().await;
287 event.map(|e| (e, a))
288 }))
289 })
290 .collect();
291
292 let mut merged = stream::select_all(streams);
293
294 info!("ChatBot event loop started");
295
296 while let Some(event) = merged.next().await {
297 self.dispatch_event(event).await;
298 }
299
300 info!("ChatBot event loop finished — all adapters exhausted");
301 Ok(())
302 }
303
304 async fn dispatch_event(&self, event: ChatEvent) {
310 match event {
311 ChatEvent::Mention { thread_id, message } => {
312 let span = tracing::info_span!("dispatch_mention", thread_id = %thread_id);
313 async {
314 let is_subscribed = self.subscriptions.contains_async(&thread_id).await;
315
316 if is_subscribed {
317 debug!(
318 thread_id = %thread_id,
319 "mention in subscribed thread — routing to subscribed handlers"
320 );
321 self.dispatch_subscribed_message(thread_id, message).await;
322 } else {
323 debug!(
324 thread_id = %thread_id,
325 handler_count = self.mention_handlers.len(),
326 "dispatching mention"
327 );
328 for handler in &self.mention_handlers {
329 let handle = self.make_thread_handle(&thread_id);
330 handler(handle, message.clone()).await;
331 }
332 }
333 }
334 .instrument(span)
335 .await;
336 }
337
338 ChatEvent::Message { thread_id, message } => {
339 let span = tracing::info_span!("dispatch_message", thread_id = %thread_id);
340 async {
341 let is_subscribed = self.subscriptions.contains_async(&thread_id).await;
342
343 if is_subscribed {
344 debug!(
345 thread_id = %thread_id,
346 "message in subscribed thread — routing to subscribed handlers"
347 );
348 self.dispatch_subscribed_message(thread_id, message).await;
349 } else {
350 self.dispatch_message_handlers(&thread_id, message).await;
351 }
352 }
353 .instrument(span)
354 .await;
355 }
356
357 ChatEvent::Reaction(reaction) => {
358 let span = tracing::info_span!(
359 "dispatch_reaction",
360 thread_id = %reaction.thread_id,
361 emoji = %reaction.emoji
362 );
363 async {
364 for (filter, handler) in &self.reaction_handlers {
365 let should_fire = match filter {
366 Some(emojis) => emojis.contains(&reaction.emoji),
367 None => true,
368 };
369 if should_fire {
370 handler(reaction.clone()).await;
371 }
372 }
373 }
374 .instrument(span)
375 .await;
376 }
377
378 ChatEvent::Action(action) => {
379 let span = tracing::info_span!(
380 "dispatch_action",
381 action_id = %action.action_id,
382 thread_id = %action.thread_id
383 );
384 async {
385 for (filter, handler) in &self.action_handlers {
386 let should_fire = match filter {
387 Some(ids) => ids.contains(&action.action_id),
388 None => true,
389 };
390 if should_fire {
391 let handle = self.make_thread_handle(&action.thread_id);
392 handler(action.clone(), handle).await;
393 }
394 }
395 }
396 .instrument(span)
397 .await;
398 }
399
400 ChatEvent::SlashCommand(cmd) => {
401 let span = tracing::info_span!(
402 "dispatch_slash_command",
403 command = %cmd.command,
404 channel_id = %cmd.channel_id
405 );
406 async {
407 for (filter, handler) in &self.slash_command_handlers {
408 let should_fire = match filter {
409 Some(commands) => commands.contains(&cmd.command),
410 None => true,
411 };
412 if should_fire {
413 handler(cmd.clone()).await;
414 }
415 }
416 }
417 .instrument(span)
418 .await;
419 }
420
421 ChatEvent::ModalSubmit(submit) => {
422 let span = tracing::info_span!(
423 "dispatch_modal_submit",
424 callback_id = %submit.callback_id
425 );
426 async {
427 for (filter, handler) in &self.modal_submit_handlers {
428 let should_fire = match filter {
429 Some(ids) => ids.contains(&submit.callback_id),
430 None => true,
431 };
432 if should_fire {
433 handler(submit.clone()).await;
434 }
435 }
436 }
437 .instrument(span)
438 .await;
439 }
440
441 ChatEvent::ModalClose(close) => {
442 let span = tracing::info_span!(
443 "dispatch_modal_close",
444 callback_id = %close.callback_id
445 );
446 async {
447 for handler in &self.modal_close_handlers {
448 handler(close.clone()).await;
449 }
450 }
451 .instrument(span)
452 .await;
453 }
454 }
455 }
456
457 async fn dispatch_subscribed_message(&self, thread_id: String, message: IncomingMessage) {
459 debug!(
460 thread_id = %thread_id,
461 handler_count = self.subscribed_message_handlers.len(),
462 "dispatching subscribed message"
463 );
464 for handler in &self.subscribed_message_handlers {
465 let handle = self.make_thread_handle(&thread_id);
466 handler(handle, message.clone()).await;
467 }
468 }
469
470 async fn dispatch_message_handlers(&self, thread_id: &str, message: IncomingMessage) {
472 debug!(
473 thread_id = %thread_id,
474 handler_count = self.message_handlers.len(),
475 "dispatching message"
476 );
477 for (pattern, handler) in &self.message_handlers {
478 let should_fire = match pattern {
479 Some(pat) => message.text.contains(pat.as_str()),
480 None => true,
481 };
482 if should_fire {
483 let handle = self.make_thread_handle(thread_id);
484 handler(handle, message.clone()).await;
485 }
486 }
487 }
488
489 fn make_thread_handle(&self, thread_id: &str) -> ThreadHandle {
501 ThreadHandle {
502 thread_id: thread_id.to_owned(),
503 adapter: Arc::new(NullAdapter),
504 subscriptions: Arc::clone(&self.subscriptions),
505 }
506 }
507}
508
509struct NullAdapter;
516
517#[async_trait::async_trait]
518impl ChatAdapter for NullAdapter {
519 fn name(&self) -> &'static str {
520 "null"
521 }
522
523 async fn post_message(
524 &self,
525 _thread_id: &str,
526 _message: &crate::message::AdapterPostableMessage,
527 ) -> Result<crate::message::SentMessage, ChatError> {
528 Err(ChatError::Closed)
529 }
530
531 async fn edit_message(
532 &self,
533 _thread_id: &str,
534 _message_id: &str,
535 _message: &crate::message::AdapterPostableMessage,
536 ) -> Result<crate::message::SentMessage, ChatError> {
537 Err(ChatError::Closed)
538 }
539
540 async fn delete_message(&self, _thread_id: &str, _message_id: &str) -> Result<(), ChatError> {
541 Err(ChatError::Closed)
542 }
543
544 fn render_card(&self, _card: &crate::card::CardElement) -> String {
545 String::new()
546 }
547
548 fn render_message(&self, _message: &crate::message::AdapterPostableMessage) -> String {
549 String::new()
550 }
551
552 async fn recv_event(&mut self) -> Option<ChatEvent> {
553 None
554 }
555}
556
557impl std::fmt::Debug for ChatBot {
558 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
559 f.debug_struct("ChatBot")
560 .field("adapters", &self.adapters.len())
561 .field("config", &self.config)
562 .field("mention_handlers", &self.mention_handlers.len())
563 .field("message_handlers", &self.message_handlers.len())
564 .field("subscribed_message_handlers", &self.subscribed_message_handlers.len())
565 .field("action_handlers", &self.action_handlers.len())
566 .field("reaction_handlers", &self.reaction_handlers.len())
567 .field("slash_command_handlers", &self.slash_command_handlers.len())
568 .field("modal_submit_handlers", &self.modal_submit_handlers.len())
569 .field("modal_close_handlers", &self.modal_close_handlers.len())
570 .finish_non_exhaustive()
571 }
572}
573
574const _: () = {
580 const fn assert_send_sync<T: Send + Sync>() {}
581 assert_send_sync::<ChatBot>();
582};
583
584#[cfg(test)]
589mod tests {
590 use std::{
591 collections::VecDeque,
592 sync::{
593 Mutex,
594 atomic::{AtomicUsize, Ordering},
595 },
596 };
597
598 use super::*;
599 use crate::message::{AdapterPostableMessage, Author, IncomingMessage, SentMessage};
600
601 struct MockAdapter {
607 name: &'static str,
608 events: Mutex<VecDeque<ChatEvent>>,
609 }
610
611 impl MockAdapter {
612 fn new(name: &'static str, events: Vec<ChatEvent>) -> Self {
613 Self { name, events: Mutex::new(VecDeque::from(events)) }
614 }
615 }
616
617 #[async_trait::async_trait]
618 impl ChatAdapter for MockAdapter {
619 fn name(&self) -> &str {
620 self.name
621 }
622
623 async fn post_message(
624 &self,
625 _thread_id: &str,
626 _message: &AdapterPostableMessage,
627 ) -> Result<SentMessage, ChatError> {
628 Ok(SentMessage {
629 id: "m1".into(),
630 thread_id: "t1".into(),
631 adapter_name: self.name.into(),
632 raw: None,
633 })
634 }
635
636 async fn edit_message(
637 &self,
638 _thread_id: &str,
639 _message_id: &str,
640 _message: &AdapterPostableMessage,
641 ) -> Result<SentMessage, ChatError> {
642 Ok(SentMessage {
643 id: "m1".into(),
644 thread_id: "t1".into(),
645 adapter_name: self.name.into(),
646 raw: None,
647 })
648 }
649
650 async fn delete_message(
651 &self,
652 _thread_id: &str,
653 _message_id: &str,
654 ) -> Result<(), ChatError> {
655 Ok(())
656 }
657
658 fn render_card(&self, _card: &crate::card::CardElement) -> String {
659 String::new()
660 }
661
662 fn render_message(&self, _message: &AdapterPostableMessage) -> String {
663 String::new()
664 }
665
666 async fn recv_event(&mut self) -> Option<ChatEvent> {
667 let Ok(mut q) = self.events.lock() else {
668 return None;
669 };
670 q.pop_front()
671 }
672 }
673
674 fn sample_author() -> Author {
679 Author {
680 user_id: "u1".into(),
681 user_name: "alice".into(),
682 full_name: "Alice".into(),
683 is_bot: false,
684 }
685 }
686
687 fn sample_message(text: &str) -> IncomingMessage {
688 IncomingMessage {
689 id: "m1".into(),
690 text: text.into(),
691 author: sample_author(),
692 attachments: vec![],
693 is_mention: false,
694 thread_id: "t1".into(),
695 timestamp: None,
696 }
697 }
698
699 #[test]
705 fn chatbot_is_send_sync() {
706 fn assert_send<T: Send>() {}
707 fn assert_sync<T: Sync>() {}
708 assert_send::<ChatBot>();
709 assert_sync::<ChatBot>();
710 }
711
712 #[test]
713 fn default_config() {
714 let cfg = ChatBotConfig::default();
715 assert_eq!(cfg.streaming_update_interval_ms, 500);
716 assert_eq!(cfg.fallback_streaming_placeholder, "...");
717 }
718
719 #[test]
720 fn new_chatbot_has_empty_handlers() {
721 let bot = ChatBot::new(ChatBotConfig::default());
722 assert!(bot.adapters.is_empty());
723 assert!(bot.mention_handlers.is_empty());
724 assert!(bot.message_handlers.is_empty());
725 assert!(bot.subscribed_message_handlers.is_empty());
726 assert!(bot.action_handlers.is_empty());
727 assert!(bot.reaction_handlers.is_empty());
728 assert!(bot.slash_command_handlers.is_empty());
729 assert!(bot.modal_submit_handlers.is_empty());
730 assert!(bot.modal_close_handlers.is_empty());
731 }
732
733 #[test]
734 fn register_mention_handler() {
735 let mut bot = ChatBot::new(ChatBotConfig::default());
736 bot.on_mention(|_thread, _msg| async {});
737 assert_eq!(bot.mention_handlers.len(), 1);
738 }
739
740 #[test]
741 fn register_message_handler_with_pattern() {
742 let mut bot = ChatBot::new(ChatBotConfig::default());
743 bot.on_message(Some("hello".into()), |_thread, _msg| async {});
744 assert_eq!(bot.message_handlers.len(), 1);
745 assert_eq!(bot.message_handlers[0].0.as_deref(), Some("hello"));
746 }
747
748 #[test]
749 fn register_message_handler_without_pattern() {
750 let mut bot = ChatBot::new(ChatBotConfig::default());
751 bot.on_message(None, |_thread, _msg| async {});
752 assert_eq!(bot.message_handlers.len(), 1);
753 assert!(bot.message_handlers[0].0.is_none());
754 }
755
756 #[test]
757 fn register_subscribed_message_handler() {
758 let mut bot = ChatBot::new(ChatBotConfig::default());
759 bot.on_subscribed_message(|_thread, _msg| async {});
760 assert_eq!(bot.subscribed_message_handlers.len(), 1);
761 }
762
763 #[test]
764 fn register_action_handler() {
765 let mut bot = ChatBot::new(ChatBotConfig::default());
766 bot.on_action(Some(vec!["btn_ok".into()]), |_action, _thread| async {});
767 assert_eq!(bot.action_handlers.len(), 1);
768 }
769
770 #[test]
771 fn register_reaction_handler() {
772 let mut bot = ChatBot::new(ChatBotConfig::default());
773 bot.on_reaction(None, |_reaction| async {});
774 assert_eq!(bot.reaction_handlers.len(), 1);
775 }
776
777 #[test]
778 fn register_slash_command_handler() {
779 let mut bot = ChatBot::new(ChatBotConfig::default());
780 bot.on_slash_command(Some(vec!["/deploy".into()]), |_cmd| async {});
781 assert_eq!(bot.slash_command_handlers.len(), 1);
782 }
783
784 #[test]
785 fn register_modal_submit_handler() {
786 let mut bot = ChatBot::new(ChatBotConfig::default());
787 bot.on_modal_submit(Some(vec!["feedback".into()]), |_submit| async {});
788 assert_eq!(bot.modal_submit_handlers.len(), 1);
789 }
790
791 #[test]
792 fn register_modal_close_handler() {
793 let mut bot = ChatBot::new(ChatBotConfig::default());
794 bot.on_modal_close(|_close| async {});
795 assert_eq!(bot.modal_close_handlers.len(), 1);
796 }
797
798 #[test]
799 fn register_multiple_handlers() {
800 let mut bot = ChatBot::new(ChatBotConfig::default());
801 bot.on_mention(|_t, _m| async {});
802 bot.on_mention(|_t, _m| async {});
803 bot.on_message(None, |_t, _m| async {});
804 bot.on_action(None, |_a, _t| async {});
805 assert_eq!(bot.mention_handlers.len(), 2);
806 assert_eq!(bot.message_handlers.len(), 1);
807 assert_eq!(bot.action_handlers.len(), 1);
808 }
809
810 #[tokio::test]
815 async fn run_returns_closed_when_no_adapters() {
816 let mut bot = ChatBot::new(ChatBotConfig::default());
817 let result = bot.run().await;
818 assert!(matches!(result, Err(ChatError::Closed)));
819 }
820
821 #[tokio::test]
822 async fn run_returns_ok_when_adapter_exhausted() {
823 let mut bot = ChatBot::new(ChatBotConfig::default());
824 bot.add_adapter(MockAdapter::new("mock", vec![]));
825 let result = bot.run().await;
826 assert!(result.is_ok());
827 }
828
829 #[tokio::test]
830 async fn dispatch_mention_event() {
831 let counter = Arc::new(AtomicUsize::new(0));
832 let c = Arc::clone(&counter);
833
834 let mut bot = ChatBot::new(ChatBotConfig::default());
835 bot.on_mention(move |_thread, _msg| {
836 let c = Arc::clone(&c);
837 async move {
838 c.fetch_add(1, Ordering::SeqCst);
839 }
840 });
841 bot.add_adapter(MockAdapter::new(
842 "mock",
843 vec![ChatEvent::Mention {
844 thread_id: "t1".into(),
845 message: sample_message("hey @bot"),
846 }],
847 ));
848
849 bot.run().await.expect("run should succeed");
850 assert_eq!(counter.load(Ordering::SeqCst), 1);
851 }
852
853 #[tokio::test]
854 async fn dispatch_message_with_pattern() {
855 let counter = Arc::new(AtomicUsize::new(0));
856 let c = Arc::clone(&counter);
857
858 let mut bot = ChatBot::new(ChatBotConfig::default());
859 bot.on_message(Some("hello".into()), move |_thread, _msg| {
860 let c = Arc::clone(&c);
861 async move {
862 c.fetch_add(1, Ordering::SeqCst);
863 }
864 });
865 bot.add_adapter(MockAdapter::new(
866 "mock",
867 vec![
868 ChatEvent::Message {
869 thread_id: "t1".into(),
870 message: sample_message("hello world"),
871 },
872 ChatEvent::Message {
873 thread_id: "t1".into(),
874 message: sample_message("goodbye world"),
875 },
876 ],
877 ));
878
879 bot.run().await.expect("run should succeed");
880 assert_eq!(counter.load(Ordering::SeqCst), 1);
882 }
883
884 #[tokio::test]
885 async fn dispatch_message_no_pattern_catches_all() {
886 let counter = Arc::new(AtomicUsize::new(0));
887 let c = Arc::clone(&counter);
888
889 let mut bot = ChatBot::new(ChatBotConfig::default());
890 bot.on_message(None, move |_thread, _msg| {
891 let c = Arc::clone(&c);
892 async move {
893 c.fetch_add(1, Ordering::SeqCst);
894 }
895 });
896 bot.add_adapter(MockAdapter::new(
897 "mock",
898 vec![
899 ChatEvent::Message { thread_id: "t1".into(), message: sample_message("anything") },
900 ChatEvent::Message { thread_id: "t1".into(), message: sample_message("else") },
901 ],
902 ));
903
904 bot.run().await.expect("run should succeed");
905 assert_eq!(counter.load(Ordering::SeqCst), 2);
906 }
907
908 #[tokio::test]
909 async fn subscribed_thread_routes_to_subscribed_handlers() {
910 let sub_counter = Arc::new(AtomicUsize::new(0));
911 let msg_counter = Arc::new(AtomicUsize::new(0));
912
913 let sc = Arc::clone(&sub_counter);
914 let mc = Arc::clone(&msg_counter);
915
916 let mut bot = ChatBot::new(ChatBotConfig::default());
917
918 let _ = bot.subscriptions.insert_async("t1".into(), ()).await;
920
921 bot.on_subscribed_message(move |_thread, _msg| {
922 let sc = Arc::clone(&sc);
923 async move {
924 sc.fetch_add(1, Ordering::SeqCst);
925 }
926 });
927 bot.on_message(None, move |_thread, _msg| {
928 let mc = Arc::clone(&mc);
929 async move {
930 mc.fetch_add(1, Ordering::SeqCst);
931 }
932 });
933
934 bot.add_adapter(MockAdapter::new(
935 "mock",
936 vec![
937 ChatEvent::Message {
938 thread_id: "t1".into(),
939 message: sample_message("in subscribed thread"),
940 },
941 ChatEvent::Message {
942 thread_id: "t2".into(),
943 message: sample_message("in non-subscribed thread"),
944 },
945 ],
946 ));
947
948 bot.run().await.expect("run should succeed");
949 assert_eq!(sub_counter.load(Ordering::SeqCst), 1);
950 assert_eq!(msg_counter.load(Ordering::SeqCst), 1);
951 }
952
953 #[tokio::test]
954 async fn dispatch_action_with_filter() {
955 let counter = Arc::new(AtomicUsize::new(0));
956 let c = Arc::clone(&counter);
957
958 let mut bot = ChatBot::new(ChatBotConfig::default());
959 bot.on_action(Some(vec!["approve".into()]), move |_action, _thread| {
960 let c = Arc::clone(&c);
961 async move {
962 c.fetch_add(1, Ordering::SeqCst);
963 }
964 });
965 bot.add_adapter(MockAdapter::new(
966 "mock",
967 vec![
968 ChatEvent::Action(ActionEvent {
969 action_id: "approve".into(),
970 thread_id: "t1".into(),
971 message_id: "m1".into(),
972 user: sample_author(),
973 value: None,
974 trigger_id: None,
975 adapter_name: "mock".into(),
976 }),
977 ChatEvent::Action(ActionEvent {
978 action_id: "reject".into(),
979 thread_id: "t1".into(),
980 message_id: "m1".into(),
981 user: sample_author(),
982 value: None,
983 trigger_id: None,
984 adapter_name: "mock".into(),
985 }),
986 ],
987 ));
988
989 bot.run().await.expect("run should succeed");
990 assert_eq!(counter.load(Ordering::SeqCst), 1);
992 }
993
994 #[tokio::test]
995 async fn dispatch_reaction_with_emoji_filter() {
996 let counter = Arc::new(AtomicUsize::new(0));
997 let c = Arc::clone(&counter);
998
999 let thumbs_up = EmojiValue::from_well_known(crate::emoji::WellKnownEmoji::ThumbsUp);
1000 let thumbs_down = EmojiValue::from_well_known(crate::emoji::WellKnownEmoji::ThumbsDown);
1001
1002 let mut bot = ChatBot::new(ChatBotConfig::default());
1003 bot.on_reaction(Some(vec![thumbs_up.clone()]), move |_reaction| {
1004 let c = Arc::clone(&c);
1005 async move {
1006 c.fetch_add(1, Ordering::SeqCst);
1007 }
1008 });
1009
1010 bot.add_adapter(MockAdapter::new(
1011 "mock",
1012 vec![
1013 ChatEvent::Reaction(ReactionEvent {
1014 thread_id: "t1".into(),
1015 message_id: "m1".into(),
1016 user: sample_author(),
1017 emoji: thumbs_up,
1018 added: true,
1019 adapter_name: "mock".into(),
1020 }),
1021 ChatEvent::Reaction(ReactionEvent {
1022 thread_id: "t1".into(),
1023 message_id: "m1".into(),
1024 user: sample_author(),
1025 emoji: thumbs_down,
1026 added: true,
1027 adapter_name: "mock".into(),
1028 }),
1029 ],
1030 ));
1031
1032 bot.run().await.expect("run should succeed");
1033 assert_eq!(counter.load(Ordering::SeqCst), 1);
1035 }
1036
1037 #[tokio::test]
1038 async fn dispatch_slash_command_with_filter() {
1039 let counter = Arc::new(AtomicUsize::new(0));
1040 let c = Arc::clone(&counter);
1041
1042 let mut bot = ChatBot::new(ChatBotConfig::default());
1043 bot.on_slash_command(Some(vec!["/deploy".into()]), move |_cmd| {
1044 let c = Arc::clone(&c);
1045 async move {
1046 c.fetch_add(1, Ordering::SeqCst);
1047 }
1048 });
1049 bot.add_adapter(MockAdapter::new(
1050 "mock",
1051 vec![
1052 ChatEvent::SlashCommand(SlashCommandEvent {
1053 command: "/deploy".into(),
1054 text: "prod".into(),
1055 channel_id: "c1".into(),
1056 user: sample_author(),
1057 trigger_id: None,
1058 adapter_name: "mock".into(),
1059 }),
1060 ChatEvent::SlashCommand(SlashCommandEvent {
1061 command: "/help".into(),
1062 text: "".into(),
1063 channel_id: "c1".into(),
1064 user: sample_author(),
1065 trigger_id: None,
1066 adapter_name: "mock".into(),
1067 }),
1068 ],
1069 ));
1070
1071 bot.run().await.expect("run should succeed");
1072 assert_eq!(counter.load(Ordering::SeqCst), 1);
1073 }
1074
1075 #[tokio::test]
1076 async fn dispatch_modal_submit_with_filter() {
1077 let counter = Arc::new(AtomicUsize::new(0));
1078 let c = Arc::clone(&counter);
1079
1080 let mut bot = ChatBot::new(ChatBotConfig::default());
1081 bot.on_modal_submit(Some(vec!["feedback".into()]), move |_submit| {
1082 let c = Arc::clone(&c);
1083 async move {
1084 c.fetch_add(1, Ordering::SeqCst);
1085 }
1086 });
1087 bot.add_adapter(MockAdapter::new(
1088 "mock",
1089 vec![
1090 ChatEvent::ModalSubmit(ModalSubmitEvent {
1091 callback_id: "feedback".into(),
1092 view_id: "v1".into(),
1093 user: sample_author(),
1094 values: std::collections::HashMap::new(),
1095 private_metadata: None,
1096 adapter_name: "mock".into(),
1097 }),
1098 ChatEvent::ModalSubmit(ModalSubmitEvent {
1099 callback_id: "other".into(),
1100 view_id: "v2".into(),
1101 user: sample_author(),
1102 values: std::collections::HashMap::new(),
1103 private_metadata: None,
1104 adapter_name: "mock".into(),
1105 }),
1106 ],
1107 ));
1108
1109 bot.run().await.expect("run should succeed");
1110 assert_eq!(counter.load(Ordering::SeqCst), 1);
1111 }
1112
1113 #[tokio::test]
1114 async fn dispatch_modal_close() {
1115 let counter = Arc::new(AtomicUsize::new(0));
1116 let c = Arc::clone(&counter);
1117
1118 let mut bot = ChatBot::new(ChatBotConfig::default());
1119 bot.on_modal_close(move |_close| {
1120 let c = Arc::clone(&c);
1121 async move {
1122 c.fetch_add(1, Ordering::SeqCst);
1123 }
1124 });
1125 bot.add_adapter(MockAdapter::new(
1126 "mock",
1127 vec![ChatEvent::ModalClose(ModalCloseEvent {
1128 callback_id: "feedback".into(),
1129 view_id: "v1".into(),
1130 user: sample_author(),
1131 adapter_name: "mock".into(),
1132 })],
1133 ));
1134
1135 bot.run().await.expect("run should succeed");
1136 assert_eq!(counter.load(Ordering::SeqCst), 1);
1137 }
1138
1139 #[tokio::test]
1140 async fn mention_in_subscribed_thread_routes_to_subscribed_handler() {
1141 let mention_counter = Arc::new(AtomicUsize::new(0));
1142 let sub_counter = Arc::new(AtomicUsize::new(0));
1143
1144 let mc = Arc::clone(&mention_counter);
1145 let sc = Arc::clone(&sub_counter);
1146
1147 let mut bot = ChatBot::new(ChatBotConfig::default());
1148
1149 let _ = bot.subscriptions.insert_async("t1".into(), ()).await;
1151
1152 bot.on_mention(move |_thread, _msg| {
1153 let mc = Arc::clone(&mc);
1154 async move {
1155 mc.fetch_add(1, Ordering::SeqCst);
1156 }
1157 });
1158 bot.on_subscribed_message(move |_thread, _msg| {
1159 let sc = Arc::clone(&sc);
1160 async move {
1161 sc.fetch_add(1, Ordering::SeqCst);
1162 }
1163 });
1164
1165 bot.add_adapter(MockAdapter::new(
1166 "mock",
1167 vec![ChatEvent::Mention {
1168 thread_id: "t1".into(),
1169 message: sample_message("hey @bot"),
1170 }],
1171 ));
1172
1173 bot.run().await.expect("run should succeed");
1174 assert_eq!(mention_counter.load(Ordering::SeqCst), 0);
1176 assert_eq!(sub_counter.load(Ordering::SeqCst), 1);
1177 }
1178
1179 #[tokio::test]
1180 async fn multiple_adapters_all_events_processed() {
1181 let counter = Arc::new(AtomicUsize::new(0));
1182 let c = Arc::clone(&counter);
1183
1184 let mut bot = ChatBot::new(ChatBotConfig::default());
1185 bot.on_message(None, move |_thread, _msg| {
1186 let c = Arc::clone(&c);
1187 async move {
1188 c.fetch_add(1, Ordering::SeqCst);
1189 }
1190 });
1191 bot.add_adapter(MockAdapter::new(
1192 "adapter-a",
1193 vec![ChatEvent::Message { thread_id: "t1".into(), message: sample_message("from A") }],
1194 ));
1195 bot.add_adapter(MockAdapter::new(
1196 "adapter-b",
1197 vec![ChatEvent::Message { thread_id: "t2".into(), message: sample_message("from B") }],
1198 ));
1199
1200 bot.run().await.expect("run should succeed");
1201 assert_eq!(counter.load(Ordering::SeqCst), 2);
1202 }
1203}