1use 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#[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 RegisteredEventInvite(RegisteredEventInvite),
308 UnregisteredEventInvite(UnregisteredEventInvite),
309 ExternalEventInvite(ExternalEventInvite),
310 RegisteredEventUpdate(RegisteredEventUpdate),
312 UnregisteredEventUpdate(UnregisteredEventUpdate),
313 ExternalEventUpdate(ExternalEventUpdate),
314 RegisteredEventCancellation(RegisteredEventCancellation),
316 UnregisteredEventCancellation(UnregisteredEventCancellation),
317 ExternalEventCancellation(ExternalEventCancellation),
318 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}