Skip to main content

slack_morphism/api/
chat_stream.rs

1//!
2//! Support for Slack chat.startStream / chat.appendStream / chat.stopStream methods
3//!
4
5use rsb_derive::Builder;
6use serde::{Deserialize, Serialize};
7use serde_with::skip_serializing_none;
8use url::Url;
9
10use crate::api::SlackAgentSessionStatus;
11use crate::models::blocks::*;
12use crate::models::*;
13use crate::ratectl::*;
14use crate::SlackClientSession;
15use crate::{ClientResult, SlackClientHttpConnector};
16
17impl<'a, SCHC> SlackClientSession<'a, SCHC>
18where
19    SCHC: SlackClientHttpConnector + Send,
20{
21    ///
22    /// https://docs.slack.dev/reference/methods/chat.startStream
23    ///
24    pub async fn chat_start_stream(
25        &self,
26        req: &SlackApiChatStartStreamRequest,
27    ) -> ClientResult<SlackApiChatStartStreamResponse> {
28        self.http_session_api
29            .http_post("chat.startStream", req, Some(&SLACK_TIER2_METHOD_CONFIG))
30            .await
31    }
32
33    ///
34    /// https://docs.slack.dev/reference/methods/chat.appendStream
35    ///
36    pub async fn chat_append_stream(
37        &self,
38        req: &SlackApiChatAppendStreamRequest,
39    ) -> ClientResult<SlackApiChatAppendStreamResponse> {
40        self.http_session_api
41            .http_post("chat.appendStream", req, Some(&SLACK_TIER4_METHOD_CONFIG))
42            .await
43    }
44
45    ///
46    /// https://docs.slack.dev/reference/methods/chat.stopStream
47    ///
48    pub async fn chat_stop_stream(
49        &self,
50        req: &SlackApiChatStopStreamRequest,
51    ) -> ClientResult<SlackApiChatStopStreamResponse> {
52        self.http_session_api
53            .http_post("chat.stopStream", req, Some(&SLACK_TIER2_METHOD_CONFIG))
54            .await
55    }
56}
57
58#[skip_serializing_none]
59#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
60pub struct SlackApiChatStartStreamRequest {
61    pub channel: SlackChannelId,
62    pub thread_ts: Option<SlackTs>,
63    pub recipient_user_id: Option<SlackUserId>,
64    pub recipient_team_id: Option<SlackTeamId>,
65    pub markdown_text: Option<String>,
66    pub chunks: Option<Vec<SlackStreamChunk>>,
67    pub task_display_mode: Option<SlackStreamTaskDisplayMode>,
68    pub icon_emoji: Option<SlackEmoji>,
69    pub icon_url: Option<Url>,
70    pub username: Option<String>,
71}
72
73#[skip_serializing_none]
74#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
75pub struct SlackApiChatStartStreamResponse {
76    pub channel: SlackChannelId,
77    pub ts: SlackTs,
78}
79
80#[skip_serializing_none]
81#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
82pub struct SlackApiChatAppendStreamRequest {
83    pub channel: SlackChannelId,
84    pub ts: SlackTs,
85    pub markdown_text: Option<String>,
86    pub chunks: Option<Vec<SlackStreamChunk>>,
87}
88
89#[skip_serializing_none]
90#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
91pub struct SlackApiChatAppendStreamResponse {
92    pub channel: SlackChannelId,
93    pub ts: SlackTs,
94}
95
96#[skip_serializing_none]
97#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
98pub struct SlackApiChatStopStreamRequest {
99    pub channel: SlackChannelId,
100    pub ts: SlackTs,
101    #[serde(flatten)]
102    pub content: SlackMessageContent,
103    pub chunks: Option<Vec<SlackStreamChunk>>,
104    pub session_status: Option<SlackAgentSessionStatus>,
105}
106
107#[skip_serializing_none]
108#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
109pub struct SlackApiChatStopStreamResponse {
110    pub channel: SlackChannelId,
111    pub ts: SlackTs,
112    pub message: Option<SlackMessage>,
113}
114
115/// A streamed content chunk for `chat.startStream` / `chat.appendStream` / `chat.stopStream`.
116/// https://docs.slack.dev/reference/methods/chat.appendStream#chunks
117#[skip_serializing_none]
118#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
119#[serde(tag = "type", rename_all = "snake_case")]
120pub enum SlackStreamChunk {
121    MarkdownText {
122        text: String,
123    },
124    TaskUpdate {
125        id: SlackTaskId,
126        title: String,
127        hide_title: Option<bool>,
128        icon: Option<SlackTaskCardIcon>,
129        status: SlackTaskCardStatus,
130        details: Option<String>,
131        output: Option<String>,
132        /// Slack rejects a source without `text`.
133        sources: Option<Vec<SlackTaskCardSource>>,
134    },
135    PlanUpdate {
136        title: String,
137    },
138    Blocks {
139        blocks: Vec<SlackBlock>,
140    },
141}
142
143/// How task updates are rendered in a streamed message.
144/// https://docs.slack.dev/reference/methods/chat.startStream#arg_task_display_mode
145#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "snake_case")]
147pub enum SlackStreamTaskDisplayMode {
148    Timeline,
149    Plan,
150}
151
152#[cfg(test)]
153mod test {
154    use super::*;
155
156    #[test]
157    fn test_slack_api_chat_stream_task_update_chunk_round_trip() {
158        let json = serde_json::json!({
159            "type": "task_update",
160            "id": "t1",
161            "title": "Searching",
162            "hide_title": true,
163            "icon": { "type": "icon", "name": "check" },
164            "status": "in_progress",
165            "sources": [{ "type": "url", "url": "https://example.com/", "text": "Example" }]
166        });
167        let chunk: SlackStreamChunk = serde_json::from_value(json.clone()).unwrap();
168        assert_eq!(
169            chunk,
170            SlackStreamChunk::TaskUpdate {
171                id: "t1".into(),
172                title: "Searching".into(),
173                hide_title: Some(true),
174                icon: Some(SlackTaskCardIcon::new("check".into())),
175                status: SlackTaskCardStatus::InProgress,
176                details: None,
177                output: None,
178                sources: Some(vec![SlackTaskCardSource::Url(SlackUrlSourceElement::new(
179                    Url::parse("https://example.com").unwrap(),
180                    "Example".into()
181                ))]),
182            }
183        );
184        assert_eq!(serde_json::to_value(&chunk).unwrap(), json);
185    }
186}