1use rsb_derive::Builder;
2use serde::{Deserialize, Serialize};
3use serde_with::skip_serializing_none;
4use url::Url;
5
6use crate::blocks::*;
7use crate::events::*;
8use crate::models::messages::*;
9use crate::*;
10
11#[allow(clippy::large_enum_variant)]
12#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
13#[serde(tag = "type")]
14pub enum SlackPushEvent {
15 #[serde(rename = "url_verification")]
16 UrlVerification(SlackUrlVerificationEvent),
17 #[serde(rename = "event_callback")]
18 EventCallback(SlackPushEventCallback),
19 #[serde(rename = "app_rate_limited")]
20 AppRateLimited(SlackAppRateLimitedEvent),
21}
22
23#[skip_serializing_none]
24#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
25pub struct SlackUrlVerificationEvent {
26 pub challenge: String,
27}
28
29#[skip_serializing_none]
30#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
31pub struct SlackAppRateLimitedEvent {
32 pub team_id: String,
33 pub minute_rate_limited: SlackDateTime,
34 pub api_app_id: String,
35}
36
37#[skip_serializing_none]
38#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
39pub struct SlackPushEventCallback {
40 pub team_id: SlackTeamId,
41 pub api_app_id: SlackAppId,
42 pub event: SlackEventCallbackBody,
43 pub event_id: SlackEventId,
44 pub event_time: SlackDateTime,
45 pub event_context: Option<SlackEventContext>,
46 pub authed_users: Option<Vec<SlackUserId>>,
47 pub authorizations: Option<Vec<SlackEventAuthorization>>,
48}
49
50#[non_exhaustive]
53#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
54#[serde(tag = "type", rename_all = "snake_case")]
55pub enum SlackEventCallbackBody {
56 Message(SlackMessageEvent),
57 AppHomeOpened(SlackAppHomeOpenedEvent),
58 AppMention(SlackAppMentionEvent),
59 AppUninstalled(SlackAppUninstalledEvent),
60 LinkShared(SlackLinkSharedEvent),
61 EmojiChanged(SlackEmojiChangedEvent),
62 MemberJoinedChannel(SlackMemberJoinedChannelEvent),
63 MemberLeftChannel(SlackMemberLeftChannelEvent),
64 ChannelCreated(SlackChannelCreatedEvent),
65 ChannelDeleted(SlackChannelDeletedEvent),
66 ChannelArchive(SlackChannelArchiveEvent),
67 ChannelRename(SlackChannelRenameEvent),
68 ChannelUnarchive(SlackChannelUnarchiveEvent),
69 TeamJoin(SlackTeamJoinEvent),
70 FileCreated(SlackFileCreatedEvent),
71 FileChange(SlackFileChangedEvent),
72 FileDeleted(SlackFileDeletedEvent),
73 FileShared(SlackFileSharedEvent),
74 FileUnshared(SlackFileUnsharedEvent),
75 FilePublic(SlackFilePublicEvent),
76 ReactionAdded(SlackReactionAddedEvent),
77 ReactionRemoved(SlackReactionRemovedEvent),
78 StarAdded(SlackStarAddedEvent),
79 StarRemoved(SlackStarRemovedEvent),
80 UserChange(SlackUserChangeEvent),
81 UserStatusChanged(SlackUserStatusChangedEvent),
82 AssistantThreadStarted(SlackAssistantThreadStartedEvent),
83 AssistantThreadContextChanged(SlackAssistantThreadContextChangedEvent),
84 AgentSessionStopped(SlackAgentSessionStoppedEvent),
85 AgentSessionTitleChanged(SlackAgentSessionTitleChangedEvent),
86 AppContextChanged(SlackAppContextChangedEvent),
87 #[serde(untagged)]
89 Unknown(serde_json::Value),
90}
91
92#[skip_serializing_none]
93#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
94pub struct SlackMessageEvent {
95 #[serde(flatten)]
96 pub origin: SlackMessageOrigin,
97 #[serde(flatten)]
98 pub content: Option<SlackMessageContent>,
99 #[serde(flatten)]
100 pub sender: SlackMessageSender,
101 pub subtype: Option<SlackMessageEventType>,
102 pub hidden: Option<bool>,
103 pub message: Option<SlackMessageEventEdited>,
104 pub previous_message: Option<SlackMessageEventEdited>,
105 pub deleted_ts: Option<SlackTs>,
106}
107
108#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
109pub enum SlackMessageEventType {
110 #[serde(rename = "bot_message")]
111 BotMessage,
112 #[serde(rename = "me_message")]
113 MeMessage,
114 #[serde(rename = "channel_join")]
115 ChannelJoin,
116 #[serde(rename = "channel_leave")]
117 ChannelLeave,
118 #[serde(rename = "bot_add")]
119 BotAdd,
120 #[serde(rename = "bot_remove")]
121 BotRemove,
122 #[serde(rename = "channel_topic")]
123 ChannelTopic,
124 #[serde(rename = "channel_purpose")]
125 ChannelPurpose,
126 #[serde(rename = "channel_name")]
127 ChannelName,
128 #[serde(rename = "file_share")]
129 FileShare,
130 #[serde(rename = "message_changed")]
131 MessageChanged,
132 #[serde(rename = "message_deleted")]
133 MessageDeleted,
134 #[serde(rename = "thread_broadcast")]
135 ThreadBroadcast,
136 #[serde(rename = "tombstone")]
137 Tombstone,
138 #[serde(rename = "joiner_notification")]
139 JoinerNotification,
140 #[serde(rename = "slackbot_response")]
141 SlackbotResponse,
142 #[serde(rename = "emoji_changed")]
143 EmojiChanged,
144 #[serde(rename = "sh_room_created")]
145 SlackHuddleRoomCreated,
146 #[serde(rename = "channel_archive")]
147 ChannelArchive,
148 #[serde(rename = "channel_unarchive")]
149 ChannelUnarchive,
150 #[serde(rename = "app_conversation_leave")]
151 AppConversationLeave,
152 #[serde(rename = "bot_enable")]
153 BotEnable,
154 #[serde(rename = "bot_disable")]
155 BotDisable,
156 #[serde(rename = "pinned_item")]
157 PinnedItem,
158 #[serde(rename = "reminder_add")]
159 ReminderAdd,
160 #[serde(rename = "file_comment")]
161 FileComment,
162 #[serde(rename = "file_created")]
163 FileCreated,
164 #[serde(rename = "file_change")]
165 FileChanged,
166 #[serde(rename = "file_deleted")]
167 FileDeleted,
168 #[serde(rename = "file_shared")]
169 FileShared,
170 #[serde(rename = "file_unshared")]
171 FileUnshared,
172 #[serde(rename = "file_public")]
173 FilePublic,
174 #[serde(rename = "huddle_thread")]
175 HuddleThread,
176 #[serde(rename = "share_shortcut")]
177 ShareShortcut,
178 #[serde(rename = "channel_canvas_updated")]
179 ChannelCanvasUpdated,
180 #[serde(rename = "tabbed_canvas_updated")]
181 TabbedCanvasUpdated,
182 #[serde(rename = "channel_posting_permissions")]
183 ChannelPostingPermissions,
184 #[serde(untagged)]
187 Other(String),
188}
189
190#[skip_serializing_none]
191#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
192pub struct SlackAppHomeOpenedEvent {
193 pub user: SlackUserId,
194 pub channel: SlackChannelId,
195 pub tab: Option<String>,
196 pub view: Option<SlackView>,
197}
198
199#[skip_serializing_none]
200#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
201pub struct SlackAppMentionEvent {
202 pub user: SlackUserId,
203 pub channel: SlackChannelId,
204 #[serde(flatten)]
205 pub content: SlackMessageContent,
206 #[serde(flatten)]
207 pub origin: SlackMessageOrigin,
208 pub edited: Option<SlackMessageEdited>,
209}
210
211#[skip_serializing_none]
212#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
213pub struct SlackMessageEventEdited {
214 #[serde(flatten)]
215 pub content: Option<SlackMessageContent>,
216 #[serde(flatten)]
217 pub sender: SlackMessageSender,
218 pub ts: SlackTs,
219 pub edited: Option<SlackMessageEdited>,
220}
221
222#[skip_serializing_none]
223#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
224pub struct SlackAppUninstalledEvent {}
225
226#[skip_serializing_none]
227#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
228pub struct SlackLinkSharedEvent {
229 pub channel: SlackChannelId,
230 pub event_ts: SlackTs,
231 pub is_bot_user_member: bool,
232 pub links: Vec<SlackLinkObject>,
233 pub message_ts: SlackTs,
234 pub source: Option<String>,
235 pub unfurl_id: Option<SlackUnfurlId>,
236 pub user: SlackUserId,
237}
238
239#[skip_serializing_none]
240#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
241pub struct SlackEmojiChangedEvent {
242 pub subtype: SlackEmojiEventType,
243 pub name: Option<String>,
244 pub names: Option<Vec<String>>,
245 pub old_name: Option<String>,
246 pub new_name: Option<String>,
247 pub value: Option<Url>,
248 pub event_ts: SlackTs,
249}
250
251#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
252pub enum SlackEmojiEventType {
253 #[serde(rename = "remove")]
254 EmojiRemoved,
255 #[serde(rename = "add")]
256 EmojiAdded,
257 #[serde(rename = "rename")]
258 EmojiRenamed,
259}
260
261#[skip_serializing_none]
262#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
263pub struct SlackLinkObject {
264 pub domain: String,
265 pub url: Url,
266}
267
268#[skip_serializing_none]
269#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
270pub struct SlackMemberJoinedChannelEvent {
271 pub user: SlackUserId,
272 pub channel: SlackChannelId,
273 pub channel_type: SlackChannelType,
274 pub team: SlackTeamId,
275 pub inviter: Option<SlackUserId>,
276}
277
278#[skip_serializing_none]
279#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
280pub struct SlackMemberLeftChannelEvent {
281 pub user: SlackUserId,
282 pub channel: SlackChannelId,
283 pub channel_type: SlackChannelType,
284 pub team: SlackTeamId,
285}
286
287#[skip_serializing_none]
288#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
289pub struct SlackChannelCreatedEvent {
290 pub channel: SlackChannelInfo,
291}
292
293#[skip_serializing_none]
294#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
295pub struct SlackChannelDeletedEvent {
296 pub channel: SlackChannelId,
297}
298
299#[skip_serializing_none]
300#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
301pub struct SlackChannelArchiveEvent {
302 pub channel: SlackChannelId,
303 pub user: SlackUserId,
304}
305
306#[skip_serializing_none]
307#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
308pub struct SlackChannelRenameEvent {
309 pub channel: SlackChannelInfo,
310}
311
312#[skip_serializing_none]
313#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
314pub struct SlackChannelUnarchiveEvent {
315 pub channel: SlackChannelId,
316 pub user: SlackUserId,
317}
318
319#[skip_serializing_none]
320#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
321pub struct SlackTeamJoinEvent {
322 pub user: SlackUser,
323}
324
325#[skip_serializing_none]
326#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
327pub struct SlackFileCreatedEvent {
328 pub user_id: SlackUserId,
329 pub file_id: SlackFileId,
330 pub file: SlackFile,
331}
332
333#[skip_serializing_none]
334#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
335pub struct SlackFileChangedEvent {
336 pub file_id: SlackFileId,
337}
338
339#[skip_serializing_none]
340#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
341pub struct SlackFileDeletedEvent {
342 pub file_id: SlackFileId,
343 pub event_ts: SlackTs,
344}
345
346#[skip_serializing_none]
347#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
348pub struct SlackFileSharedEvent {
349 pub channel_id: SlackChannelId,
350 pub file_id: SlackFileId,
351 pub user_id: SlackUserId,
352 pub event_ts: SlackTs,
353}
354
355#[skip_serializing_none]
356#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
357pub struct SlackFileUnsharedEvent {
358 pub file_id: SlackFileId,
359}
360
361#[skip_serializing_none]
362#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
363pub struct SlackFilePublicEvent {
364 pub file_id: SlackFileId,
365}
366
367#[skip_serializing_none]
368#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
369pub struct SlackReactionAddedEvent {
370 pub user: SlackUserId,
371 pub reaction: SlackReactionName,
372 pub item_user: Option<SlackUserId>,
373 pub item: SlackReactionsItem,
374 pub event_ts: SlackTs,
375}
376
377#[skip_serializing_none]
378#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
379pub struct SlackReactionRemovedEvent {
380 pub user: SlackUserId,
381 pub reaction: SlackReactionName,
382 pub item_user: Option<SlackUserId>,
383 pub item: SlackReactionsItem,
384 pub event_ts: SlackTs,
385}
386
387#[skip_serializing_none]
388#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
389pub struct SlackStarAddedEvent {
390 pub user: SlackUserId,
391 pub item: SlackStarsItem,
392 pub event_ts: SlackTs,
393}
394
395#[skip_serializing_none]
396#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
397pub struct SlackStarRemovedEvent {
398 pub user: SlackUserId,
399 pub item: SlackStarsItem,
400 pub event_ts: SlackTs,
401}
402
403#[skip_serializing_none]
404#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
405pub struct SlackUserChangeEvent {
406 pub user: SlackUser,
407 pub event_ts: SlackTs,
408 pub cache_ts: SlackDateTime,
409}
410
411#[skip_serializing_none]
412#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
413pub struct SlackUserStatusChangedEvent {
414 pub user: SlackUser,
415 pub event_ts: SlackTs,
416 pub cache_ts: SlackDateTime,
417}
418
419#[skip_serializing_none]
420#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
421pub struct SlackAssistantThreadStartedEvent {
422 pub event_ts: SlackTs,
423 pub assistant_thread: SlackAssistantThread,
424}
425
426#[skip_serializing_none]
427#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
428pub struct SlackAssistantThreadContextChangedEvent {
429 pub event_ts: SlackTs,
430 pub assistant_thread: SlackAssistantThread,
431}
432
433#[skip_serializing_none]
435#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
436pub struct SlackAgentSessionStoppedEvent {
437 pub channel: SlackChannelId,
438 pub thread_ts: Option<SlackTs>,
439 pub streaming_message_ts: Option<Vec<SlackTs>>,
440 pub user: Option<SlackUserId>,
441 pub event_ts: Option<SlackTs>,
442}
443
444#[skip_serializing_none]
446#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
447pub struct SlackAgentSessionTitleChangedEvent {
448 pub channel: SlackChannelId,
449 pub thread_ts: Option<SlackTs>,
450 pub title: String,
451 pub previous_title: Option<String>,
452 pub user: Option<SlackUserId>,
453 pub team_id: Option<SlackTeamId>,
454 pub enterprise_id: Option<SlackEnterpriseId>,
456 pub event_ts: Option<SlackTs>,
457}
458
459#[skip_serializing_none]
464#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
465pub struct SlackAppContextChangedEvent {
466 pub context: SlackAppContext,
467}
468
469#[skip_serializing_none]
470#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
471pub struct SlackAppContext {
472 pub entities: Option<Vec<SlackAppContextEntity>>,
473}
474
475#[skip_serializing_none]
476#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
477pub struct SlackAppContextEntity {
478 #[serde(rename = "type")]
480 pub entity_type: SlackAppContextEntityType,
481 pub value: String,
482 pub team_id: Option<SlackTeamId>,
483}
484
485#[cfg(test)]
486mod test {
487 use super::*;
488
489 #[test]
490 fn test_slack_event_message_change_event() {
491 let payload = include_str!("./fixtures/message_changed.json");
492 let event: SlackPushEventCallback = serde_json::from_str(payload).unwrap();
493 match event.event {
494 SlackEventCallbackBody::Message(SlackMessageEvent {
495 subtype,
496 message,
497 previous_message,
498 ..
499 }) => {
500 assert_eq!(subtype, Some(SlackMessageEventType::MessageChanged));
501 if let Some(message) = message {
502 assert_eq!(message.sender.user, Some("UXXXXXXXXXX".into()));
503 assert_eq!(message.sender.bot_id, None);
504 assert_eq!(message.ts, "1701735043.989889".into());
505 assert_eq!(
506 message.edited.map(|edited| edited.ts),
507 Some("1701743154.000000".into())
508 );
509 assert_eq!(
510 message.content.unwrap().text,
511 Some("edited message".to_string())
512 );
513 } else {
514 panic!("Message is None");
515 }
516 if let Some(previous_message) = previous_message {
517 assert_eq!(previous_message.sender.user, Some("UXXXXXXXXXX".into()));
518 assert_eq!(previous_message.sender.bot_id, None);
519 assert_eq!(previous_message.ts, "1701735043.989889".into());
520 assert_eq!(
521 previous_message.edited.map(|edited| edited.ts),
522 Some("1701742890.000000".into())
523 );
524 assert_eq!(
525 previous_message.content.unwrap().text,
526 Some("hey!".to_string())
527 );
528 } else {
529 panic!("PreviousMessage is None");
530 }
531 }
532 _ => panic!("Unexpected event type"),
533 }
534 }
535
536 #[test]
537 fn test_slack_event_message_changed_by_bot_event() {
538 let payload = include_str!("./fixtures/message_changed_by_bot.json");
539 let event: SlackPushEventCallback = serde_json::from_str(payload).unwrap();
540 match event.event {
541 SlackEventCallbackBody::Message(SlackMessageEvent {
542 subtype,
543 message,
544 previous_message,
545 ..
546 }) => {
547 assert_eq!(subtype, Some(SlackMessageEventType::MessageChanged));
548 if let Some(message) = message {
549 assert_eq!(message.sender.user, None);
550 assert_eq!(message.sender.bot_id, Some("BXXXXXXXXXX".into()));
551 assert_eq!(message.ts, "1701735043.989889".into());
552 assert_eq!(
553 message.edited.map(|edited| edited.ts),
554 Some("1701743154.000000".into())
555 );
556 assert_eq!(
557 message.content.unwrap().text,
558 Some("edited message".to_string())
559 );
560 } else {
561 panic!("Message is None");
562 }
563 if let Some(previous_message) = previous_message {
564 assert_eq!(previous_message.sender.user, None);
565 assert_eq!(previous_message.sender.bot_id, Some("BXXXXXXXXXX".into()));
566 assert_eq!(previous_message.ts, "1701735043.989889".into());
567 assert_eq!(
568 previous_message.edited.map(|edited| edited.ts),
569 Some("1701742890.000000".into())
570 );
571 assert_eq!(
572 previous_message.content.unwrap().text,
573 Some("hey!".to_string())
574 );
575 } else {
576 panic!("PreviousMessage is None");
577 }
578 }
579 _ => panic!("Unexpected event type"),
580 }
581 }
582
583 #[test]
584 fn test_slack_event_agent_session_stopped() {
585 let payload = include_str!("./fixtures/agent_session_stopped.json");
586 let event: SlackPushEventCallback = serde_json::from_str(payload).unwrap();
587 match event.event {
588 SlackEventCallbackBody::AgentSessionStopped(SlackAgentSessionStoppedEvent {
589 channel,
590 thread_ts,
591 streaming_message_ts,
592 ..
593 }) => {
594 assert_eq!(channel, "CXXXXXXXXXX".into());
595 assert_eq!(thread_ts, Some("1757000000.000100".into()));
596 assert_eq!(streaming_message_ts, Some(vec!["1757000001.000200".into()]));
597 }
598 _ => panic!("Unexpected event type"),
599 }
600 }
601
602 #[test]
603 fn test_slack_event_app_context_changed() {
604 let payload = include_str!("./fixtures/app_context_changed.json");
605 let event: SlackPushEventCallback = serde_json::from_str(payload).unwrap();
606 match event.event {
607 SlackEventCallbackBody::AppContextChanged(SlackAppContextChangedEvent { context }) => {
608 assert_eq!(
609 context.entities,
610 Some(vec![SlackAppContextEntity {
611 entity_type: "slack#/types/channel_id".to_string().into(),
612 value: "C01234ABDCE".into(),
613 team_id: Some("T0ABCDE6543".into()),
614 }])
615 );
616 }
617 _ => panic!("Unexpected event type"),
618 }
619
620 let event: SlackEventCallbackBody =
621 serde_json::from_str(r#"{"type":"app_context_changed","context":{}}"#).unwrap();
622 assert_eq!(
623 event,
624 SlackEventCallbackBody::AppContextChanged(SlackAppContextChangedEvent {
625 context: SlackAppContext { entities: None }
626 })
627 );
628 }
629
630 #[test]
631 fn test_slack_event_unknown_type() {
632 let payload = serde_json::json!({"type":"some_future_event","x":1});
633 let event: SlackEventCallbackBody = serde_json::from_value(payload.clone()).unwrap();
634 assert_eq!(event, SlackEventCallbackBody::Unknown(payload.clone()));
635 assert_eq!(serde_json::to_value(&event).unwrap(), payload);
636 }
637
638 #[test]
639 fn test_slack_event_malformed_known_type_is_unknown() {
640 let payload = serde_json::json!({"type":"app_context_changed","context":"nope"});
641 let event: SlackEventCallbackBody = serde_json::from_value(payload.clone()).unwrap();
642 assert_eq!(event, SlackEventCallbackBody::Unknown(payload));
643 }
644
645 #[test]
646 fn test_slack_event_agent_session_title_changed() {
647 let payload = include_str!("./fixtures/agent_session_title_changed.json");
648 let event: SlackPushEventCallback = serde_json::from_str(payload).unwrap();
649 match event.event {
650 SlackEventCallbackBody::AgentSessionTitleChanged(
651 SlackAgentSessionTitleChangedEvent {
652 channel,
653 title,
654 previous_title,
655 team_id,
656 ..
657 },
658 ) => {
659 assert_eq!(channel, "C0123ABC456".into());
660 assert_eq!(title, "Bora Bora trip prep".to_string());
661 assert_eq!(previous_title, Some("Scuba diving research".to_string()));
662 assert_eq!(team_id, Some("T0123ABC456".into()));
663 }
664 _ => panic!("Unexpected event type"),
665 }
666 }
667}