Skip to main content

mur_chat/
platform.rs

1//! Platform trait — abstraction for different chat platforms.
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5
6/// Supported chat platforms.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum Platform {
10    Slack,
11    Telegram,
12    Discord,
13}
14
15/// Incoming message from any platform.
16#[derive(Debug, Clone)]
17pub struct IncomingMessage {
18    pub platform: Platform,
19    pub channel_id: String,
20    pub user_id: String,
21    pub user_name: String,
22    pub text: String,
23    pub thread_id: Option<String>,
24    pub message_id: String,
25}
26
27/// Outgoing message to send.
28#[derive(Debug, Clone)]
29pub struct OutgoingMessage {
30    pub channel_id: String,
31    pub text: String,
32    pub thread_id: Option<String>,
33    pub blocks: Option<serde_json::Value>, // Platform-specific rich content
34}
35
36/// Approval request sent to chat.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ApprovalRequest {
39    pub execution_id: String,
40    pub step_name: String,
41    pub description: String,
42    pub action: String,
43    /// Optional list of user IDs permitted to approve or deny this request.
44    /// When empty, any user can approve (the platform's existing behavior).
45    #[serde(default)]
46    pub allowed_approvers: Vec<String>,
47}
48
49/// Progress update during workflow execution.
50#[derive(Debug, Clone, Serialize)]
51pub struct ProgressUpdate {
52    pub execution_id: String,
53    pub workflow_id: String,
54    pub step_index: usize,
55    pub total_steps: usize,
56    pub step_name: String,
57    pub status: ProgressStatus,
58    pub output: Option<String>,
59    pub duration_ms: Option<u64>,
60}
61
62/// Status of a progress update.
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
64#[serde(rename_all = "snake_case")]
65pub enum ProgressStatus {
66    Started,
67    StepRunning,
68    StepDone,
69    StepFailed,
70    Completed,
71    Failed,
72}
73
74/// Workflow completion notification.
75#[derive(Debug, Clone, Serialize)]
76pub struct WorkflowNotification {
77    pub execution_id: String,
78    pub workflow_id: String,
79    pub success: bool,
80    pub steps_completed: usize,
81    pub total_steps: usize,
82    pub duration_ms: u64,
83    pub error: Option<String>,
84}
85
86/// Interaction payload from a button click (approve/deny).
87#[derive(Debug, Clone, Deserialize)]
88pub struct InteractionPayload {
89    pub action_id: String,
90    pub execution_id: String,
91    pub user_id: String,
92    pub user_name: String,
93}
94
95/// Chat platform interface.
96#[allow(async_fn_in_trait)]
97pub trait ChatPlatform: Send + Sync {
98    /// Send a message to a channel.
99    async fn send_message(&self, msg: &OutgoingMessage) -> Result<String>;
100
101    /// Send an approval request with interactive buttons.
102    async fn send_approval(&self, channel_id: &str, request: &ApprovalRequest) -> Result<String>;
103
104    /// Update an existing message.
105    async fn update_message(&self, channel_id: &str, message_id: &str, text: &str) -> Result<()>;
106
107    /// React to a message with an emoji.
108    async fn add_reaction(&self, channel_id: &str, message_id: &str, emoji: &str) -> Result<()>;
109
110    /// Send a progress update (step started/completed) in a thread.
111    async fn send_progress(
112        &self,
113        channel_id: &str,
114        thread_id: &str,
115        progress: &ProgressUpdate,
116    ) -> Result<String>;
117
118    /// Send a workflow completion/failure notification.
119    async fn send_notification(
120        &self,
121        channel_id: &str,
122        thread_id: Option<&str>,
123        notification: &WorkflowNotification,
124    ) -> Result<String>;
125
126    /// Start a new thread for a workflow execution.
127    /// Returns the thread ID (message timestamp for Slack).
128    async fn start_thread(
129        &self,
130        channel_id: &str,
131        execution_id: &str,
132        workflow_id: &str,
133        total_steps: usize,
134        shadow: bool,
135    ) -> Result<String>;
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_platform_serialization() {
144        let p = Platform::Slack;
145        let json = serde_json::to_string(&p).unwrap();
146        assert_eq!(json, "\"slack\"");
147    }
148
149    #[test]
150    fn test_progress_status_serialization() {
151        let s = ProgressStatus::StepRunning;
152        let json = serde_json::to_string(&s).unwrap();
153        assert_eq!(json, "\"step_running\"");
154    }
155
156    #[test]
157    fn test_progress_update() {
158        let update = ProgressUpdate {
159            execution_id: "e1".into(),
160            workflow_id: "w1".into(),
161            step_index: 0,
162            total_steps: 3,
163            step_name: "build".into(),
164            status: ProgressStatus::StepRunning,
165            output: None,
166            duration_ms: None,
167        };
168        let json = serde_json::to_string(&update).unwrap();
169        assert!(json.contains("step_running"));
170    }
171
172    #[test]
173    fn test_workflow_notification() {
174        let notif = WorkflowNotification {
175            execution_id: "e1".into(),
176            workflow_id: "deploy".into(),
177            success: true,
178            steps_completed: 3,
179            total_steps: 3,
180            duration_ms: 1500,
181            error: None,
182        };
183        let json = serde_json::to_string(&notif).unwrap();
184        assert!(json.contains("\"success\":true"));
185    }
186
187    #[test]
188    fn test_interaction_payload_deserialize() {
189        let json = r#"{"action_id":"approve","execution_id":"e1","user_id":"U123","user_name":"alice"}"#;
190        let payload: InteractionPayload = serde_json::from_str(json).unwrap();
191        assert_eq!(payload.action_id, "approve");
192        assert_eq!(payload.execution_id, "e1");
193    }
194}