matrix_sdk_ffi/
ruma.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{collections::BTreeSet, sync::Arc, time::Duration};
16
17use extension_trait::extension_trait;
18use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
19use ruma::{
20    assign,
21    events::{
22        call::notify::NotifyType as RumaNotifyType,
23        location::AssetType as RumaAssetType,
24        poll::start::PollKind as RumaPollKind,
25        room::{
26            message::{
27                AudioInfo as RumaAudioInfo,
28                AudioMessageEventContent as RumaAudioMessageEventContent,
29                EmoteMessageEventContent as RumaEmoteMessageEventContent, FileInfo as RumaFileInfo,
30                FileMessageEventContent as RumaFileMessageEventContent,
31                FormattedBody as RumaFormattedBody,
32                ImageMessageEventContent as RumaImageMessageEventContent,
33                LocationMessageEventContent as RumaLocationMessageEventContent,
34                MessageType as RumaMessageType,
35                NoticeMessageEventContent as RumaNoticeMessageEventContent,
36                RoomMessageEventContentWithoutRelation,
37                TextMessageEventContent as RumaTextMessageEventContent,
38                UnstableAudioDetailsContentBlock as RumaUnstableAudioDetailsContentBlock,
39                UnstableVoiceContentBlock as RumaUnstableVoiceContentBlock,
40                VideoInfo as RumaVideoInfo,
41                VideoMessageEventContent as RumaVideoMessageEventContent,
42            },
43            ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
44            ThumbnailInfo as RumaThumbnailInfo,
45        },
46    },
47    matrix_uri::MatrixId as RumaMatrixId,
48    serde::JsonObject,
49    MatrixToUri, MatrixUri as RumaMatrixUri, OwnedUserId, UInt, UserId,
50};
51use tracing::info;
52
53use crate::{
54    error::{ClientError, MediaInfoError},
55    helpers::unwrap_or_clone_arc,
56    timeline::MessageContent,
57    utils::u64_to_uint,
58};
59
60#[derive(uniffi::Enum)]
61pub enum AuthData {
62    /// Password-based authentication (`m.login.password`).
63    Password { password_details: AuthDataPasswordDetails },
64}
65
66#[derive(uniffi::Record)]
67pub struct AuthDataPasswordDetails {
68    /// One of the user's identifiers.
69    identifier: String,
70
71    /// The plaintext password.
72    password: String,
73}
74
75impl From<AuthData> for ruma::api::client::uiaa::AuthData {
76    fn from(value: AuthData) -> ruma::api::client::uiaa::AuthData {
77        match value {
78            AuthData::Password { password_details } => {
79                let user_id = ruma::UserId::parse(password_details.identifier).unwrap();
80
81                ruma::api::client::uiaa::AuthData::Password(ruma::api::client::uiaa::Password::new(
82                    user_id.into(),
83                    password_details.password,
84                ))
85            }
86        }
87    }
88}
89
90/// Parse a matrix entity from a given URI, be it either
91/// a `matrix.to` link or a `matrix:` URI
92#[matrix_sdk_ffi_macros::export]
93pub fn parse_matrix_entity_from(uri: String) -> Option<MatrixEntity> {
94    if let Ok(matrix_uri) = RumaMatrixUri::parse(&uri) {
95        return Some(MatrixEntity {
96            id: matrix_uri.id().into(),
97            via: matrix_uri.via().iter().map(|via| via.to_string()).collect(),
98        });
99    }
100
101    if let Ok(matrix_to_uri) = MatrixToUri::parse(&uri) {
102        return Some(MatrixEntity {
103            id: matrix_to_uri.id().into(),
104            via: matrix_to_uri.via().iter().map(|via| via.to_string()).collect(),
105        });
106    }
107
108    None
109}
110
111/// A Matrix entity that can be a room, room alias, user, or event, and a list
112/// of via servers.
113#[derive(uniffi::Record)]
114pub struct MatrixEntity {
115    id: MatrixId,
116    via: Vec<String>,
117}
118
119/// A Matrix ID that can be a room, room alias, user, or event.
120#[derive(Clone, uniffi::Enum)]
121pub enum MatrixId {
122    Room { id: String },
123    RoomAlias { alias: String },
124    User { id: String },
125    EventOnRoomId { room_id: String, event_id: String },
126    EventOnRoomAlias { alias: String, event_id: String },
127}
128
129impl From<&RumaMatrixId> for MatrixId {
130    fn from(value: &RumaMatrixId) -> Self {
131        match value {
132            RumaMatrixId::User(id) => MatrixId::User { id: id.to_string() },
133            RumaMatrixId::Room(id) => MatrixId::Room { id: id.to_string() },
134            RumaMatrixId::RoomAlias(id) => MatrixId::RoomAlias { alias: id.to_string() },
135
136            RumaMatrixId::Event(room_id_or_alias, event_id) => {
137                if room_id_or_alias.is_room_id() {
138                    MatrixId::EventOnRoomId {
139                        room_id: room_id_or_alias.to_string(),
140                        event_id: event_id.to_string(),
141                    }
142                } else if room_id_or_alias.is_room_alias_id() {
143                    MatrixId::EventOnRoomAlias {
144                        alias: room_id_or_alias.to_string(),
145                        event_id: event_id.to_string(),
146                    }
147                } else {
148                    panic!("Unexpected MatrixId type: {:?}", room_id_or_alias)
149                }
150            }
151            _ => panic!("Unexpected MatrixId type: {:?}", value),
152        }
153    }
154}
155
156#[matrix_sdk_ffi_macros::export]
157pub fn message_event_content_new(
158    msgtype: MessageType,
159) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
160    Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msgtype.try_into()?)))
161}
162
163#[matrix_sdk_ffi_macros::export]
164pub fn message_event_content_from_markdown(
165    md: String,
166) -> Arc<RoomMessageEventContentWithoutRelation> {
167    Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_markdown(md)))
168}
169
170#[matrix_sdk_ffi_macros::export]
171pub fn message_event_content_from_markdown_as_emote(
172    md: String,
173) -> Arc<RoomMessageEventContentWithoutRelation> {
174    Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_markdown(md)))
175}
176
177#[matrix_sdk_ffi_macros::export]
178pub fn message_event_content_from_html(
179    body: String,
180    html_body: String,
181) -> Arc<RoomMessageEventContentWithoutRelation> {
182    Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::text_html(
183        body, html_body,
184    )))
185}
186
187#[matrix_sdk_ffi_macros::export]
188pub fn message_event_content_from_html_as_emote(
189    body: String,
190    html_body: String,
191) -> Arc<RoomMessageEventContentWithoutRelation> {
192    Arc::new(RoomMessageEventContentWithoutRelation::new(RumaMessageType::emote_html(
193        body, html_body,
194    )))
195}
196
197#[derive(Clone, uniffi::Object)]
198pub struct MediaSource {
199    pub(crate) media_source: RumaMediaSource,
200}
201
202#[matrix_sdk_ffi_macros::export]
203impl MediaSource {
204    #[uniffi::constructor]
205    pub fn from_url(url: String) -> Result<Arc<MediaSource>, ClientError> {
206        let media_source = RumaMediaSource::Plain(url.into());
207        media_source.verify()?;
208
209        Ok(Arc::new(MediaSource { media_source }))
210    }
211
212    pub fn url(&self) -> String {
213        self.media_source.url()
214    }
215
216    // Used on Element X Android
217    #[uniffi::constructor]
218    pub fn from_json(json: String) -> Result<Arc<Self>, ClientError> {
219        let media_source: RumaMediaSource = serde_json::from_str(&json)?;
220        media_source.verify()?;
221
222        Ok(Arc::new(MediaSource { media_source }))
223    }
224
225    // Used on Element X Android
226    pub fn to_json(&self) -> String {
227        serde_json::to_string(&self.media_source)
228            .expect("Media source should always be serializable ")
229    }
230}
231
232impl TryFrom<RumaMediaSource> for MediaSource {
233    type Error = ClientError;
234
235    fn try_from(value: RumaMediaSource) -> Result<Self, Self::Error> {
236        value.verify()?;
237        Ok(Self { media_source: value })
238    }
239}
240
241impl TryFrom<&RumaMediaSource> for MediaSource {
242    type Error = ClientError;
243
244    fn try_from(value: &RumaMediaSource) -> Result<Self, Self::Error> {
245        value.verify()?;
246        Ok(Self { media_source: value.clone() })
247    }
248}
249
250impl From<MediaSource> for RumaMediaSource {
251    fn from(value: MediaSource) -> Self {
252        value.media_source
253    }
254}
255
256#[extension_trait]
257pub(crate) impl MediaSourceExt for RumaMediaSource {
258    fn verify(&self) -> Result<(), ClientError> {
259        match self {
260            RumaMediaSource::Plain(url) => {
261                url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
262            }
263            RumaMediaSource::Encrypted(file) => {
264                file.url.validate().map_err(|e| ClientError::Generic { msg: e.to_string() })?;
265            }
266        }
267
268        Ok(())
269    }
270
271    fn url(&self) -> String {
272        match self {
273            RumaMediaSource::Plain(url) => url.to_string(),
274            RumaMediaSource::Encrypted(file) => file.url.to_string(),
275        }
276    }
277}
278
279#[extension_trait]
280pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWithoutRelation {
281    fn with_mentions(self: Arc<Self>, mentions: Mentions) -> Arc<Self> {
282        let mut content = unwrap_or_clone_arc(self);
283        content.mentions = Some(mentions.into());
284        Arc::new(content)
285    }
286}
287
288#[derive(Clone)]
289pub struct Mentions {
290    pub user_ids: Vec<String>,
291    pub room: bool,
292}
293
294impl From<Mentions> for ruma::events::Mentions {
295    fn from(value: Mentions) -> Self {
296        let mut user_ids = BTreeSet::<OwnedUserId>::new();
297        for user_id in value.user_ids {
298            if let Ok(user_id) = UserId::parse(user_id) {
299                user_ids.insert(user_id);
300            }
301        }
302        let mut result = Self::default();
303        result.user_ids = user_ids;
304        result.room = value.room;
305        result
306    }
307}
308
309#[derive(Clone, uniffi::Enum)]
310pub enum MessageType {
311    Emote { content: EmoteMessageContent },
312    Image { content: ImageMessageContent },
313    Audio { content: AudioMessageContent },
314    Video { content: VideoMessageContent },
315    File { content: FileMessageContent },
316    Notice { content: NoticeMessageContent },
317    Text { content: TextMessageContent },
318    Location { content: LocationContent },
319    Other { msgtype: String, body: String },
320}
321
322/// From MSC2530: https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/2530-body-as-caption.md
323/// If the filename field is present in a media message, clients should treat
324/// body as a caption instead of a file name. Otherwise, the body is the
325/// file name.
326///
327/// So:
328/// - if a media has a filename and a caption, the body is the caption, filename
329///   is its own field.
330/// - if a media only has a filename, then body is the filename.
331fn get_body_and_filename(filename: String, caption: Option<String>) -> (String, Option<String>) {
332    if let Some(caption) = caption {
333        (caption, Some(filename))
334    } else {
335        (filename, None)
336    }
337}
338
339impl TryFrom<MessageType> for RumaMessageType {
340    type Error = ClientError;
341
342    fn try_from(value: MessageType) -> Result<Self, Self::Error> {
343        Ok(match value {
344            MessageType::Emote { content } => {
345                Self::Emote(assign!(RumaEmoteMessageEventContent::plain(content.body), {
346                    formatted: content.formatted.map(Into::into),
347                }))
348            }
349            MessageType::Image { content } => {
350                let (body, filename) = get_body_and_filename(content.filename, content.caption);
351                let mut event_content =
352                    RumaImageMessageEventContent::new(body, (*content.source).clone().into())
353                        .info(content.info.map(Into::into).map(Box::new));
354                event_content.formatted = content.formatted_caption.map(Into::into);
355                event_content.filename = filename;
356                Self::Image(event_content)
357            }
358            MessageType::Audio { content } => {
359                let (body, filename) = get_body_and_filename(content.filename, content.caption);
360                let mut event_content =
361                    RumaAudioMessageEventContent::new(body, (*content.source).clone().into())
362                        .info(content.info.map(Into::into).map(Box::new));
363                event_content.formatted = content.formatted_caption.map(Into::into);
364                event_content.filename = filename;
365                Self::Audio(event_content)
366            }
367            MessageType::Video { content } => {
368                let (body, filename) = get_body_and_filename(content.filename, content.caption);
369                let mut event_content =
370                    RumaVideoMessageEventContent::new(body, (*content.source).clone().into())
371                        .info(content.info.map(Into::into).map(Box::new));
372                event_content.formatted = content.formatted_caption.map(Into::into);
373                event_content.filename = filename;
374                Self::Video(event_content)
375            }
376            MessageType::File { content } => {
377                let (body, filename) = get_body_and_filename(content.filename, content.caption);
378                let mut event_content =
379                    RumaFileMessageEventContent::new(body, (*content.source).clone().into())
380                        .info(content.info.map(Into::into).map(Box::new));
381                event_content.formatted = content.formatted_caption.map(Into::into);
382                event_content.filename = filename;
383                Self::File(event_content)
384            }
385            MessageType::Notice { content } => {
386                Self::Notice(assign!(RumaNoticeMessageEventContent::plain(content.body), {
387                    formatted: content.formatted.map(Into::into),
388                }))
389            }
390            MessageType::Text { content } => {
391                Self::Text(assign!(RumaTextMessageEventContent::plain(content.body), {
392                    formatted: content.formatted.map(Into::into),
393                }))
394            }
395            MessageType::Location { content } => {
396                Self::Location(RumaLocationMessageEventContent::new(content.body, content.geo_uri))
397            }
398            MessageType::Other { msgtype, body } => {
399                Self::new(&msgtype, body, JsonObject::default())?
400            }
401        })
402    }
403}
404
405impl TryFrom<RumaMessageType> for MessageType {
406    type Error = ClientError;
407
408    fn try_from(value: RumaMessageType) -> Result<Self, Self::Error> {
409        Ok(match value {
410            RumaMessageType::Emote(c) => MessageType::Emote {
411                content: EmoteMessageContent {
412                    body: c.body.clone(),
413                    formatted: c.formatted.as_ref().map(Into::into),
414                },
415            },
416            RumaMessageType::Image(c) => MessageType::Image {
417                content: ImageMessageContent {
418                    filename: c.filename().to_owned(),
419                    caption: c.caption().map(ToString::to_string),
420                    formatted_caption: c.formatted_caption().map(Into::into),
421                    source: Arc::new(c.source.try_into()?),
422                    info: c.info.as_deref().map(TryInto::try_into).transpose()?,
423                },
424            },
425
426            RumaMessageType::Audio(c) => MessageType::Audio {
427                content: AudioMessageContent {
428                    filename: c.filename().to_owned(),
429                    caption: c.caption().map(ToString::to_string),
430                    formatted_caption: c.formatted_caption().map(Into::into),
431                    source: Arc::new(c.source.try_into()?),
432                    info: c.info.as_deref().map(Into::into),
433                    audio: c.audio.map(Into::into),
434                    voice: c.voice.map(Into::into),
435                },
436            },
437            RumaMessageType::Video(c) => MessageType::Video {
438                content: VideoMessageContent {
439                    filename: c.filename().to_owned(),
440                    caption: c.caption().map(ToString::to_string),
441                    formatted_caption: c.formatted_caption().map(Into::into),
442                    source: Arc::new(c.source.try_into()?),
443                    info: c.info.as_deref().map(TryInto::try_into).transpose()?,
444                },
445            },
446            RumaMessageType::File(c) => MessageType::File {
447                content: FileMessageContent {
448                    filename: c.filename().to_owned(),
449                    caption: c.caption().map(ToString::to_string),
450                    formatted_caption: c.formatted_caption().map(Into::into),
451                    source: Arc::new(c.source.try_into()?),
452                    info: c.info.as_deref().map(TryInto::try_into).transpose()?,
453                },
454            },
455            RumaMessageType::Notice(c) => MessageType::Notice {
456                content: NoticeMessageContent {
457                    body: c.body.clone(),
458                    formatted: c.formatted.as_ref().map(Into::into),
459                },
460            },
461            RumaMessageType::Text(c) => MessageType::Text {
462                content: TextMessageContent {
463                    body: c.body.clone(),
464                    formatted: c.formatted.as_ref().map(Into::into),
465                },
466            },
467            RumaMessageType::Location(c) => {
468                let (description, zoom_level) =
469                    c.location.map(|loc| (loc.description, loc.zoom_level)).unwrap_or((None, None));
470                MessageType::Location {
471                    content: LocationContent {
472                        body: c.body,
473                        geo_uri: c.geo_uri,
474                        description,
475                        zoom_level: zoom_level.and_then(|z| z.get().try_into().ok()),
476                        asset: c.asset.and_then(|a| match a.type_ {
477                            RumaAssetType::Self_ => Some(AssetType::Sender),
478                            RumaAssetType::Pin => Some(AssetType::Pin),
479                            _ => None,
480                        }),
481                    },
482                }
483            }
484            _ => MessageType::Other {
485                msgtype: value.msgtype().to_owned(),
486                body: value.body().to_owned(),
487            },
488        })
489    }
490}
491
492#[derive(Clone, uniffi::Enum)]
493pub enum NotifyType {
494    Ring,
495    Notify,
496}
497
498impl From<RumaNotifyType> for NotifyType {
499    fn from(val: RumaNotifyType) -> Self {
500        match val {
501            RumaNotifyType::Ring => Self::Ring,
502            _ => Self::Notify,
503        }
504    }
505}
506
507impl From<NotifyType> for RumaNotifyType {
508    fn from(value: NotifyType) -> Self {
509        match value {
510            NotifyType::Ring => RumaNotifyType::Ring,
511            NotifyType::Notify => RumaNotifyType::Notify,
512        }
513    }
514}
515
516#[derive(Clone, uniffi::Record)]
517pub struct EmoteMessageContent {
518    pub body: String,
519    pub formatted: Option<FormattedBody>,
520}
521
522#[derive(Clone, uniffi::Record)]
523pub struct ImageMessageContent {
524    /// The computed filename, for use in a client.
525    pub filename: String,
526    pub caption: Option<String>,
527    pub formatted_caption: Option<FormattedBody>,
528    pub source: Arc<MediaSource>,
529    pub info: Option<ImageInfo>,
530}
531
532#[derive(Clone, uniffi::Record)]
533pub struct AudioMessageContent {
534    /// The computed filename, for use in a client.
535    pub filename: String,
536    pub caption: Option<String>,
537    pub formatted_caption: Option<FormattedBody>,
538    pub source: Arc<MediaSource>,
539    pub info: Option<AudioInfo>,
540    pub audio: Option<UnstableAudioDetailsContent>,
541    pub voice: Option<UnstableVoiceContent>,
542}
543
544#[derive(Clone, uniffi::Record)]
545pub struct VideoMessageContent {
546    /// The computed filename, for use in a client.
547    pub filename: String,
548    pub caption: Option<String>,
549    pub formatted_caption: Option<FormattedBody>,
550    pub source: Arc<MediaSource>,
551    pub info: Option<VideoInfo>,
552}
553
554#[derive(Clone, uniffi::Record)]
555pub struct FileMessageContent {
556    /// The computed filename, for use in a client.
557    pub filename: String,
558    pub caption: Option<String>,
559    pub formatted_caption: Option<FormattedBody>,
560    pub source: Arc<MediaSource>,
561    pub info: Option<FileInfo>,
562}
563
564#[derive(Clone, uniffi::Record)]
565pub struct ImageInfo {
566    pub height: Option<u64>,
567    pub width: Option<u64>,
568    pub mimetype: Option<String>,
569    pub size: Option<u64>,
570    pub thumbnail_info: Option<ThumbnailInfo>,
571    pub thumbnail_source: Option<Arc<MediaSource>>,
572    pub blurhash: Option<String>,
573    pub is_animated: Option<bool>,
574}
575
576impl From<ImageInfo> for RumaImageInfo {
577    fn from(value: ImageInfo) -> Self {
578        assign!(RumaImageInfo::new(), {
579            height: value.height.map(u64_to_uint),
580            width: value.width.map(u64_to_uint),
581            mimetype: value.mimetype,
582            size: value.size.map(u64_to_uint),
583            thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
584            thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
585            blurhash: value.blurhash,
586            is_animated: value.is_animated,
587        })
588    }
589}
590
591impl TryFrom<&ImageInfo> for BaseImageInfo {
592    type Error = MediaInfoError;
593
594    fn try_from(value: &ImageInfo) -> Result<Self, MediaInfoError> {
595        let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
596            .map_err(|_| MediaInfoError::InvalidField)?;
597        let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
598            .map_err(|_| MediaInfoError::InvalidField)?;
599        let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
600            .map_err(|_| MediaInfoError::InvalidField)?;
601        let blurhash = value.blurhash.clone().ok_or(MediaInfoError::MissingField)?;
602
603        Ok(BaseImageInfo {
604            height: Some(height),
605            width: Some(width),
606            size: Some(size),
607            blurhash: Some(blurhash),
608            is_animated: value.is_animated,
609        })
610    }
611}
612
613#[derive(Clone, uniffi::Record)]
614pub struct AudioInfo {
615    pub duration: Option<Duration>,
616    pub size: Option<u64>,
617    pub mimetype: Option<String>,
618}
619
620impl From<AudioInfo> for RumaAudioInfo {
621    fn from(value: AudioInfo) -> Self {
622        assign!(RumaAudioInfo::new(), {
623            duration: value.duration,
624            size: value.size.map(u64_to_uint),
625            mimetype: value.mimetype,
626        })
627    }
628}
629
630impl TryFrom<&AudioInfo> for BaseAudioInfo {
631    type Error = MediaInfoError;
632
633    fn try_from(value: &AudioInfo) -> Result<Self, MediaInfoError> {
634        let duration = value.duration.ok_or(MediaInfoError::MissingField)?;
635        let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
636            .map_err(|_| MediaInfoError::InvalidField)?;
637
638        Ok(BaseAudioInfo { duration: Some(duration), size: Some(size) })
639    }
640}
641
642#[derive(Clone, uniffi::Record)]
643pub struct UnstableAudioDetailsContent {
644    pub duration: Duration,
645    pub waveform: Vec<u16>,
646}
647
648impl From<RumaUnstableAudioDetailsContentBlock> for UnstableAudioDetailsContent {
649    fn from(details: RumaUnstableAudioDetailsContentBlock) -> Self {
650        Self {
651            duration: details.duration,
652            waveform: details
653                .waveform
654                .iter()
655                .map(|x| u16::try_from(x.get()).unwrap_or(0))
656                .collect(),
657        }
658    }
659}
660
661#[derive(Clone, uniffi::Record)]
662pub struct UnstableVoiceContent {}
663
664impl From<RumaUnstableVoiceContentBlock> for UnstableVoiceContent {
665    fn from(_details: RumaUnstableVoiceContentBlock) -> Self {
666        Self {}
667    }
668}
669
670#[derive(Clone, uniffi::Record)]
671pub struct VideoInfo {
672    pub duration: Option<Duration>,
673    pub height: Option<u64>,
674    pub width: Option<u64>,
675    pub mimetype: Option<String>,
676    pub size: Option<u64>,
677    pub thumbnail_info: Option<ThumbnailInfo>,
678    pub thumbnail_source: Option<Arc<MediaSource>>,
679    pub blurhash: Option<String>,
680}
681
682impl From<VideoInfo> for RumaVideoInfo {
683    fn from(value: VideoInfo) -> Self {
684        assign!(RumaVideoInfo::new(), {
685            duration: value.duration,
686            height: value.height.map(u64_to_uint),
687            width: value.width.map(u64_to_uint),
688            mimetype: value.mimetype,
689            size: value.size.map(u64_to_uint),
690            thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
691            thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
692            blurhash: value.blurhash,
693        })
694    }
695}
696
697impl TryFrom<&VideoInfo> for BaseVideoInfo {
698    type Error = MediaInfoError;
699
700    fn try_from(value: &VideoInfo) -> Result<Self, MediaInfoError> {
701        let duration = value.duration.ok_or(MediaInfoError::MissingField)?;
702        let height = UInt::try_from(value.height.ok_or(MediaInfoError::MissingField)?)
703            .map_err(|_| MediaInfoError::InvalidField)?;
704        let width = UInt::try_from(value.width.ok_or(MediaInfoError::MissingField)?)
705            .map_err(|_| MediaInfoError::InvalidField)?;
706        let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
707            .map_err(|_| MediaInfoError::InvalidField)?;
708        let blurhash = value.blurhash.clone().ok_or(MediaInfoError::MissingField)?;
709
710        Ok(BaseVideoInfo {
711            duration: Some(duration),
712            height: Some(height),
713            width: Some(width),
714            size: Some(size),
715            blurhash: Some(blurhash),
716        })
717    }
718}
719
720#[derive(Clone, uniffi::Record)]
721pub struct FileInfo {
722    pub mimetype: Option<String>,
723    pub size: Option<u64>,
724    pub thumbnail_info: Option<ThumbnailInfo>,
725    pub thumbnail_source: Option<Arc<MediaSource>>,
726}
727
728impl From<FileInfo> for RumaFileInfo {
729    fn from(value: FileInfo) -> Self {
730        assign!(RumaFileInfo::new(), {
731            mimetype: value.mimetype,
732            size: value.size.map(u64_to_uint),
733            thumbnail_info: value.thumbnail_info.map(Into::into).map(Box::new),
734            thumbnail_source: value.thumbnail_source.map(|source| (*source).clone().into()),
735        })
736    }
737}
738
739impl TryFrom<&FileInfo> for BaseFileInfo {
740    type Error = MediaInfoError;
741
742    fn try_from(value: &FileInfo) -> Result<Self, MediaInfoError> {
743        let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
744            .map_err(|_| MediaInfoError::InvalidField)?;
745
746        Ok(BaseFileInfo { size: Some(size) })
747    }
748}
749
750#[derive(Clone, uniffi::Record)]
751pub struct ThumbnailInfo {
752    pub height: Option<u64>,
753    pub width: Option<u64>,
754    pub mimetype: Option<String>,
755    pub size: Option<u64>,
756}
757
758impl From<ThumbnailInfo> for RumaThumbnailInfo {
759    fn from(value: ThumbnailInfo) -> Self {
760        assign!(RumaThumbnailInfo::new(), {
761            height: value.height.map(u64_to_uint),
762            width: value.width.map(u64_to_uint),
763            mimetype: value.mimetype,
764            size: value.size.map(u64_to_uint),
765        })
766    }
767}
768
769#[derive(Clone, uniffi::Record)]
770pub struct NoticeMessageContent {
771    pub body: String,
772    pub formatted: Option<FormattedBody>,
773}
774
775#[derive(Clone, uniffi::Record)]
776pub struct TextMessageContent {
777    pub body: String,
778    pub formatted: Option<FormattedBody>,
779}
780
781#[derive(Clone, uniffi::Record)]
782pub struct LocationContent {
783    pub body: String,
784    pub geo_uri: String,
785    pub description: Option<String>,
786    pub zoom_level: Option<u8>,
787    pub asset: Option<AssetType>,
788}
789
790#[derive(Clone, uniffi::Enum)]
791pub enum AssetType {
792    Sender,
793    Pin,
794}
795
796impl From<AssetType> for RumaAssetType {
797    fn from(value: AssetType) -> Self {
798        match value {
799            AssetType::Sender => Self::Self_,
800            AssetType::Pin => Self::Pin,
801        }
802    }
803}
804
805#[derive(Clone, uniffi::Record)]
806pub struct FormattedBody {
807    pub format: MessageFormat,
808    pub body: String,
809}
810
811impl From<FormattedBody> for RumaFormattedBody {
812    fn from(f: FormattedBody) -> Self {
813        Self {
814            format: match f.format {
815                MessageFormat::Html => matrix_sdk::ruma::events::room::message::MessageFormat::Html,
816                MessageFormat::Unknown { format } => format.into(),
817            },
818            body: f.body,
819        }
820    }
821}
822
823impl From<&RumaFormattedBody> for FormattedBody {
824    fn from(f: &RumaFormattedBody) -> Self {
825        Self {
826            format: match &f.format {
827                matrix_sdk::ruma::events::room::message::MessageFormat::Html => MessageFormat::Html,
828                _ => MessageFormat::Unknown { format: f.format.to_string() },
829            },
830            body: f.body.clone(),
831        }
832    }
833}
834
835#[derive(Clone, uniffi::Enum)]
836pub enum MessageFormat {
837    Html,
838    Unknown { format: String },
839}
840
841impl TryFrom<&matrix_sdk::ruma::events::room::ImageInfo> for ImageInfo {
842    type Error = ClientError;
843
844    fn try_from(info: &matrix_sdk::ruma::events::room::ImageInfo) -> Result<Self, Self::Error> {
845        let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
846            height: info.height.map(Into::into),
847            width: info.width.map(Into::into),
848            mimetype: info.mimetype.clone(),
849            size: info.size.map(Into::into),
850        });
851
852        Ok(Self {
853            height: info.height.map(Into::into),
854            width: info.width.map(Into::into),
855            mimetype: info.mimetype.clone(),
856            size: info.size.map(Into::into),
857            thumbnail_info,
858            thumbnail_source: info
859                .thumbnail_source
860                .as_ref()
861                .map(TryInto::try_into)
862                .transpose()?
863                .map(Arc::new),
864            blurhash: info.blurhash.clone(),
865            is_animated: info.is_animated,
866        })
867    }
868}
869
870impl From<&RumaAudioInfo> for AudioInfo {
871    fn from(info: &RumaAudioInfo) -> Self {
872        Self {
873            duration: info.duration,
874            size: info.size.map(Into::into),
875            mimetype: info.mimetype.clone(),
876        }
877    }
878}
879
880impl TryFrom<&RumaVideoInfo> for VideoInfo {
881    type Error = ClientError;
882
883    fn try_from(info: &RumaVideoInfo) -> Result<Self, Self::Error> {
884        let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
885            height: info.height.map(Into::into),
886            width: info.width.map(Into::into),
887            mimetype: info.mimetype.clone(),
888            size: info.size.map(Into::into),
889        });
890
891        Ok(Self {
892            duration: info.duration,
893            height: info.height.map(Into::into),
894            width: info.width.map(Into::into),
895            mimetype: info.mimetype.clone(),
896            size: info.size.map(Into::into),
897            thumbnail_info,
898            thumbnail_source: info
899                .thumbnail_source
900                .as_ref()
901                .map(TryInto::try_into)
902                .transpose()?
903                .map(Arc::new),
904            blurhash: info.blurhash.clone(),
905        })
906    }
907}
908
909impl TryFrom<&RumaFileInfo> for FileInfo {
910    type Error = ClientError;
911
912    fn try_from(info: &RumaFileInfo) -> Result<Self, Self::Error> {
913        let thumbnail_info = info.thumbnail_info.as_ref().map(|info| ThumbnailInfo {
914            height: info.height.map(Into::into),
915            width: info.width.map(Into::into),
916            mimetype: info.mimetype.clone(),
917            size: info.size.map(Into::into),
918        });
919
920        Ok(Self {
921            mimetype: info.mimetype.clone(),
922            size: info.size.map(Into::into),
923            thumbnail_info,
924            thumbnail_source: info
925                .thumbnail_source
926                .as_ref()
927                .map(TryInto::try_into)
928                .transpose()?
929                .map(Arc::new),
930        })
931    }
932}
933
934#[derive(Clone, uniffi::Enum)]
935pub enum PollKind {
936    Disclosed,
937    Undisclosed,
938}
939
940impl From<PollKind> for RumaPollKind {
941    fn from(value: PollKind) -> Self {
942        match value {
943            PollKind::Disclosed => Self::Disclosed,
944            PollKind::Undisclosed => Self::Undisclosed,
945        }
946    }
947}
948
949impl From<RumaPollKind> for PollKind {
950    fn from(value: RumaPollKind) -> Self {
951        match value {
952            RumaPollKind::Disclosed => Self::Disclosed,
953            RumaPollKind::Undisclosed => Self::Undisclosed,
954            _ => {
955                info!("Unknown poll kind, defaulting to undisclosed");
956                Self::Undisclosed
957            }
958        }
959    }
960}
961
962/// Creates a [`RoomMessageEventContentWithoutRelation`] given a
963/// [`MessageContent`] value.
964#[matrix_sdk_ffi_macros::export]
965pub fn content_without_relation_from_message(
966    message: MessageContent,
967) -> Result<Arc<RoomMessageEventContentWithoutRelation>, ClientError> {
968    let msg_type = message.msg_type.try_into()?;
969    Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msg_type)))
970}