1use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value};
4
5#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
6#[serde(rename_all = "snake_case")]
7pub enum InteractionTone {
8 #[default]
9 Normal,
10 Danger,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16#[serde(rename_all = "camelCase", deny_unknown_fields)]
17pub struct InteractionCard {
18 #[serde(rename = "type")]
19 pub card_type: String,
20 pub title: String,
21 pub message: String,
22 pub tone: InteractionTone,
23 pub continue_label: String,
24 pub cancel_label: String,
25 pub data: Map<String, Value>,
26}
27
28impl InteractionCard {
29 #[must_use]
30 pub fn new(
31 card_type: impl Into<String>,
32 title: impl Into<String>,
33 message: impl Into<String>,
34 ) -> Self {
35 Self {
36 card_type: card_type.into(),
37 title: title.into(),
38 message: message.into(),
39 tone: InteractionTone::Normal,
40 continue_label: "Continue".into(),
41 cancel_label: "Cancel".into(),
42 data: Map::new(),
43 }
44 }
45
46 #[must_use]
47 pub fn is_valid(&self) -> bool {
48 !self.card_type.is_empty()
49 && self
50 .card_type
51 .bytes()
52 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'.' || c == b'_')
53 && !self.title.trim().is_empty()
54 && !self.continue_label.trim().is_empty()
55 && !self.cancel_label.trim().is_empty()
56 }
57}
58
59#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
60#[serde(rename_all = "snake_case")]
61pub enum InteractionKind {
62 Approval,
63 UserAction,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
67#[serde(rename_all = "camelCase", deny_unknown_fields)]
68pub struct PendingInteractionItem {
69 pub action_id: String,
70 pub kind: InteractionKind,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub expires_at: Option<String>,
73 pub card: InteractionCard,
74}
75
76impl PendingInteractionItem {
77 #[must_use]
78 pub fn accepts(&self, response: &InteractionResponse) -> bool {
79 self.action_id == response.action_id
80 && response
81 .client_report
82 .as_ref()
83 .is_none_or(|report| self.kind == InteractionKind::UserAction && report.is_valid())
84 && matches!(
85 (self.kind, &response.response),
86 (
87 InteractionKind::Approval,
88 InteractionAnswer::Approve | InteractionAnswer::Reject
89 ) | (
90 InteractionKind::UserAction,
91 InteractionAnswer::Continue { .. } | InteractionAnswer::Cancel
92 )
93 )
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
98#[serde(rename_all = "camelCase", deny_unknown_fields)]
99pub struct PendingInteraction {
100 pub request_id: String,
101 pub batch_id: String,
102 pub title: String,
103 pub message: String,
104 pub items: Vec<PendingInteractionItem>,
105 pub answered_action_ids: Vec<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
112#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
113pub enum InteractionAnswer {
114 #[serde(deserialize_with = "crate::execution::deserialize_empty_variant")]
115 Approve,
116 #[serde(deserialize_with = "crate::execution::deserialize_empty_variant")]
117 Reject,
118 Continue {
119 data: Value,
120 },
121 #[serde(deserialize_with = "crate::execution::deserialize_empty_variant")]
122 Cancel,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
128#[serde(deny_unknown_fields)]
129pub struct ClientOperationWarning {
130 pub code: String,
131 pub message: String,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
135#[serde(deny_unknown_fields)]
136pub struct ClientOperationReport {
137 pub warnings: Vec<ClientOperationWarning>,
138}
139
140impl ClientOperationReport {
141 pub fn is_valid(&self) -> bool {
142 !self.warnings.is_empty()
143 && self.warnings.iter().all(|warning| {
144 !warning.code.trim().is_empty() && !warning.message.trim().is_empty()
145 })
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
150#[serde(rename_all = "camelCase", deny_unknown_fields)]
151pub struct InteractionResponse {
152 pub action_id: String,
153 pub response: InteractionAnswer,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub client_report: Option<ClientOperationReport>,
156}