Skip to main content

conversation_api/
interaction.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
4#[serde(tag = "type", rename_all = "snake_case")]
5#[serde(deny_unknown_fields)]
6pub enum PreviewValue {
7    Text { text: String },
8    TextList { items: Vec<String> },
9    Number { value: f64 },
10    Bool { value: bool },
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[serde(rename_all = "camelCase")]
15#[serde(deny_unknown_fields)]
16pub struct PreviewDetail {
17    pub label: String,
18    pub value: PreviewValue,
19}
20
21#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
22#[serde(rename_all = "snake_case")]
23pub enum InteractionTone {
24    #[default]
25    Normal,
26    Danger,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(
31    tag = "type",
32    rename_all = "snake_case",
33    rename_all_fields = "camelCase"
34)]
35#[serde(deny_unknown_fields)]
36pub enum ClientTask {
37    InstallIntegrations { integration_ids: Vec<String> },
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
41#[serde(rename_all = "camelCase")]
42#[serde(deny_unknown_fields)]
43pub struct InteractionPreview {
44    pub action_code: String,
45    pub title: String,
46    pub message: String,
47    #[serde(default)]
48    pub tone: InteractionTone,
49    #[serde(default)]
50    pub details: Vec<PreviewDetail>,
51    pub proceed_label: String,
52    pub reject_label: String,
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub client_task: Option<ClientTask>,
55}
56
57#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
58#[serde(rename_all = "snake_case")]
59pub enum InteractionKind {
60    #[default]
61    Approval,
62    UserAction,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66#[serde(rename_all = "camelCase")]
67pub struct PendingInteractionItem {
68    pub action_id: String,
69    pub kind: InteractionKind,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub expires_at: Option<String>,
72    pub preview: InteractionPreview,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76#[serde(rename_all = "camelCase")]
77pub struct PendingInteraction {
78    pub request_id: String,
79    pub batch_id: String,
80    pub title: String,
81    pub message: String,
82    pub items: Vec<PendingInteractionItem>,
83}
84
85#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
86#[serde(rename_all = "snake_case")]
87pub enum InteractionDecisionValue {
88    Approve,
89    Reject,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93#[serde(rename_all = "camelCase", deny_unknown_fields)]
94pub struct InteractionDecision {
95    pub action_id: String,
96    pub decision: InteractionDecisionValue,
97}
98
99impl Default for InteractionPreview {
100    fn default() -> Self {
101        Self::confirmation(
102            "interaction",
103            "Review action",
104            "Review this action before continuing.",
105        )
106    }
107}
108
109impl InteractionPreview {
110    #[must_use]
111    pub fn confirmation(
112        action_code: impl Into<String>,
113        title: impl Into<String>,
114        message: impl Into<String>,
115    ) -> Self {
116        Self {
117            action_code: action_code.into(),
118            title: title.into(),
119            message: message.into(),
120            tone: InteractionTone::Normal,
121            details: Vec::new(),
122            proceed_label: "Continue".to_owned(),
123            reject_label: "Cancel".to_owned(),
124            client_task: None,
125        }
126    }
127
128    /// Returns whether the facade supplied a complete display-only interaction contract.
129    #[must_use]
130    pub fn is_valid(&self) -> bool {
131        !self.action_code.trim().is_empty()
132            && !self.title.trim().is_empty()
133            && !self.proceed_label.trim().is_empty()
134            && !self.reject_label.trim().is_empty()
135            && self.details.iter().all(|detail| {
136                !detail.label.trim().is_empty()
137                    && match &detail.value {
138                        PreviewValue::Number { value } => value.is_finite(),
139                        PreviewValue::TextList { items } => {
140                            items.iter().all(|item| !item.trim().is_empty())
141                        }
142                        PreviewValue::Text { .. } | PreviewValue::Bool { .. } => true,
143                    }
144            })
145            && match &self.client_task {
146                Some(ClientTask::InstallIntegrations { integration_ids }) => {
147                    !integration_ids.is_empty()
148                        && integration_ids
149                            .iter()
150                            .all(|integration_id| !integration_id.trim().is_empty())
151                }
152                None => true,
153            }
154    }
155}