Skip to main content

opentalk_mail_worker_protocol/v1/
mod.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5use chrono::{TimeZone, Utc};
6use opentalk_types_common::{
7    events::{EventDescription, EventTitle},
8    rooms::{RoomAlias, RoomPassword},
9    shared_folders::SharedFolder,
10    streaming::RoomStreamingTarget,
11    users::{Language, UserTitle},
12    utils::ExampleData,
13};
14use uuid::Uuid;
15
16mod invites;
17
18pub use invites::{
19    ExternalEventCancellation, ExternalEventInvite, ExternalEventUninvite, ExternalEventUpdate,
20    RegisteredEventCancellation, RegisteredEventInvite, RegisteredEventUninvite,
21    RegisteredEventUpdate, UnregisteredEventCancellation, UnregisteredEventInvite,
22    UnregisteredEventUninvite, UnregisteredEventUpdate,
23};
24
25#[derive(Clone, PartialEq, Eq, Debug)]
26#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
27#[cfg_attr(
28    feature = "utoipa",
29    derive(utoipa::ToSchema),
30    schema(example = json!(Email::example_data()))
31)]
32pub struct Email(String);
33
34impl ExampleData for Email {
35    fn example_data() -> Self {
36        Email::from("alice.adams@example.com")
37    }
38}
39
40impl Email {
41    pub fn new(s: String) -> Self {
42        Self(s)
43    }
44}
45
46impl From<&str> for Email {
47    fn from(s: &str) -> Self {
48        Self(s.to_owned())
49    }
50}
51
52impl From<String> for Email {
53    fn from(s: String) -> Self {
54        Self(s)
55    }
56}
57
58impl AsRef<str> for Email {
59    fn as_ref(&self) -> &str {
60        &self.0
61    }
62}
63
64#[derive(Clone, PartialEq, Eq, Debug)]
65#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
66#[cfg_attr(
67    feature = "utoipa",
68    derive(utoipa::ToSchema),
69    schema(example = json!(RegisteredUser::example_data()))
70)]
71pub struct RegisteredUser {
72    pub email: Email,
73    pub title: UserTitle,
74    pub first_name: String,
75    pub last_name: String,
76    pub language: Language,
77}
78
79impl ExampleData for RegisteredUser {
80    fn example_data() -> Self {
81        Self {
82            email: Email::from("alice.adams@example.com"),
83            title: "Dr.".parse().expect("valid user title"),
84            first_name: "Alice".to_string(),
85            last_name: "Adams".to_string(),
86            language: "en".parse().expect("valid language"),
87        }
88    }
89}
90
91#[derive(Clone, PartialEq, Eq, Debug)]
92#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
93#[cfg_attr(
94    feature = "utoipa",
95    derive(utoipa::ToSchema),
96    schema(example = json!(UnregisteredUser::example_data()))
97)]
98pub struct UnregisteredUser {
99    pub email: Email,
100    pub first_name: String,
101    pub last_name: String,
102}
103
104impl ExampleData for UnregisteredUser {
105    fn example_data() -> Self {
106        Self {
107            email: Email::from("bob.burton@example.com"),
108            first_name: "Bob".to_string(),
109            last_name: "Burton".to_string(),
110        }
111    }
112}
113
114#[derive(Clone, PartialEq, Eq, Debug)]
115#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
116#[cfg_attr(
117    feature = "utoipa",
118    derive(utoipa::ToSchema),
119    schema(example = json!(ExternalUser::example_data()))
120)]
121pub struct ExternalUser {
122    pub email: Email,
123}
124
125impl ExampleData for ExternalUser {
126    fn example_data() -> Self {
127        Self {
128            email: Email::from("charlie.cooper@example.com"),
129        }
130    }
131}
132
133#[derive(Clone, PartialEq, Eq, Debug)]
134#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
135#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
136pub enum User {
137    Registered(RegisteredUser),
138    Unregistered(UnregisteredUser),
139    External(ExternalUser),
140}
141
142#[derive(Clone, PartialEq, Eq, Debug)]
143#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
144#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
145pub struct Time {
146    pub time: chrono::DateTime<Utc>,
147    pub timezone: String,
148}
149
150impl ExampleData for Time {
151    fn example_data() -> Self {
152        Self {
153            time: Utc.with_ymd_and_hms(2024, 7, 5, 17, 2, 42).unwrap(),
154            timezone: "Europe/Berlin".to_string(),
155        }
156    }
157}
158
159#[derive(Clone, PartialEq, Eq, Debug)]
160#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
161#[cfg_attr(
162    feature = "utoipa",
163    derive(utoipa::ToSchema),
164    schema(example = json!(Event::example_data()))
165)]
166pub struct Event {
167    pub id: Uuid,
168    pub name: EventTitle,
169    pub created_at: Time,
170    pub start_time: Option<Time>,
171    pub end_time: Option<Time>,
172    pub rrule: Option<String>,
173    pub description: EventDescription,
174    pub room: Room,
175    pub call_in: Option<CallIn>,
176    pub revision: i32,
177    pub shared_folder: Option<SharedFolder>,
178    pub adhoc_retention_seconds: Option<u64>,
179    pub streaming_targets: Vec<RoomStreamingTarget>,
180}
181
182impl ExampleData for Event {
183    fn example_data() -> Self {
184        Self {
185            id: Uuid::from_u128(0xabadcafe),
186            name: "Weekly teammeeting".parse().expect("valid event title"),
187            created_at: Time::example_data(),
188            start_time: None,
189            end_time: None,
190            rrule: None,
191            description: "The team's regular weekly meeting"
192                .parse()
193                .expect("valid event description"),
194            room: Room::example_data(),
195            call_in: Some(CallIn::example_data()),
196            revision: 3,
197            shared_folder: Some(SharedFolder::example_data()),
198            adhoc_retention_seconds: None,
199            streaming_targets: vec![],
200        }
201    }
202}
203
204#[derive(Clone, PartialEq, Eq, Debug)]
205#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
206#[cfg_attr(
207    feature = "utoipa",
208    derive(utoipa::ToSchema),
209    schema(example = json!(EventException::example_data()))
210)]
211pub struct EventException {
212    pub exception_date: Time,
213    pub kind: EventExceptionKind,
214    pub title: Option<EventTitle>,
215    pub description: Option<EventDescription>,
216    pub is_all_day: Option<bool>,
217    pub starts_at: Option<Time>,
218    pub ends_at: Option<Time>,
219}
220
221impl ExampleData for EventException {
222    fn example_data() -> Self {
223        Self {
224            exception_date: Time::example_data(),
225            kind: EventExceptionKind::Modified,
226            title: Some("Another weekly meeting".parse().expect("valid event title")),
227            description: None,
228            is_all_day: None,
229            starts_at: None,
230            ends_at: None,
231        }
232    }
233}
234
235#[derive(Clone, PartialEq, Eq, Debug)]
236#[cfg_attr(
237    feature = "serde",
238    derive(serde::Deserialize, serde::Serialize),
239    serde(rename_all = "snake_case")
240)]
241#[cfg_attr(
242    feature = "utoipa",
243    derive(utoipa::ToSchema),
244    schema(example = json!(EventExceptionKind::Modified))
245)]
246pub enum EventExceptionKind {
247    Modified,
248    Canceled,
249}
250
251#[derive(Clone, PartialEq, Eq, Debug)]
252#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
253#[cfg_attr(
254    feature = "utoipa",
255    derive(utoipa::ToSchema),
256    schema(example = json!(Room::example_data()))
257)]
258pub struct Room {
259    pub id: Uuid,
260    pub alias: Option<RoomAlias>,
261    pub password: Option<RoomPassword>,
262}
263
264impl ExampleData for Room {
265    fn example_data() -> Self {
266        Self {
267            id: Uuid::from_u128(0xabcdef99),
268            alias: Some(RoomAlias::example_data()),
269            password: Some("v3rys3cr3t".parse().unwrap()),
270        }
271    }
272}
273
274#[derive(Clone, PartialEq, Eq, Debug)]
275#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
276#[cfg_attr(
277    feature = "utoipa",
278    derive(utoipa::ToSchema),
279    schema(example = json!(CallIn::example_data())),
280)]
281pub struct CallIn {
282    pub sip_tel: String,
283    pub sip_id: String,
284    pub sip_password: String,
285}
286
287impl ExampleData for CallIn {
288    fn example_data() -> Self {
289        Self {
290            sip_tel: "+99-1234567890".to_string(),
291            sip_id: "1234567890".to_string(),
292            sip_password: "9876543210".to_string(),
293        }
294    }
295}
296
297/// The different kinds of MailTasks that are currently supported
298#[derive(PartialEq, Eq, Debug)]
299#[cfg_attr(
300    feature = "serde",
301    derive(serde::Deserialize, serde::Serialize),
302    serde(tag = "message", rename_all = "snake_case")
303)]
304#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema), schema(as = v1::Message))]
305pub enum Message {
306    // Invites
307    RegisteredEventInvite(RegisteredEventInvite),
308    UnregisteredEventInvite(UnregisteredEventInvite),
309    ExternalEventInvite(ExternalEventInvite),
310    // Updates
311    RegisteredEventUpdate(RegisteredEventUpdate),
312    UnregisteredEventUpdate(UnregisteredEventUpdate),
313    ExternalEventUpdate(ExternalEventUpdate),
314    // Cancellations
315    RegisteredEventCancellation(RegisteredEventCancellation),
316    UnregisteredEventCancellation(UnregisteredEventCancellation),
317    ExternalEventCancellation(ExternalEventCancellation),
318    // Uninvites
319    RegisteredEventUninvite(RegisteredEventUninvite),
320    UnregisteredEventUninvite(UnregisteredEventUninvite),
321    ExternalEventUninvite(ExternalEventUninvite),
322}
323
324impl From<RegisteredEventInvite> for Message {
325    fn from(value: RegisteredEventInvite) -> Self {
326        Message::RegisteredEventInvite(value)
327    }
328}
329
330impl From<UnregisteredEventInvite> for Message {
331    fn from(value: UnregisteredEventInvite) -> Self {
332        Message::UnregisteredEventInvite(value)
333    }
334}
335
336impl From<ExternalEventInvite> for Message {
337    fn from(value: ExternalEventInvite) -> Self {
338        Message::ExternalEventInvite(value)
339    }
340}
341
342impl From<RegisteredEventUpdate> for Message {
343    fn from(value: RegisteredEventUpdate) -> Self {
344        Message::RegisteredEventUpdate(value)
345    }
346}
347
348impl From<UnregisteredEventUpdate> for Message {
349    fn from(value: UnregisteredEventUpdate) -> Self {
350        Message::UnregisteredEventUpdate(value)
351    }
352}
353
354impl From<ExternalEventUpdate> for Message {
355    fn from(value: ExternalEventUpdate) -> Self {
356        Message::ExternalEventUpdate(value)
357    }
358}
359
360impl From<RegisteredEventCancellation> for Message {
361    fn from(value: RegisteredEventCancellation) -> Self {
362        Message::RegisteredEventCancellation(value)
363    }
364}
365
366impl From<UnregisteredEventCancellation> for Message {
367    fn from(value: UnregisteredEventCancellation) -> Self {
368        Message::UnregisteredEventCancellation(value)
369    }
370}
371
372impl From<ExternalEventCancellation> for Message {
373    fn from(value: ExternalEventCancellation) -> Self {
374        Message::ExternalEventCancellation(value)
375    }
376}
377
378impl From<RegisteredEventUninvite> for Message {
379    fn from(value: RegisteredEventUninvite) -> Self {
380        Message::RegisteredEventUninvite(value)
381    }
382}
383
384impl From<UnregisteredEventUninvite> for Message {
385    fn from(value: UnregisteredEventUninvite) -> Self {
386        Message::UnregisteredEventUninvite(value)
387    }
388}
389
390impl From<ExternalEventUninvite> for Message {
391    fn from(value: ExternalEventUninvite) -> Self {
392        Message::ExternalEventUninvite(value)
393    }
394}
395
396#[cfg(all(test, feature = "serde"))]
397mod tests {
398    use chrono::FixedOffset;
399    use opentalk_types_common::shared_folders::SharedFolderAccess;
400    use pretty_assertions::assert_eq;
401
402    use super::*;
403    use crate::*;
404
405    #[test]
406    fn test_basic_format() {
407        let basic_invite = MailTask::V1(Message::RegisteredEventInvite(RegisteredEventInvite {
408            inviter: RegisteredUser {
409                email: "bob@example.org".into(),
410                title: "Prof. Dr.".parse().expect("valid user title"),
411                first_name: "Bob".into(),
412                last_name: "Inviter".into(),
413                language: "de".parse().expect("valid language"),
414            },
415            event: Event {
416                id: Uuid::from_u128(1),
417                name: "Guten Morgen Meeting".parse().expect("valid event title"),
418                description: "".parse().expect("valid event description"),
419                created_at: Time {
420                    time: chrono::DateTime::<FixedOffset>::parse_from_rfc3339(
421                        "2021-12-29T15:00:00+00:00",
422                    )
423                    .unwrap()
424                    .into(),
425                    timezone: "UTC".into(),
426                },
427                start_time: Some(Time {
428                    time: chrono::DateTime::<FixedOffset>::parse_from_rfc3339(
429                        "2021-12-29T15:00:00+02:00",
430                    )
431                    .unwrap()
432                    .into(),
433                    timezone: "Europe/Berlin".into(),
434                }),
435                end_time: Some(Time {
436                    time: chrono::DateTime::<FixedOffset>::parse_from_rfc3339(
437                        "2021-12-29T15:30:00+02:00",
438                    )
439                    .unwrap()
440                    .into(),
441                    timezone: "Europe/Berlin".into(),
442                }),
443                rrule: None,
444                room: Room {
445                    id: Uuid::from_u128(0),
446                    alias: Some(RoomAlias::example_data()),
447                    password: Some("password123".parse().unwrap()),
448                },
449                call_in: Some(CallIn {
450                    sip_tel: "+497652917".into(),
451                    sip_id: "2".into(),
452                    sip_password: "987".into(),
453                }),
454                revision: 0,
455                shared_folder: Some(SharedFolder {
456                    read: SharedFolderAccess {
457                        url: "https://nextcloud.example.com/s/TArrLyC3K7c5Jbg".to_string(),
458                        password: "DLgoYrFEoy".to_string(),
459                    },
460                    read_write: None,
461                }),
462                adhoc_retention_seconds: Some(86400),
463                streaming_targets: Vec::new(),
464            },
465            invitee: RegisteredUser {
466                email: "lastname@example.org".into(),
467                title: "Prof. Dr.".parse().expect("valid user title"),
468                first_name: "FirstName".into(),
469                last_name: "LastName".into(),
470                language: "de".parse().expect("valid language"),
471            },
472        }));
473
474        assert_eq!(
475            basic_invite,
476            serde_json::from_value(serde_json::json!({
477                "version": "1",
478                "message": "registered_event_invite",
479                "event": {
480                    "id": Uuid::from_u128(1),
481                    "name": "Guten Morgen Meeting",
482                    "description": "",
483                    "created_at": {"time":"2021-12-29T15:00:00+00:00", "timezone": "UTC"},
484                    "start_time": {"time":"2021-12-29T15:00:00+02:00", "timezone": "Europe/Berlin"},
485                    "end_time": {"time": "2021-12-29T15:30:00+02:00", "timezone": "Europe/Berlin"},
486                    "room": {
487                        "id": Uuid::from_u128(0),
488                        "alias": RoomAlias::example_data(),
489                        "password": "password123"
490                    },
491                    "call_in": {
492                        "sip_tel": "+497652917",
493                        "sip_id": "2",
494                        "sip_password": "987"
495                    },
496                    "revision": 0,
497                    "shared_folder": {
498                        "read": {
499                            "url": "https://nextcloud.example.com/s/TArrLyC3K7c5Jbg",
500                            "password": "DLgoYrFEoy"
501                        },
502                    },
503                    "adhoc_retention_seconds" : 86400,
504                    "streaming_targets": Vec::<RoomStreamingTarget>::new(),
505                },
506                "invitee": {
507                    "email": "lastname@example.org",
508                    "title": "Prof. Dr.",
509                    "first_name": "FirstName",
510                    "last_name": "LastName",
511                    "language": "de"
512                },
513                "inviter": {
514                    "email": "bob@example.org",
515                    "title": "Prof. Dr.",
516                    "first_name": "Bob",
517                    "last_name": "Inviter",
518                    "language": "de"
519                }
520            }))
521            .unwrap()
522        );
523    }
524
525    #[test]
526    fn test_no_time() {
527        let basic_invite = MailTask::V1(Message::RegisteredEventInvite(RegisteredEventInvite {
528            inviter: RegisteredUser {
529                email: "bob@example.org".into(),
530                title: "Prof. Dr.".parse().expect("valid user title"),
531                first_name: "Bob".into(),
532                last_name: "Inviter".into(),
533                language: "de".parse().expect("valid language"),
534            },
535            event: Event {
536                id: Uuid::from_u128(1),
537                name: "Guten Morgen Meeting".parse().expect("valid event title"),
538                description: "".parse().expect("valid event description"),
539                created_at: Time {
540                    time: chrono::DateTime::<FixedOffset>::parse_from_rfc3339(
541                        "2021-12-29T15:00:00+00:00",
542                    )
543                    .unwrap()
544                    .into(),
545                    timezone: "UTC".into(),
546                },
547                start_time: None,
548                end_time: None,
549                rrule: None,
550                room: Room {
551                    id: Uuid::from_u128(0),
552                    alias: None,
553                    password: None,
554                },
555                call_in: Some(CallIn {
556                    sip_tel: "+497652917".into(),
557                    sip_id: "2".into(),
558                    sip_password: "987".into(),
559                }),
560                revision: 0,
561                shared_folder: Some(SharedFolder {
562                    read: SharedFolderAccess {
563                        url: "https://nextcloud.example.com/s/TArrLyC3K7c5Jbg".to_string(),
564                        password: "DLgoYrFEoy".to_string(),
565                    },
566                    read_write: None,
567                }),
568                adhoc_retention_seconds: None,
569                streaming_targets: Vec::new(),
570            },
571            invitee: RegisteredUser {
572                email: "lastname@example.org".into(),
573                title: "Prof. Dr.".parse().expect("valid user title"),
574                first_name: "FirstName".into(),
575                last_name: "LastName".into(),
576                language: "de".parse().expect("valid language"),
577            },
578        }));
579
580        assert_eq!(
581            basic_invite,
582            serde_json::from_value(serde_json::json!({
583                "version": "1",
584                "message": "registered_event_invite",
585                "event": {
586                    "id": Uuid::from_u128(1),
587                    "name": "Guten Morgen Meeting",
588                    "created_at": {"time":"2021-12-29T15:00:00+00:00", "timezone": "UTC"},
589                    "description": "",
590                    "room": {
591                        "id": Uuid::from_u128(0),
592                    },
593                    "call_in": {
594                        "sip_tel": "+497652917",
595                        "sip_id": "2",
596                        "sip_password": "987"
597                    },
598                    "revision": 0,
599                    "shared_folder": {
600                        "read": {
601                            "url": "https://nextcloud.example.com/s/TArrLyC3K7c5Jbg",
602                            "password": "DLgoYrFEoy"
603                        },
604                    },
605                    "streaming_targets": Vec::<RoomStreamingTarget>::new(),
606                },
607                "invitee": {
608                    "email": "lastname@example.org",
609                    "title": "Prof. Dr.",
610                    "first_name": "FirstName",
611                    "last_name": "LastName",
612                    "language": "de"
613                },
614                "inviter": {
615                    "email": "bob@example.org",
616                    "title": "Prof. Dr.",
617                    "first_name": "Bob",
618                    "last_name": "Inviter",
619                    "language": "de"
620                }
621            }))
622            .unwrap()
623        );
624    }
625}