1use serde::{Deserialize, Serialize};
2use serde_json::{Value, json};
3
4#[derive(Clone, Debug, Serialize, Deserialize)]
6pub struct Activity {
7 #[serde(default)]
8 pub(crate) kind: ActivityKind,
9 #[serde(default, skip_serializing_if = "Option::is_none")]
10 tenant: Option<String>,
11 #[serde(default, skip_serializing_if = "Option::is_none")]
12 pack_id: Option<String>,
13 #[serde(default, skip_serializing_if = "Option::is_none")]
14 flow_id: Option<String>,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
16 flow_type: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 session_id: Option<String>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 provider_id: Option<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
26 messaging_endpoint_id: Option<String>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 user_id: Option<String>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 channel_id: Option<String>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 conversation_id: Option<String>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
41 welcome_flow_hint: Option<WelcomeFlowHint>,
42 #[serde(default)]
43 payload: Value,
44}
45
46#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
74pub struct WelcomeFlowHint {
75 pub pack_id: String,
76 pub flow_id: String,
77}
78
79#[derive(Clone, Debug, Serialize, Deserialize, Default)]
80#[serde(tag = "kind", rename_all = "snake_case")]
81pub enum ActivityKind {
82 #[default]
84 Message,
85 Custom {
87 action: String,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 flow_type: Option<String>,
90 },
91}
92
93impl Activity {
94 pub fn text(text: impl Into<String>) -> Self {
96 Self {
97 kind: ActivityKind::Message,
98 tenant: None,
99 pack_id: None,
100 flow_id: None,
101 flow_type: Some("messaging".into()),
102 session_id: None,
103 provider_id: None,
104 messaging_endpoint_id: None,
105 user_id: None,
106 channel_id: None,
107 conversation_id: None,
108 welcome_flow_hint: None,
109 payload: json!({ "text": text.into() }),
110 }
111 }
112
113 pub fn custom(action: impl Into<String>, payload: Value) -> Self {
115 Self {
116 kind: ActivityKind::Custom {
117 action: action.into(),
118 flow_type: None,
119 },
120 tenant: None,
121 pack_id: None,
122 flow_id: None,
123 flow_type: None,
124 session_id: None,
125 provider_id: None,
126 messaging_endpoint_id: None,
127 user_id: None,
128 channel_id: None,
129 conversation_id: None,
130 welcome_flow_hint: None,
131 payload,
132 }
133 }
134
135 pub fn with_welcome_flow_hint(mut self, hint: WelcomeFlowHint) -> Self {
141 self.welcome_flow_hint = Some(hint);
142 self
143 }
144
145 pub fn welcome_flow_hint(&self) -> Option<&WelcomeFlowHint> {
147 self.welcome_flow_hint.as_ref()
148 }
149
150 pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
152 self.tenant = Some(tenant.into());
153 self
154 }
155
156 pub fn with_flow(mut self, flow_id: impl Into<String>) -> Self {
158 self.flow_id = Some(flow_id.into());
159 self
160 }
161
162 pub fn with_pack(mut self, pack_id: impl Into<String>) -> Self {
164 self.pack_id = Some(pack_id.into());
165 self
166 }
167
168 pub fn with_flow_type(mut self, flow_type: impl Into<String>) -> Self {
170 let flow_type = flow_type.into();
171 self.flow_type = Some(flow_type.clone());
172 if let ActivityKind::Custom {
173 flow_type: inner, ..
174 } = &mut self.kind
175 {
176 *inner = Some(flow_type);
177 }
178 self
179 }
180
181 #[cfg(feature = "greentic-x-provider")]
186 pub(crate) fn with_payload_field(mut self, key: impl Into<String>, value: Value) -> Self {
187 match &mut self.payload {
188 Value::Object(object) => {
189 object.insert(key.into(), value);
190 }
191 existing => {
192 let original = std::mem::replace(existing, Value::Null);
193 *existing = json!({
194 key.into(): value,
195 "value": original,
196 });
197 }
198 }
199 self
200 }
201
202 pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
204 self.session_id = Some(session_id.into());
205 self
206 }
207
208 pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
210 self.provider_id = Some(provider.into());
211 self
212 }
213
214 pub fn with_messaging_endpoint(mut self, endpoint_id: impl Into<String>) -> Self {
219 self.messaging_endpoint_id = Some(endpoint_id.into());
220 self
221 }
222
223 pub fn from_user(mut self, user: impl Into<String>) -> Self {
225 self.user_id = Some(user.into());
226 self
227 }
228
229 pub fn in_channel(mut self, channel: impl Into<String>) -> Self {
231 self.channel_id = Some(channel.into());
232 self
233 }
234
235 pub fn in_conversation(mut self, conversation: impl Into<String>) -> Self {
237 self.conversation_id = Some(conversation.into());
238 self
239 }
240
241 pub fn tenant(&self) -> Option<&str> {
243 self.tenant.as_deref()
244 }
245
246 pub fn pack_id(&self) -> Option<&str> {
248 self.pack_id.as_deref()
249 }
250
251 pub fn flow_id(&self) -> Option<&str> {
253 self.flow_id.as_deref()
254 }
255
256 pub fn flow_type(&self) -> Option<&str> {
258 self.flow_type
259 .as_deref()
260 .or_else(|| self.kind.flow_type_hint())
261 }
262
263 pub fn session_id(&self) -> Option<&str> {
265 self.session_id.as_deref()
266 }
267
268 pub fn provider_id(&self) -> Option<&str> {
270 self.provider_id.as_deref()
271 }
272
273 pub fn messaging_endpoint_id(&self) -> Option<&str> {
275 self.messaging_endpoint_id.as_deref()
276 }
277
278 pub fn user(&self) -> Option<&str> {
280 self.user_id.as_deref()
281 }
282
283 pub fn channel(&self) -> Option<&str> {
285 self.channel_id.as_deref()
286 }
287
288 pub fn conversation(&self) -> Option<&str> {
290 self.conversation_id.as_deref()
291 }
292
293 pub fn payload(&self) -> &Value {
295 &self.payload
296 }
297
298 pub(crate) fn action(&self) -> Option<&str> {
299 self.kind.action_hint()
300 }
301
302 pub(crate) fn into_payload(self) -> Value {
303 self.payload
304 }
305
306 pub(crate) fn ensure_tenant(mut self, tenant: &str) -> Self {
307 if self.tenant.is_none() {
308 self.tenant = Some(tenant.to_string());
309 }
310 self
311 }
312
313 pub(crate) fn from_output(payload: Value, tenant: &str) -> Self {
314 Activity::custom("response", payload).ensure_tenant(tenant)
315 }
316}
317
318impl ActivityKind {
319 fn flow_type_hint(&self) -> Option<&str> {
320 match self {
321 ActivityKind::Message => Some("messaging"),
322 ActivityKind::Custom { flow_type, .. } => flow_type.as_deref(),
323 }
324 }
325
326 fn action_hint(&self) -> Option<&str> {
327 match self {
328 ActivityKind::Message => Some("messaging"),
329 ActivityKind::Custom { action, .. } => Some(action.as_str()),
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn with_messaging_endpoint_sets_field() {
340 let activity = Activity::text("hi").with_messaging_endpoint("teams-legal");
341 assert_eq!(activity.messaging_endpoint_id(), Some("teams-legal"));
342 }
343
344 #[cfg(feature = "greentic-x-provider")]
345 #[test]
346 fn with_payload_field_adds_field_to_object_payload() {
347 let activity = Activity::text("show traffic")
348 .with_payload_field("fast2flow", json!({"entities": [{"kind": "date"}]}));
349
350 assert_eq!(
351 activity.payload()["fast2flow"]["entities"][0]["kind"],
352 "date"
353 );
354 assert_eq!(activity.payload()["text"], "show traffic");
355 }
356
357 #[test]
358 fn messaging_endpoint_id_defaults_to_none() {
359 let activity = Activity::text("hi");
360 assert!(activity.messaging_endpoint_id().is_none());
361 }
362
363 #[test]
364 fn messaging_endpoint_id_round_trips_through_serde() {
365 let original = Activity::text("hi").with_messaging_endpoint("teams-legal");
366 let encoded = serde_json::to_string(&original).expect("serialize");
367 assert!(encoded.contains("\"messaging_endpoint_id\":\"teams-legal\""));
368 let decoded: Activity = serde_json::from_str(&encoded).expect("deserialize");
369 assert_eq!(decoded.messaging_endpoint_id(), Some("teams-legal"));
370 }
371
372 #[test]
373 fn messaging_endpoint_id_serde_skips_when_unset() {
374 let encoded = serde_json::to_string(&Activity::text("hi")).expect("serialize");
378 assert!(!encoded.contains("messaging_endpoint_id"));
379 let decoded: Activity = serde_json::from_str(&encoded).expect("deserialize");
380 assert!(decoded.messaging_endpoint_id().is_none());
381 }
382}