Skip to content

Commit

Permalink
ffi: Support sending image attachments through the timeline
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanceriu authored May 2, 2023
1 parent 557d27a commit 17fd4dd
Show file tree
Hide file tree
Showing 4 changed files with 300 additions and 5 deletions.
24 changes: 24 additions & 0 deletions bindings/matrix-sdk-ffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,27 @@ impl From<mime::FromStrError> for ClientError {
anyhow::Error::from(e).into()
}
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum RoomError {
#[error("Invalid attachment data")]
InvalidAttachmentData,
#[error("Invalid attachment mime type")]
InvalidAttachmentMimeType,
#[error("Timeline unavailable")]
TimelineUnavailable,
#[error("Invalid thumbnail data")]
InvalidThumbnailData,
#[error("Failed sending attachment")]
FailedSendingAttachment,
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum TimelineError {
#[error("Required value missing from the media info")]
MissingMediaInfoField,
#[error("Media info field invalid")]
InvalidMediaInfoField,
}
134 changes: 133 additions & 1 deletion bindings/matrix-sdk-ffi/src/room.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use std::{
convert::TryFrom,
fs,
sync::{Arc, RwLock},
};

use anyhow::{anyhow, bail, Context, Result};
use futures_util::StreamExt;
use matrix_sdk::{
attachment::{
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
},
room::{timeline::Timeline, Receipts, Room as SdkRoom},
ruma::{
api::client::{receipt::create_receipt::v3::ReceiptType, room::report_content},
Expand All @@ -25,7 +30,11 @@ use mime::Mime;
use tracing::error;

use super::RUNTIME;
use crate::{error::ClientError, RoomMember, TimelineDiff, TimelineItem, TimelineListener};
use crate::{
error::{ClientError, RoomError},
AudioInfo, FileInfo, ImageInfo, RoomMember, ThumbnailInfo, TimelineDiff, TimelineItem,
TimelineListener, VideoInfo,
};

#[derive(uniffi::Enum)]
pub enum Membership {
Expand Down Expand Up @@ -637,6 +646,129 @@ impl Room {
Ok(())
})
}

pub fn send_image(
&self,
url: String,
thumbnail_url: String,
image_info: ImageInfo,
) -> Result<(), RoomError> {
let mime_str = image_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;

let base_image_info =
BaseImageInfo::try_from(&image_info).map_err(|_| RoomError::InvalidAttachmentData)?;

let attachment_info = AttachmentInfo::Image(base_image_info);

let attachment_config = match image_info.thumbnail_info {
Some(thumbnail_image_info) => {
let thumbnail = self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
}
None => AttachmentConfig::new().info(attachment_info),
};

self.send_attachment(url, mime_type, attachment_config)
}

pub fn send_video(
&self,
url: String,
thumbnail_url: String,
video_info: VideoInfo,
) -> Result<(), RoomError> {
let mime_str = video_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;

let base_video_info: BaseVideoInfo =
BaseVideoInfo::try_from(&video_info).map_err(|_| RoomError::InvalidAttachmentData)?;

let attachment_info = AttachmentInfo::Video(base_video_info);

let attachment_config = match video_info.thumbnail_info {
Some(thumbnail_image_info) => {
let thumbnail = self.build_thumbnail_info(thumbnail_url, thumbnail_image_info)?;
AttachmentConfig::with_thumbnail(thumbnail).info(attachment_info)
}
None => AttachmentConfig::new().info(attachment_info),
};

self.send_attachment(url, mime_type, attachment_config)
}

pub fn send_audio(&self, url: String, audio_info: AudioInfo) -> Result<(), RoomError> {
let mime_str = audio_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;

let base_audio_info: BaseAudioInfo =
BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?;

let attachment_info = AttachmentInfo::Audio(base_audio_info);
let attachment_config = AttachmentConfig::new().info(attachment_info);

self.send_attachment(url, mime_type, attachment_config)
}

pub fn send_file(&self, url: String, file_info: FileInfo) -> Result<(), RoomError> {
let mime_str = file_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;

let base_file_info: BaseFileInfo =
BaseFileInfo::try_from(&file_info).map_err(|_| RoomError::InvalidAttachmentData)?;

let attachment_info = AttachmentInfo::File(base_file_info);
let attachment_config = AttachmentConfig::new().info(attachment_info);

self.send_attachment(url, mime_type, attachment_config)
}
}

impl Room {
fn build_thumbnail_info(
&self,
thumbnail_url: String,
thumbnail_info: ThumbnailInfo,
) -> Result<Thumbnail, RoomError> {
let thumbnail_data =
fs::read(thumbnail_url).map_err(|_| RoomError::InvalidThumbnailData)?;

let base_thumbnail_info = BaseThumbnailInfo::try_from(&thumbnail_info)
.map_err(|_| RoomError::InvalidAttachmentData)?;

let mime_str =
thumbnail_info.mimetype.as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;

Ok(Thumbnail {
data: thumbnail_data,
content_type: mime_type,
info: Some(base_thumbnail_info),
})
}

fn send_attachment(
&self,
url: String,
mime_type: Mime,
attachment_config: AttachmentConfig,
) -> Result<(), RoomError> {
let timeline_guard = self.timeline.read().unwrap();
let timeline = timeline_guard.as_ref().ok_or(RoomError::TimelineUnavailable)?;

RUNTIME.block_on(async move {
timeline
.send_attachment(url, mime_type, attachment_config)
.await
.map_err(|_| RoomError::FailedSendingAttachment)?;
Ok(())
})
}
}

impl std::ops::Deref for Room {
Expand Down
94 changes: 91 additions & 3 deletions bindings/matrix-sdk-ffi/src/timeline.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use std::sync::Arc;
use std::{sync::Arc, time::Duration};

use anyhow::bail;
use extension_trait::extension_trait;
use eyeball_im::VectorDiff;
use matrix_sdk::room::timeline::{Profile, TimelineDetails};
pub use matrix_sdk::ruma::events::room::{message::RoomMessageEventContent, MediaSource};
use matrix_sdk::{
attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo},
room::timeline::{Profile, TimelineDetails},
};
use ruma::UInt;
use tracing::warn;

use crate::helpers::unwrap_or_clone_arc;
use crate::{error::TimelineError, helpers::unwrap_or_clone_arc};

#[uniffi::export]
pub fn media_source_from_url(url: String) -> Arc<MediaSource> {
Expand Down Expand Up @@ -582,6 +586,27 @@ pub struct ImageInfo {
pub blurhash: Option<String>,
}

impl TryFrom<&ImageInfo> for BaseImageInfo {
type Error = TimelineError;

fn try_from(value: &ImageInfo) -> Result<Self, TimelineError> {
let height = UInt::try_from(value.height.ok_or(TimelineError::MissingMediaInfoField)?)
.map_err(|_| TimelineError::InvalidMediaInfoField)?;
let width = UInt::try_from(value.width.ok_or(TimelineError::MissingMediaInfoField)?)
.map_err(|_| TimelineError::InvalidMediaInfoField)?;
let size = UInt::try_from(value.size.ok_or(TimelineError::MissingMediaInfoField)?)
.map_err(|_| TimelineError::InvalidMediaInfoField)?;
let blurhash = value.blurhash.clone().ok_or(TimelineError::MissingMediaInfoField)?;

Ok(BaseImageInfo {
height: Some(height),
width: Some(width),
size: Some(size),
blurhash: Some(blurhash),
})
}
}

#[derive(Clone, uniffi::Record)]
pub struct AudioInfo {
// FIXME: duration should be a std::time::Duration once the UniFFI proc-macro API adds support
Expand All @@ -591,6 +616,19 @@ pub struct AudioInfo {
pub mimetype: Option<String>,
}

impl TryFrom<&AudioInfo> for BaseAudioInfo {
type Error = TimelineError;

fn try_from(value: &AudioInfo) -> Result<Self, TimelineError> {
let duration =
value.duration.map(Duration::from_secs).ok_or(TimelineError::MissingMediaInfoField)?;
let size = UInt::try_from(value.size.ok_or(TimelineError::MissingMediaInfoField)?)
.map_err(|_| TimelineError::InvalidMediaInfoField)?;

Ok(BaseAudioInfo { duration: Some(duration), size: Some(size) })
}
}

#[derive(Clone, uniffi::Record)]
pub struct VideoInfo {
pub duration: Option<u64>,
Expand All @@ -603,6 +641,30 @@ pub struct VideoInfo {
pub blurhash: Option<String>,
}

impl TryFrom<&VideoInfo> for BaseVideoInfo {
type Error = TimelineError;

fn try_from(value: &VideoInfo) -> Result<Self, TimelineError> {
let duration =
value.duration.map(Duration::from_secs).ok_or(TimelineError::MissingMediaInfoField)?;
let height = UInt::try_from(value.height.ok_or(TimelineError::MissingMediaInfoField)?)
.map_err(|_| TimelineError::InvalidMediaInfoField)?;
let width = UInt::try_from(value.width.ok_or(TimelineError::MissingMediaInfoField)?)
.map_err(|_| TimelineError::InvalidMediaInfoField)?;
let size = UInt::try_from(value.size.ok_or(TimelineError::MissingMediaInfoField)?)
.map_err(|_| TimelineError::InvalidMediaInfoField)?;
let blurhash = value.blurhash.clone().ok_or(TimelineError::MissingMediaInfoField)?;

Ok(BaseVideoInfo {
duration: Some(duration),
height: Some(height),
width: Some(width),
size: Some(size),
blurhash: Some(blurhash),
})
}
}

#[derive(Clone, uniffi::Record)]
pub struct FileInfo {
pub mimetype: Option<String>,
Expand All @@ -611,6 +673,17 @@ pub struct FileInfo {
pub thumbnail_source: Option<Arc<MediaSource>>,
}

impl TryFrom<&FileInfo> for BaseFileInfo {
type Error = TimelineError;

fn try_from(value: &FileInfo) -> Result<Self, TimelineError> {
let size = UInt::try_from(value.size.ok_or(TimelineError::MissingMediaInfoField)?)
.map_err(|_| TimelineError::InvalidMediaInfoField)?;

Ok(BaseFileInfo { size: Some(size) })
}
}

#[derive(Clone, uniffi::Record)]
pub struct ThumbnailInfo {
pub height: Option<u64>,
Expand All @@ -619,6 +692,21 @@ pub struct ThumbnailInfo {
pub size: Option<u64>,
}

impl TryFrom<&ThumbnailInfo> for BaseThumbnailInfo {
type Error = TimelineError;

fn try_from(value: &ThumbnailInfo) -> Result<Self, TimelineError> {
let height = UInt::try_from(value.height.ok_or(TimelineError::MissingMediaInfoField)?)
.map_err(|_| TimelineError::InvalidMediaInfoField)?;
let width = UInt::try_from(value.width.ok_or(TimelineError::MissingMediaInfoField)?)
.map_err(|_| TimelineError::InvalidMediaInfoField)?;
let size = UInt::try_from(value.size.ok_or(TimelineError::MissingMediaInfoField)?)
.map_err(|_| TimelineError::InvalidMediaInfoField)?;

Ok(BaseThumbnailInfo { height: Some(height), width: Some(width), size: Some(size) })
}
}

#[derive(Clone, uniffi::Record)]
pub struct NoticeMessageContent {
pub body: String,
Expand Down
Loading

0 comments on commit 17fd4dd

Please sign in to comment.