Skip to main content

slack_morphism/api/
canvases.rs

1//!
2//! Support for Slack canvases.* and conversations.canvases.create methods
3//!
4
5use rsb_derive::Builder;
6use serde::{Deserialize, Serialize};
7use serde_with::skip_serializing_none;
8
9use crate::models::*;
10use crate::ratectl::*;
11use crate::SlackClientSession;
12use crate::{ClientResult, SlackClientHttpConnector};
13
14impl<'a, SCHC> SlackClientSession<'a, SCHC>
15where
16    SCHC: SlackClientHttpConnector + Send,
17{
18    ///
19    /// https://docs.slack.dev/reference/methods/canvases.create
20    ///
21    pub async fn canvases_create(
22        &self,
23        req: &SlackApiCanvasesCreateRequest,
24    ) -> ClientResult<SlackApiCanvasesCreateResponse> {
25        self.http_session_api
26            .http_post("canvases.create", req, Some(&SLACK_TIER2_METHOD_CONFIG))
27            .await
28    }
29
30    ///
31    /// https://docs.slack.dev/reference/methods/canvases.edit
32    ///
33    pub async fn canvases_edit(
34        &self,
35        req: &SlackApiCanvasesEditRequest,
36    ) -> ClientResult<SlackApiCanvasesEditResponse> {
37        self.http_session_api
38            .http_post("canvases.edit", req, Some(&SLACK_TIER3_METHOD_CONFIG))
39            .await
40    }
41
42    ///
43    /// https://docs.slack.dev/reference/methods/canvases.delete
44    ///
45    pub async fn canvases_delete(
46        &self,
47        req: &SlackApiCanvasesDeleteRequest,
48    ) -> ClientResult<SlackApiCanvasesDeleteResponse> {
49        self.http_session_api
50            .http_post("canvases.delete", req, Some(&SLACK_TIER3_METHOD_CONFIG))
51            .await
52    }
53
54    ///
55    /// https://docs.slack.dev/reference/methods/canvases.sections.lookup
56    ///
57    pub async fn canvases_sections_lookup(
58        &self,
59        req: &SlackApiCanvasesSectionsLookupRequest,
60    ) -> ClientResult<SlackApiCanvasesSectionsLookupResponse> {
61        self.http_session_api
62            .http_post(
63                "canvases.sections.lookup",
64                req,
65                Some(&SLACK_TIER3_METHOD_CONFIG),
66            )
67            .await
68    }
69
70    ///
71    /// https://docs.slack.dev/reference/methods/canvases.access.set
72    ///
73    pub async fn canvases_access_set(
74        &self,
75        req: &SlackApiCanvasesAccessSetRequest,
76    ) -> ClientResult<SlackApiCanvasesAccessSetResponse> {
77        self.http_session_api
78            .http_post("canvases.access.set", req, Some(&SLACK_TIER3_METHOD_CONFIG))
79            .await
80    }
81
82    ///
83    /// https://docs.slack.dev/reference/methods/canvases.access.delete
84    ///
85    pub async fn canvases_access_delete(
86        &self,
87        req: &SlackApiCanvasesAccessDeleteRequest,
88    ) -> ClientResult<SlackApiCanvasesAccessDeleteResponse> {
89        self.http_session_api
90            .http_post(
91                "canvases.access.delete",
92                req,
93                Some(&SLACK_TIER3_METHOD_CONFIG),
94            )
95            .await
96    }
97
98    ///
99    /// https://docs.slack.dev/reference/methods/conversations.canvases.create
100    ///
101    pub async fn conversations_canvases_create(
102        &self,
103        req: &SlackApiConversationsCanvasesCreateRequest,
104    ) -> ClientResult<SlackApiConversationsCanvasesCreateResponse> {
105        self.http_session_api
106            .http_post(
107                "conversations.canvases.create",
108                req,
109                Some(&SLACK_TIER2_METHOD_CONFIG),
110            )
111            .await
112    }
113}
114
115/// Canvas content, serialised as `{"type":"markdown","markdown":"..."}`.
116/// Markdown is limited to 1 MiB; see the method pages for the supported elements.
117#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
118#[serde(tag = "type", rename = "markdown")]
119pub struct SlackCanvasDocumentContent {
120    pub markdown: String,
121}
122
123/// `channel_id` tabs the canvas in that channel and is required for free teams.
124#[skip_serializing_none]
125#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
126pub struct SlackApiCanvasesCreateRequest {
127    pub title: Option<String>,
128    pub document_content: Option<SlackCanvasDocumentContent>,
129    pub channel_id: Option<SlackChannelId>,
130}
131
132#[skip_serializing_none]
133#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
134pub struct SlackApiCanvasesCreateResponse {
135    pub canvas_id: SlackCanvasId,
136}
137
138/// `changes` currently takes exactly one change per call.
139#[skip_serializing_none]
140#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
141pub struct SlackApiCanvasesEditRequest {
142    pub canvas_id: SlackCanvasId,
143    pub changes: Vec<SlackCanvasChange>,
144}
145
146#[skip_serializing_none]
147#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
148pub struct SlackApiCanvasesEditResponse {}
149
150/// One edit. `insert_before` / `insert_after` / `delete` need `section_id`;
151/// every insert and `replace` need `document_content` (`replace` without a
152/// `section_id` replaces the whole canvas); `rename` takes `title_content`.
153#[skip_serializing_none]
154#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
155pub struct SlackCanvasChange {
156    pub operation: SlackCanvasOperation,
157    pub section_id: Option<SlackCanvasSectionId>,
158    pub document_content: Option<SlackCanvasDocumentContent>,
159    pub title_content: Option<SlackCanvasDocumentContent>,
160}
161
162#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
163#[serde(rename_all = "snake_case")]
164pub enum SlackCanvasOperation {
165    InsertAfter,
166    InsertBefore,
167    InsertAtStart,
168    InsertAtEnd,
169    Replace,
170    Delete,
171    Rename,
172}
173
174#[skip_serializing_none]
175#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
176pub struct SlackApiCanvasesDeleteRequest {
177    pub canvas_id: SlackCanvasId,
178}
179
180#[skip_serializing_none]
181#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
182pub struct SlackApiCanvasesDeleteResponse {}
183
184#[skip_serializing_none]
185#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
186pub struct SlackApiCanvasesSectionsLookupRequest {
187    pub canvas_id: SlackCanvasId,
188    pub criteria: SlackCanvasSectionsLookupCriteria,
189}
190
191/// Criteria combine: `section_types` narrows by kind, `contains_text` by content.
192#[skip_serializing_none]
193#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
194pub struct SlackCanvasSectionsLookupCriteria {
195    pub section_types: Option<Vec<SlackCanvasSectionType>>,
196    pub contains_text: Option<String>,
197}
198
199/// Section kinds that `canvases.sections.lookup` can filter on.
200/// https://docs.slack.dev/reference/methods/canvases.sections.lookup#usage-info
201#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
202#[serde(rename_all = "snake_case")]
203pub enum SlackCanvasSectionType {
204    AnyHeader,
205    Blockquote,
206    Callout,
207    CanvasUnfurl,
208    Chart,
209    Citation,
210    FileUnfurl,
211    Flexbox,
212    H1,
213    H2,
214    H3,
215    HorizontalLine,
216    List,
217    MessageUnfurl,
218    SfdcRecordMention,
219    SfdcRecordUnfurl,
220    Table,
221    UserMention,
222    UserUnfurl,
223    /// A kind this crate does not model yet, sent verbatim.
224    #[serde(untagged)]
225    Other(String),
226}
227
228#[skip_serializing_none]
229#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
230pub struct SlackApiCanvasesSectionsLookupResponse {
231    pub sections: Vec<SlackCanvasSection>,
232}
233
234#[skip_serializing_none]
235#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
236pub struct SlackCanvasSection {
237    pub id: SlackCanvasSectionId,
238}
239
240/// Exactly one of `channel_ids` / `user_ids` (1–20 entries). Only regular
241/// channels are accepted; use `user_ids` for DMs and MPDMs. `owner` is valid
242/// for users only.
243#[skip_serializing_none]
244#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
245pub struct SlackApiCanvasesAccessSetRequest {
246    pub canvas_id: SlackCanvasId,
247    pub access_level: SlackCanvasAccessLevel,
248    pub channel_ids: Option<Vec<SlackChannelId>>,
249    pub user_ids: Option<Vec<SlackUserId>>,
250}
251
252#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
253#[serde(rename_all = "snake_case")]
254pub enum SlackCanvasAccessLevel {
255    Read,
256    Write,
257    Owner,
258}
259
260#[skip_serializing_none]
261#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
262pub struct SlackApiCanvasesAccessSetResponse {}
263
264/// Exactly one of `channel_ids` / `user_ids` (a single entry each).
265#[skip_serializing_none]
266#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
267pub struct SlackApiCanvasesAccessDeleteRequest {
268    pub canvas_id: SlackCanvasId,
269    pub channel_ids: Option<Vec<SlackChannelId>>,
270    pub user_ids: Option<Vec<SlackUserId>>,
271}
272
273#[skip_serializing_none]
274#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
275pub struct SlackApiCanvasesAccessDeleteResponse {}
276
277/// A channel already holding a canvas answers `channel_canvas_already_exists`;
278/// its id is in `channel.properties.canvas` from `conversations.info`.
279#[skip_serializing_none]
280#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
281pub struct SlackApiConversationsCanvasesCreateRequest {
282    pub channel_id: SlackChannelId,
283    pub document_content: Option<SlackCanvasDocumentContent>,
284    pub title: Option<String>,
285}
286
287#[skip_serializing_none]
288#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
289pub struct SlackApiConversationsCanvasesCreateResponse {
290    pub canvas_id: SlackCanvasId,
291}
292
293#[cfg(test)]
294mod test {
295    use super::*;
296
297    /// Deserialises, re-serialises and compares; the envelope's `ok` is not
298    /// part of any response model, so it is dropped from the expectation.
299    fn round_trip<T>(payload: &str) -> T
300    where
301        T: Serialize + for<'de> Deserialize<'de>,
302    {
303        let mut expected: serde_json::Value = serde_json::from_str(payload).unwrap();
304        expected.as_object_mut().unwrap().remove("ok");
305        let model: T = serde_json::from_str(payload).unwrap();
306        assert_eq!(serde_json::to_value(&model).unwrap(), expected);
307        model
308    }
309
310    #[test]
311    fn test_slack_api_canvases_create_request_matches_docs_example() {
312        let req = SlackApiCanvasesCreateRequest::new()
313            .with_title("Your Brilliant Title".into())
314            .with_document_content(SlackCanvasDocumentContent::new(
315                "> standalone canvas!".into(),
316            ));
317        assert_eq!(
318            serde_json::to_value(&req).unwrap(),
319            serde_json::json!({
320                "title": "Your Brilliant Title",
321                "document_content": { "type": "markdown", "markdown": "> standalone canvas!" }
322            })
323        );
324        let resp: SlackApiCanvasesCreateResponse =
325            round_trip(r#"{ "ok": true, "canvas_id": "F1234ABCD" }"#);
326        assert_eq!(resp.canvas_id, "F1234ABCD".into());
327    }
328
329    #[test]
330    fn test_slack_api_canvases_edit_request_fixture() {
331        let req: SlackApiCanvasesEditRequest = round_trip(include_str!(
332            "./fixtures/slack_api_canvases_edit_request.json"
333        ));
334        assert_eq!(req.canvas_id, "F0166DCSTS7".into());
335        assert_eq!(req.changes.len(), 1);
336        assert_eq!(req.changes[0].operation, SlackCanvasOperation::InsertAfter);
337        assert_eq!(
338            req.changes[0].section_id,
339            Some("temp:C:VXX8e648e6984e441c6aa8c61173".into())
340        );
341        assert_eq!(
342            req.changes[0].document_content.as_ref().unwrap().markdown,
343            "- [ ] asparagus\n- [ ] coffee\n"
344        );
345
346        let rename = SlackCanvasChange::new(SlackCanvasOperation::Rename)
347            .with_title_content(SlackCanvasDocumentContent::new("Project Status".into()));
348        assert_eq!(
349            serde_json::to_value(&rename).unwrap(),
350            serde_json::json!({
351                "operation": "rename",
352                "title_content": { "type": "markdown", "markdown": "Project Status" }
353            })
354        );
355    }
356
357    #[test]
358    fn test_slack_api_canvases_sections_lookup_fixtures() {
359        let req: SlackApiCanvasesSectionsLookupRequest = round_trip(include_str!(
360            "./fixtures/slack_api_canvases_sections_lookup_request.json"
361        ));
362        assert_eq!(
363            req.criteria.section_types,
364            Some(vec![SlackCanvasSectionType::H1, SlackCanvasSectionType::H2])
365        );
366        assert_eq!(req.criteria.contains_text.as_deref(), Some("Grocery List"));
367
368        let future: SlackCanvasSectionType = serde_json::from_str(r#""hologram""#).unwrap();
369        assert_eq!(future, SlackCanvasSectionType::Other("hologram".into()));
370
371        let resp: SlackApiCanvasesSectionsLookupResponse = round_trip(include_str!(
372            "./fixtures/slack_api_canvases_sections_lookup_response.json"
373        ));
374        assert_eq!(
375            resp.sections[0].id,
376            "temp:C:eBa219af721c664422cb90a52fac".into()
377        );
378    }
379
380    #[test]
381    fn test_slack_api_canvases_access_requests_serialize_only_given_targets() {
382        let set = SlackApiCanvasesAccessSetRequest::new(
383            "F1234ABCD".into(),
384            SlackCanvasAccessLevel::Write,
385        )
386        .with_channel_ids(vec!["C1234ABCD".into()]);
387        assert_eq!(
388            serde_json::to_value(&set).unwrap(),
389            serde_json::json!({
390                "canvas_id": "F1234ABCD",
391                "access_level": "write",
392                "channel_ids": ["C1234ABCD"]
393            })
394        );
395
396        let delete = SlackApiCanvasesAccessDeleteRequest::new("F1234ABCD".into())
397            .with_user_ids(vec!["U1234ABCD".into()]);
398        assert_eq!(
399            serde_json::to_value(&delete).unwrap(),
400            serde_json::json!({ "canvas_id": "F1234ABCD", "user_ids": ["U1234ABCD"] })
401        );
402
403        let empty: SlackApiCanvasesAccessSetResponse = round_trip(r#"{ "ok": true }"#);
404        assert_eq!(empty, SlackApiCanvasesAccessSetResponse::new());
405    }
406
407    #[test]
408    fn test_slack_api_conversations_canvases_create_round_trip() {
409        let req = SlackApiConversationsCanvasesCreateRequest::new("C1234ABCD".into())
410            .with_document_content(SlackCanvasDocumentContent::new("> channel canvas!".into()));
411        assert_eq!(
412            serde_json::to_value(&req).unwrap(),
413            serde_json::json!({
414                "channel_id": "C1234ABCD",
415                "document_content": { "type": "markdown", "markdown": "> channel canvas!" }
416            })
417        );
418        let resp: SlackApiConversationsCanvasesCreateResponse =
419            round_trip(r#"{ "ok": true, "canvas_id": "F1234ABCD" }"#);
420        assert_eq!(resp.canvas_id, "F1234ABCD".into());
421    }
422}