Skip to main content

bob_chat/
event.rs

1//! Chat event types for adapter-to-bot communication.
2//!
3//! Events represent incoming payloads from chat adapters: messages, mentions,
4//! reactions, interactive actions, slash commands, and modal interactions.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    emoji::EmojiValue,
12    message::{Author, IncomingMessage},
13};
14
15// ---------------------------------------------------------------------------
16// ActionEvent
17// ---------------------------------------------------------------------------
18
19/// An interactive element (button, menu, etc.) was activated by a user.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ActionEvent {
22    /// Identifier of the action element that was triggered.
23    pub action_id: String,
24    /// Thread where the action originated.
25    pub thread_id: String,
26    /// Message containing the interactive element.
27    pub message_id: String,
28    /// The user who performed the action.
29    pub user: Author,
30    /// Optional value associated with the action element.
31    pub value: Option<String>,
32    /// Trigger identifier for opening modals or other follow-ups.
33    pub trigger_id: Option<String>,
34    /// Name of the adapter that delivered this event.
35    pub adapter_name: String,
36}
37
38// ---------------------------------------------------------------------------
39// ReactionEvent
40// ---------------------------------------------------------------------------
41
42/// A reaction was added to or removed from a message.
43#[derive(Debug, Clone)]
44pub struct ReactionEvent {
45    /// Thread containing the reacted message.
46    pub thread_id: String,
47    /// The message that was reacted to.
48    pub message_id: String,
49    /// The user who added or removed the reaction.
50    pub user: Author,
51    /// The emoji used for the reaction.
52    pub emoji: EmojiValue,
53    /// `true` when the reaction was added, `false` when removed.
54    pub added: bool,
55    /// Name of the adapter that delivered this event.
56    pub adapter_name: String,
57}
58
59// ---------------------------------------------------------------------------
60// SlashCommandEvent
61// ---------------------------------------------------------------------------
62
63/// A slash command was invoked by a user.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct SlashCommandEvent {
66    /// The command name (e.g. `/deploy`).
67    pub command: String,
68    /// Any text following the command name.
69    pub text: String,
70    /// Channel where the command was issued.
71    pub channel_id: String,
72    /// The user who invoked the command.
73    pub user: Author,
74    /// Trigger identifier for opening modals or other follow-ups.
75    pub trigger_id: Option<String>,
76    /// Name of the adapter that delivered this event.
77    pub adapter_name: String,
78}
79
80// ---------------------------------------------------------------------------
81// ModalSubmitEvent
82// ---------------------------------------------------------------------------
83
84/// A modal view was submitted by a user.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ModalSubmitEvent {
87    /// Callback identifier used to route this submission.
88    pub callback_id: String,
89    /// Platform-assigned view identifier.
90    pub view_id: String,
91    /// The user who submitted the modal.
92    pub user: Author,
93    /// Key-value pairs from the modal's input elements.
94    pub values: HashMap<String, String>,
95    /// Opaque metadata string passed through from the modal open call.
96    pub private_metadata: Option<String>,
97    /// Name of the adapter that delivered this event.
98    pub adapter_name: String,
99}
100
101// ---------------------------------------------------------------------------
102// ModalCloseEvent
103// ---------------------------------------------------------------------------
104
105/// A modal view was closed (dismissed) by a user without submitting.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ModalCloseEvent {
108    /// Callback identifier used to route this close event.
109    pub callback_id: String,
110    /// Platform-assigned view identifier.
111    pub view_id: String,
112    /// The user who closed the modal.
113    pub user: Author,
114    /// Name of the adapter that delivered this event.
115    pub adapter_name: String,
116}
117
118// ---------------------------------------------------------------------------
119// ChatEvent
120// ---------------------------------------------------------------------------
121
122/// Top-level event dispatched from a chat adapter to the bot.
123///
124/// Covers all supported interaction types: plain messages, mentions,
125/// reactions, interactive actions, slash commands, and modal lifecycle.
126#[derive(Debug, Clone)]
127pub enum ChatEvent {
128    /// A regular message was posted in a channel or thread.
129    Message {
130        /// Thread the message belongs to.
131        thread_id: String,
132        /// The received message.
133        message: IncomingMessage,
134    },
135    /// The bot was explicitly mentioned in a message.
136    Mention {
137        /// Thread the message belongs to.
138        thread_id: String,
139        /// The received message containing the mention.
140        message: IncomingMessage,
141    },
142    /// A reaction was added or removed.
143    Reaction(ReactionEvent),
144    /// An interactive element was activated.
145    Action(ActionEvent),
146    /// A slash command was invoked.
147    SlashCommand(SlashCommandEvent),
148    /// A modal was submitted.
149    ModalSubmit(ModalSubmitEvent),
150    /// A modal was closed without submitting.
151    ModalClose(ModalCloseEvent),
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    fn sample_author() -> Author {
159        Author {
160            user_id: "u1".into(),
161            user_name: "alice".into(),
162            full_name: "Alice".into(),
163            is_bot: false,
164        }
165    }
166
167    fn sample_incoming() -> IncomingMessage {
168        IncomingMessage {
169            id: "m1".into(),
170            text: "hello".into(),
171            author: sample_author(),
172            attachments: vec![],
173            is_mention: false,
174            thread_id: "t1".into(),
175            timestamp: None,
176        }
177    }
178
179    #[test]
180    fn chat_event_message_variant() {
181        let event = ChatEvent::Message { thread_id: "t1".into(), message: sample_incoming() };
182        assert!(matches!(event, ChatEvent::Message { .. }));
183    }
184
185    #[test]
186    fn chat_event_mention_variant() {
187        let event = ChatEvent::Mention { thread_id: "t1".into(), message: sample_incoming() };
188        assert!(matches!(event, ChatEvent::Mention { .. }));
189    }
190
191    #[test]
192    fn chat_event_action_variant() {
193        let event = ChatEvent::Action(ActionEvent {
194            action_id: "btn_approve".into(),
195            thread_id: "t1".into(),
196            message_id: "m1".into(),
197            user: sample_author(),
198            value: Some("yes".into()),
199            trigger_id: None,
200            adapter_name: "slack".into(),
201        });
202        assert!(matches!(event, ChatEvent::Action(_)));
203    }
204
205    #[test]
206    fn chat_event_slash_command_variant() {
207        let event = ChatEvent::SlashCommand(SlashCommandEvent {
208            command: "/deploy".into(),
209            text: "prod".into(),
210            channel_id: "c1".into(),
211            user: sample_author(),
212            trigger_id: None,
213            adapter_name: "slack".into(),
214        });
215        assert!(matches!(event, ChatEvent::SlashCommand(_)));
216    }
217
218    #[test]
219    fn chat_event_modal_submit_variant() {
220        let event = ChatEvent::ModalSubmit(ModalSubmitEvent {
221            callback_id: "feedback".into(),
222            view_id: "v1".into(),
223            user: sample_author(),
224            values: HashMap::from([("rating".into(), "5".into())]),
225            private_metadata: None,
226            adapter_name: "slack".into(),
227        });
228        assert!(matches!(event, ChatEvent::ModalSubmit(_)));
229    }
230
231    #[test]
232    fn chat_event_modal_close_variant() {
233        let event = ChatEvent::ModalClose(ModalCloseEvent {
234            callback_id: "feedback".into(),
235            view_id: "v1".into(),
236            user: sample_author(),
237            adapter_name: "slack".into(),
238        });
239        assert!(matches!(event, ChatEvent::ModalClose(_)));
240    }
241
242    #[test]
243    fn chat_event_clone_and_debug() {
244        let event = ChatEvent::Action(ActionEvent {
245            action_id: "a".into(),
246            thread_id: "t".into(),
247            message_id: "m".into(),
248            user: sample_author(),
249            value: None,
250            trigger_id: None,
251            adapter_name: "test".into(),
252        });
253        let cloned = event.clone();
254        // Verify Debug output is non-empty.
255        let dbg = format!("{cloned:?}");
256        assert!(!dbg.is_empty());
257    }
258}