1use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10use serde_json::{Map, Value};
11
12#[derive(Debug, Clone, PartialEq, Deserialize)]
16#[non_exhaustive]
17pub struct EventCallback {
18 #[serde(default)]
20 pub team_id: Option<String>,
21 #[serde(default)]
23 pub api_app_id: Option<String>,
24 pub event_id: String,
26 #[serde(default)]
28 pub event_time: Option<i64>,
29 pub event: Value,
31 #[serde(skip)]
33 pub retry_num: Option<u32>,
34 #[serde(skip)]
36 pub retry_reason: Option<String>,
37}
38
39impl EventCallback {
40 #[must_use]
42 pub fn event_type(&self) -> &str {
43 self.event.get("type").and_then(Value::as_str).unwrap_or("")
44 }
45
46 pub fn parse<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
51 T::deserialize(&self.event)
52 }
53}
54
55#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
57#[serde(default)]
58#[non_exhaustive]
59pub struct MessageEvent {
60 #[serde(rename = "type")]
62 pub kind: String,
63 pub subtype: Option<String>,
65 pub channel: Option<String>,
67 pub channel_type: Option<String>,
69 pub user: Option<String>,
71 pub bot_id: Option<String>,
73 pub text: Option<String>,
75 pub ts: Option<String>,
77 pub thread_ts: Option<String>,
79}
80
81#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
85#[non_exhaustive]
86pub struct SlashCommand {
87 pub command: String,
89 #[serde(default)]
91 pub text: String,
92 #[serde(default)]
94 pub user_id: String,
95 #[serde(default)]
97 pub user_name: String,
98 #[serde(default)]
100 pub channel_id: String,
101 #[serde(default)]
103 pub channel_name: String,
104 #[serde(default)]
106 pub team_id: String,
107 #[serde(default)]
109 pub team_domain: String,
110 #[serde(default)]
112 pub enterprise_id: Option<String>,
113 #[serde(default)]
115 pub api_app_id: Option<String>,
116 #[serde(default)]
118 pub response_url: String,
119 #[serde(default)]
121 pub trigger_id: String,
122}
123
124#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
128#[non_exhaustive]
129pub struct Message {
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub text: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub blocks: Option<Vec<Value>>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub thread_ts: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub response_type: Option<String>,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub replace_original: Option<bool>,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub delete_original: Option<bool>,
148}
149
150impl Message {
151 #[must_use]
153 pub fn text(text: impl Into<String>) -> Self {
154 Self {
155 text: Some(text.into()),
156 ..Self::default()
157 }
158 }
159
160 #[must_use]
162 pub fn blocks(mut self, blocks: Vec<Value>) -> Self {
163 self.blocks = Some(blocks);
164 self
165 }
166
167 #[must_use]
169 pub fn in_thread(mut self, ts: impl Into<String>) -> Self {
170 self.thread_ts = Some(ts.into());
171 self
172 }
173
174 #[must_use]
176 pub fn ephemeral(mut self) -> Self {
177 self.response_type = Some("ephemeral".to_owned());
178 self
179 }
180
181 #[must_use]
183 pub fn in_channel(mut self) -> Self {
184 self.response_type = Some("in_channel".to_owned());
185 self
186 }
187
188 #[must_use]
190 pub const fn replace_original(mut self) -> Self {
191 self.replace_original = Some(true);
192 self
193 }
194
195 #[must_use]
197 pub const fn delete_original(mut self) -> Self {
198 self.delete_original = Some(true);
199 self
200 }
201
202 pub(crate) fn to_map(&self) -> Map<String, Value> {
204 match serde_json::to_value(self) {
205 Ok(Value::Object(m)) => m,
206 _ => Map::new(),
207 }
208 }
209}
210
211impl From<&str> for Message {
212 fn from(text: &str) -> Self {
213 Self::text(text)
214 }
215}
216
217impl From<String> for Message {
218 fn from(text: String) -> Self {
219 Self::text(text)
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
225#[non_exhaustive]
226pub enum CommandResponse {
227 Ack,
229 Ephemeral(Message),
231 InChannel(Message),
233}
234
235impl CommandResponse {
236 #[must_use]
238 pub fn ephemeral(msg: impl Into<Message>) -> Self {
239 Self::Ephemeral(msg.into())
240 }
241
242 #[must_use]
244 pub fn in_channel(msg: impl Into<Message>) -> Self {
245 Self::InChannel(msg.into())
246 }
247
248 #[must_use]
250 pub fn into_message(self) -> Option<Message> {
251 match self {
252 Self::Ack => None,
253 Self::Ephemeral(m) => Some(m.ephemeral()),
254 Self::InChannel(m) => Some(m.in_channel()),
255 }
256 }
257}
258
259#[derive(Debug, Clone, PartialEq)]
263#[non_exhaustive]
264pub struct BlockAction {
265 pub action_id: String,
267 pub block_id: Option<String>,
269 pub value: Option<String>,
271 pub action: Value,
273 pub user_id: Option<String>,
275 pub team_id: Option<String>,
277 pub channel_id: Option<String>,
279 pub message_ts: Option<String>,
281 pub response_url: Option<String>,
283 pub trigger_id: Option<String>,
285 pub payload: Value,
287}
288
289#[derive(Debug, Clone, PartialEq)]
291#[non_exhaustive]
292pub struct ViewPayload {
293 pub callback_id: String,
295 pub view_id: Option<String>,
297 pub private_metadata: Option<String>,
299 pub view: Value,
301 pub user_id: Option<String>,
303 pub team_id: Option<String>,
305 pub trigger_id: Option<String>,
307 pub payload: Value,
309}
310
311impl ViewPayload {
312 #[must_use]
314 pub fn state_values(&self) -> Option<&Value> {
315 self.view.get("state")?.get("values")
316 }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
321#[non_exhaustive]
322pub enum ViewResponse {
323 Close,
325 Clear,
327 Update(Value),
329 Push(Value),
331 Errors(BTreeMap<String, String>),
333}
334
335impl ViewResponse {
336 #[must_use]
338 pub fn errors<K: Into<String>, V: Into<String>>(
339 errors: impl IntoIterator<Item = (K, V)>,
340 ) -> Self {
341 Self::Errors(
342 errors
343 .into_iter()
344 .map(|(k, v)| (k.into(), v.into()))
345 .collect(),
346 )
347 }
348
349 #[must_use]
351 pub fn to_body(&self) -> Option<Value> {
352 let body = match self {
353 Self::Close => return None,
354 Self::Clear => serde_json::json!({"response_action": "clear"}),
355 Self::Update(view) => serde_json::json!({"response_action": "update", "view": view}),
356 Self::Push(view) => serde_json::json!({"response_action": "push", "view": view}),
357 Self::Errors(errors) => {
358 serde_json::json!({"response_action": "errors", "errors": errors})
359 }
360 };
361 Some(body)
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub enum ShortcutKind {
368 Global,
370 Message,
372}
373
374#[derive(Debug, Clone, PartialEq)]
376#[non_exhaustive]
377pub struct Shortcut {
378 pub kind: ShortcutKind,
380 pub callback_id: String,
382 pub trigger_id: String,
384 pub user_id: Option<String>,
386 pub team_id: Option<String>,
388 pub channel_id: Option<String>,
390 pub message: Option<Value>,
392 pub response_url: Option<String>,
394 pub payload: Value,
396}
397
398#[derive(Debug, Clone, PartialEq)]
400pub(crate) enum Interaction {
401 BlockActions(Vec<BlockAction>),
402 ViewSubmission(ViewPayload),
403 ViewClosed(ViewPayload),
404 Shortcut(Shortcut),
405 BlockSuggestion,
407 Other,
409}
410
411impl Interaction {
412 pub(crate) fn parse(payload: Value) -> Option<Self> {
414 let kind = payload.get("type")?.as_str()?.to_owned();
415 let user_id = str_at(&payload, &["user", "id"]);
416 let team_id =
418 str_at(&payload, &["team", "id"]).or_else(|| str_at(&payload, &["user", "team_id"]));
419 let trigger_id = str_at(&payload, &["trigger_id"]);
420 let response_url = str_at(&payload, &["response_url"]);
421 let channel_id = str_at(&payload, &["channel", "id"]);
422 match kind.as_str() {
423 "block_actions" => {
424 let actions = payload.get("actions")?.as_array()?;
425 let message_ts = str_at(&payload, &["container", "message_ts"])
426 .or_else(|| str_at(&payload, &["message", "ts"]));
427 let list = actions
428 .iter()
429 .map(|a| {
430 Some(BlockAction {
431 action_id: a.get("action_id")?.as_str()?.to_owned(),
432 block_id: str_at(a, &["block_id"]),
433 value: str_at(a, &["value"]),
434 action: a.clone(),
435 user_id: user_id.clone(),
436 team_id: team_id.clone(),
437 channel_id: channel_id.clone(),
438 message_ts: message_ts.clone(),
439 response_url: response_url.clone(),
440 trigger_id: trigger_id.clone(),
441 payload: payload.clone(),
442 })
443 })
444 .collect::<Option<Vec<_>>>()?;
445 Some(Self::BlockActions(list))
446 }
447 "view_submission" | "view_closed" => {
448 let view = payload.get("view")?.clone();
449 let v = ViewPayload {
450 callback_id: str_at(&view, &["callback_id"])?,
451 view_id: str_at(&view, &["id"]),
452 private_metadata: str_at(&view, &["private_metadata"]),
453 view,
454 user_id,
455 team_id,
456 trigger_id,
457 payload,
458 };
459 Some(if kind == "view_submission" {
460 Self::ViewSubmission(v)
461 } else {
462 Self::ViewClosed(v)
463 })
464 }
465 "shortcut" | "message_action" => Some(Self::Shortcut(Shortcut {
466 kind: if kind == "shortcut" {
467 ShortcutKind::Global
468 } else {
469 ShortcutKind::Message
470 },
471 callback_id: str_at(&payload, &["callback_id"])?,
472 trigger_id: trigger_id?,
473 user_id,
474 team_id,
475 channel_id,
476 message: payload.get("message").cloned(),
477 response_url,
478 payload,
479 })),
480 "block_suggestion" => Some(Self::BlockSuggestion),
481 _ => Some(Self::Other),
482 }
483 }
484}
485
486fn str_at(v: &Value, path: &[&str]) -> Option<String> {
488 let mut cur = v;
489 for key in path {
490 cur = cur.get(key)?;
491 }
492 cur.as_str().map(str::to_owned)
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use serde_json::json;
499
500 #[test]
501 fn message_serializes_only_set_fields() {
502 let m = Message::text("hi").in_thread("1.0").ephemeral();
503 assert_eq!(
504 serde_json::to_value(&m).unwrap(),
505 json!({"text": "hi", "thread_ts": "1.0", "response_type": "ephemeral"})
506 );
507 let m = Message::default().delete_original();
508 assert_eq!(m.to_map().len(), 1);
509 assert_eq!(Message::from(String::from("a")), Message::from("a"));
510 }
511
512 #[test]
513 fn command_response_sets_type() {
514 assert_eq!(CommandResponse::Ack.into_message(), None);
515 let m = CommandResponse::in_channel("x").into_message().unwrap();
516 assert_eq!(m.response_type.as_deref(), Some("in_channel"));
517 let m = CommandResponse::ephemeral(Message::text("y"))
518 .into_message()
519 .unwrap();
520 assert_eq!(m.response_type.as_deref(), Some("ephemeral"));
521 }
522
523 #[test]
524 fn view_response_bodies() {
525 assert_eq!(ViewResponse::Close.to_body(), None);
526 assert_eq!(
527 ViewResponse::Clear.to_body().unwrap(),
528 json!({"response_action": "clear"})
529 );
530 assert_eq!(
531 ViewResponse::Update(json!({"type": "modal"}))
532 .to_body()
533 .unwrap(),
534 json!({"response_action": "update", "view": {"type": "modal"}})
535 );
536 assert_eq!(
537 ViewResponse::errors([("b", "bad")]).to_body().unwrap(),
538 json!({"response_action": "errors", "errors": {"b": "bad"}})
539 );
540 }
541
542 #[test]
543 fn event_callback_parse() {
544 let ev: EventCallback = serde_json::from_value(json!({
545 "event_id": "E1", "event": {"type": "message", "text": "t", "channel": "C"}
546 }))
547 .unwrap();
548 assert_eq!(ev.event_type(), "message");
549 let m: MessageEvent = ev.parse().unwrap();
550 assert_eq!(m.text.as_deref(), Some("t"));
551 let ev2: EventCallback =
552 serde_json::from_value(json!({"event_id": "E2", "event": 5})).unwrap();
553 assert_eq!(ev2.event_type(), "");
554 }
555
556 #[test]
557 fn interaction_parse_kinds() {
558 let p = json!({"type": "block_actions", "actions": [{"action_id": "a"}, {"action_id": "b"}], "user": {"id": "U"}});
559 let Some(Interaction::BlockActions(acts)) = Interaction::parse(p) else {
560 panic!("not block actions")
561 };
562 assert_eq!(acts.len(), 2);
563 assert_eq!(acts[1].action_id, "b");
564 assert_eq!(acts[0].user_id.as_deref(), Some("U"));
565 let p = json!({"type": "block_actions", "actions": [{"action_id": "a"}], "container": {"type": "view"}});
567 let Some(Interaction::BlockActions(acts)) = Interaction::parse(p) else {
568 panic!()
569 };
570 assert!(acts[0].response_url.is_none() && acts[0].message_ts.is_none());
571 let p = json!({"type": "view_submission", "view": {"id": "V", "callback_id": "c", "private_metadata": "m"}});
572 let Some(Interaction::ViewSubmission(v)) = Interaction::parse(p) else {
573 panic!()
574 };
575 assert_eq!(
576 (
577 v.callback_id.as_str(),
578 v.view_id.as_deref(),
579 v.private_metadata.as_deref()
580 ),
581 ("c", Some("V"), Some("m"))
582 );
583 assert!(matches!(
584 Interaction::parse(json!({"type": "view_closed", "view": {"callback_id": "c"}})),
585 Some(Interaction::ViewClosed(_))
586 ));
587 assert!(matches!(
588 Interaction::parse(json!({"type": "block_suggestion"})),
589 Some(Interaction::BlockSuggestion)
590 ));
591 assert!(matches!(
592 Interaction::parse(json!({"type": "new_kind"})),
593 Some(Interaction::Other)
594 ));
595 let p = json!({"type": "view_closed", "team": null, "user": {"id": "U", "team_id": "T9"}, "view": {"callback_id": "c"}});
597 let Some(Interaction::ViewClosed(v)) = Interaction::parse(p) else {
598 panic!()
599 };
600 assert_eq!(v.team_id.as_deref(), Some("T9"));
601 assert_eq!(Interaction::parse(json!({"type": "view_submission"})), None);
603 assert_eq!(Interaction::parse(json!({"type": "block_actions"})), None);
604 assert_eq!(
605 Interaction::parse(json!({"type": "shortcut", "callback_id": "x"})),
606 None
607 );
608 assert_eq!(Interaction::parse(json!({"no": "type"})), None);
609 assert_eq!(Interaction::parse(json!([1])), None);
610 }
611
612 #[test]
613 fn slash_command_needs_command() {
614 let c: SlashCommand = serde_urlencoded::from_str("command=%2Fa&text=b").unwrap();
615 assert_eq!((c.command.as_str(), c.text.as_str()), ("/a", "b"));
616 assert!(serde_urlencoded::from_str::<SlashCommand>("text=b").is_err());
617 }
618}