matrix-sdk: Move room specific methods to room structs

master
Julian Sparber 2021-03-09 00:12:59 +01:00
parent 450036cf86
commit 31dd031269
9 changed files with 952 additions and 722 deletions

View File

@ -4,7 +4,7 @@ use tokio::time::{sleep, Duration};
use matrix_sdk::{
self, async_trait,
events::{room::member::MemberEventContent, StrippedStateEvent},
Client, ClientConfig, EventHandler, Room, RoomType, SyncSettings,
room, Client, ClientConfig, EventHandler, Room, SyncSettings,
};
use url::Url;
@ -30,11 +30,11 @@ impl EventHandler for AutoJoinBot {
return;
}
if room.room_type() == RoomType::Invited {
if let Some(room) = room::Invited::new(self.client.clone(), room) {
println!("Autojoining room {}", room.room_id());
let mut delay = 2;
while let Err(err) = self.client.join_room_by_id(room.room_id()).await {
while let Err(err) = room.accept_invitation().await {
// retry autojoin due to synapse sending invites, before the
// invited user can join for more information see
// https://github.com/matrix-org/synapse/issues/4345

View File

@ -6,7 +6,8 @@ use matrix_sdk::{
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
AnyMessageEventContent, SyncMessageEvent,
},
Client, ClientConfig, EventHandler, Room, RoomType, SyncSettings,
room::Joined,
Client, ClientConfig, EventHandler, Room, SyncSettings,
};
use url::Url;
@ -25,7 +26,7 @@ impl CommandBot {
#[async_trait]
impl EventHandler for CommandBot {
async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
if room.room_type() == RoomType::Joined {
if let Some(room) = Joined::new(self.client.clone(), room) {
let msg_body = if let SyncMessageEvent {
content:
MessageEventContent {
@ -47,12 +48,9 @@ impl EventHandler for CommandBot {
println!("sending");
self.client
// send our message to the room we found the "!party" command in
// the last parameter is an optional Uuid which we don't care about.
.room_send(room.room_id(), content, None)
.await
.unwrap();
// send our message to the room we found the "!party" command in
// the last parameter is an optional Uuid which we don't care about.
room.send(content, None).await.unwrap();
println!("message sent");
}

View File

@ -14,7 +14,8 @@ use matrix_sdk::{
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
SyncMessageEvent,
},
Client, EventHandler, Room, RoomType, SyncSettings,
room::Joined,
Client, EventHandler, Room, SyncSettings,
};
use url::Url;
@ -33,7 +34,7 @@ impl ImageBot {
#[async_trait]
impl EventHandler for ImageBot {
async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
if room.room_type() == RoomType::Joined {
if let Some(room) = Joined::new(self.client.clone(), room) {
let msg_body = if let SyncMessageEvent {
content:
MessageEventContent {
@ -52,14 +53,7 @@ impl EventHandler for ImageBot {
println!("sending image");
let mut image = self.image.lock().await;
self.client
.room_send_attachment(
room.room_id(),
"cat",
&mime::IMAGE_JPEG,
&mut *image,
None,
)
room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, None)
.await
.unwrap();

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,16 @@
use crate::{Client, Room};
use matrix_sdk_common::api::r0::{
membership::{get_member_events, join_room_by_id, leave_room},
message::get_message_events,
};
use std::ops::Deref;
use crate::{Client, Result, Room, RoomMember};
/// A struct containing methodes that are common for Joined, Invited and Left Rooms
#[derive(Debug, Clone)]
pub struct Common {
inner: Room,
client: Client,
pub(crate) client: Client,
}
impl Deref for Common {
@ -24,11 +29,98 @@ impl Common {
///
/// * `room` - The underlaying room.
pub fn new(client: Client, room: Room) -> Self {
// TODO: Make this private
Self {
inner: room,
client,
}
}
// TODO: add common mehtods e.g forget_room()
/// Leave this room.
///
/// Only invited and joined rooms can be left
pub(crate) async fn leave(&self) -> Result<()> {
let request = leave_room::Request::new(self.inner.room_id());
let _response = self.client.send(request, None).await?;
Ok(())
}
/// Join this room.
///
/// Only invited and left rooms can be joined via this method
pub(crate) async fn join(&self) -> Result<()> {
let request = join_room_by_id::Request::new(self.inner.room_id());
let _resposne = self.client.send(request, None).await?;
Ok(())
}
/// Sends a request to `/_matrix/client/r0/rooms/{room_id}/messages` and returns
/// a `get_message_events::Response` that contains a chunk of room and state events
/// (`AnyRoomEvent` and `AnyStateEvent`).
///
/// # Arguments
///
/// * `request` - The easiest way to create this request is using the
/// `get_message_events::Request` itself.
///
/// # Examples
/// ```no_run
/// # use std::convert::TryFrom;
/// use matrix_sdk::Client;
/// # use matrix_sdk::identifiers::room_id;
/// # use matrix_sdk::api::r0::filter::RoomEventFilter;
/// # use matrix_sdk::api::r0::message::get_message_events::Request as MessagesRequest;
/// # use url::Url;
///
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// let room_id = room_id!("!roomid:example.com");
/// let request = MessagesRequest::backward(&room_id, "t47429-4392820_219380_26003_2265");
///
/// let mut client = Client::new(homeserver).unwrap();
/// # let room = client
/// # .get_joined_room(&room_id)
/// # .unwrap();
/// # use futures::executor::block_on;
/// # block_on(async {
/// assert!(room.messages(request).await.is_ok());
/// # });
/// ```
pub async fn messages(
&self,
request: impl Into<get_message_events::Request<'_>>,
) -> Result<get_message_events::Response> {
let request = request.into();
self.client.send(request, None).await
}
pub(crate) async fn request_members(&self) -> Result<()> {
// TODO: don't send a request if a request is being sent
let request = get_member_events::Request::new(self.inner.room_id());
let response = self.client.send(request, None).await?;
self.client
.base_client
.receive_members(self.inner.room_id(), &response)
.await?;
Ok(())
}
/// Get active members for this room, includes invited, joined members.
pub async fn active_members(&self) -> Result<Vec<RoomMember>> {
if !self.are_members_synced() {
self.request_members().await?;
}
Ok(self.inner.active_members().await?)
}
/// Get all members for this room, includes invited, joined and left members.
pub async fn members(&self) -> Result<Vec<RoomMember>> {
if !self.are_members_synced() {
self.request_members().await?;
}
Ok(self.inner.members().await?)
}
}

View File

@ -1,4 +1,4 @@
use crate::{room::Common, Client, Room, RoomType};
use crate::{room::Common, Client, Result, Room, RoomType};
use std::ops::Deref;
/// A room in the invited state.
@ -18,6 +18,7 @@ impl Invited {
///
/// * `room` - The underlaying room.
pub fn new(client: Client, room: Room) -> Option<Self> {
// TODO: Make this private
if room.room_type() == RoomType::Invited {
Some(Self {
inner: Common::new(client, room),
@ -26,6 +27,16 @@ impl Invited {
None
}
}
/// Reject the invitation.
pub async fn reject_invitation(&self) -> Result<()> {
self.inner.leave().await
}
/// Accept the invitation.
pub async fn accept_invitation(&self) -> Result<()> {
self.inner.join().await
}
}
impl Deref for Invited {

View File

@ -1,5 +1,44 @@
use crate::{room::Common, Client, Room, RoomType};
use std::ops::Deref;
use crate::{room::Common, Client, Result, Room, RoomType};
use std::{io::Read, ops::Deref, sync::Arc};
use matrix_sdk_common::{
api::r0::{
membership::{
ban_user,
invite_user::{self, InvitationRecipient},
kick_user, Invite3pid,
},
message::send_message_event,
read_marker::set_read_marker,
receipt::create_receipt,
state::send_state_event_for_key,
typing::create_typing_event::{Request as TypingRequest, Typing},
},
assign,
events::{
room::{
message::{
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent,
MessageEventContent, MessageType, VideoMessageEventContent,
},
EncryptedFile,
},
AnyMessageEventContent, AnyStateEventContent,
},
identifiers::{EventId, UserId},
uuid::Uuid,
};
use mime::{self, Mime};
#[cfg(feature = "encryption")]
use matrix_sdk_common::locks::Mutex;
#[cfg(feature = "encryption")]
use matrix_sdk_base::crypto::AttachmentEncryptor;
#[cfg(feature = "encryption")]
use tracing::instrument;
/// A room in the joined state.
///
@ -26,6 +65,7 @@ impl Joined {
///
/// * `room` - The underlaying room.
pub fn new(client: Client, room: Room) -> Option<Self> {
// TODO: Make this private
if room.room_type() == RoomType::Joined {
Some(Self {
inner: Common::new(client, room),
@ -34,4 +74,491 @@ impl Joined {
None
}
}
/// Leave this room.
pub async fn leave(&self) -> Result<()> {
self.inner.leave().await
}
/// Ban the user with `UserId` from this room.
///
/// # Arguments
///
/// * `user_id` - The user to ban with `UserId`.
///
/// * `reason` - The reason for banning this user.
pub async fn ban_user(&self, user_id: &UserId, reason: Option<&str>) -> Result<()> {
let request = assign!(ban_user::Request::new(self.inner.room_id(), user_id), {
reason
});
self.client.send(request, None).await?;
Ok(())
}
/// Kick a user out of this room.
///
/// # Arguments
///
/// * `user_id` - The `UserId` of the user that should be kicked out of the room.
///
/// * `reason` - Optional reason why the room member is being kicked out.
pub async fn kick_user(&self, user_id: &UserId, reason: Option<&str>) -> Result<()> {
let request = assign!(kick_user::Request::new(self.inner.room_id(), user_id), {
reason
});
self.client.send(request, None).await?;
Ok(())
}
/// Invite the specified user by `UserId` to this room.
///
/// # Arguments
///
/// * `user_id` - The `UserId` of the user to invite to the room.
pub async fn invite_user_by_id(&self, user_id: &UserId) -> Result<()> {
let recipient = InvitationRecipient::UserId { user_id };
let request = invite_user::Request::new(self.inner.room_id(), recipient);
self.client.send(request, None).await?;
Ok(())
}
/// Invite the specified user by third party id to this room.
///
/// # Arguments
///
/// * `invite_id` - A third party id of a user to invite to the room.
pub async fn invite_user_by_3pid(&self, invite_id: Invite3pid<'_>) -> Result<()> {
let recipient = InvitationRecipient::ThirdPartyId(invite_id);
let request = invite_user::Request::new(self.inner.room_id(), recipient);
self.client.send(request, None).await?;
Ok(())
}
/// Send a request to notify this room of a user typing.
///
/// # Arguments
///
/// * `typing` - Whether the user is typing, and how long.
///
/// # Examples
///
/// ```no_run
/// # use std::time::Duration;
/// # use matrix_sdk::{
/// # Client, SyncSettings,
/// # api::r0::typing::create_typing_event::Typing,
/// # identifiers::room_id,
/// # };
/// # use futures::executor::block_on;
/// # use url::Url;
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
/// # let mut client = Client::new(homeserver).unwrap();
/// # let room_id = room_id!("!test:localhost");
/// # let room = client
/// # .get_joined_room(&room_id!("!SVkFJHzfwvuaIEawgC:localhost"))
/// # .unwrap();
/// # room
/// .typing_notice(Typing::Yes(Duration::from_secs(4)))
/// .await
/// .expect("Can't get devices from server");
/// # });
///
/// ```
pub async fn typing_notice(&self, typing: impl Into<Typing>) -> Result<()> {
// TODO: don't send a request if a typing notice is being sent or is already active
let request = TypingRequest::new(
self.inner.own_user_id(),
self.inner.room_id(),
typing.into(),
);
self.client.send(request, None).await?;
Ok(())
}
/// Send a request to notify this room that the user has read specific event.
///
/// # Arguments
///
/// * `event_id` - The `EventId` specifies the event to set the read receipt on.
pub async fn read_receipt(&self, event_id: &EventId) -> Result<()> {
let request = create_receipt::Request::new(
self.inner.room_id(),
create_receipt::ReceiptType::Read,
event_id,
);
self.client.send(request, None).await?;
Ok(())
}
/// Send a request to notify this room that the user has read up to specific event.
///
/// # Arguments
///
/// * fully_read - The `EventId` of the event the user has read to.
///
/// * read_receipt - An `EventId` to specify the event to set the read receipt on.
pub async fn read_marker(
&self,
fully_read: &EventId,
read_receipt: Option<&EventId>,
) -> Result<()> {
let request = assign!(
set_read_marker::Request::new(self.inner.room_id(), fully_read),
{ read_receipt }
);
self.client.send(request, None).await?;
Ok(())
}
/// Share a group session for the given room.
///
/// This will create Olm sessions with all the users/device pairs in the
/// room if necessary and share a group session with them.
///
/// Does nothing if no group session needs to be shared.
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
async fn preshare_group_session(&self) -> Result<()> {
// TODO expose this publicly so people can pre-share a group session if
// e.g. a user starts to type a message for a room.
#[allow(clippy::map_clone)]
if let Some(mutex) = self
.client
.group_session_locks
.get(self.inner.room_id())
.map(|m| m.clone())
{
// If a group session share request is already going on,
// await the release of the lock.
mutex.lock().await;
} else {
// Otherwise create a new lock and share the group
// session.
let mutex = Arc::new(Mutex::new(()));
self.client
.group_session_locks
.insert(self.inner.room_id().clone(), mutex.clone());
let _guard = mutex.lock().await;
{
let joined = self
.client
.store()
.get_joined_user_ids(self.inner.room_id())
.await?;
let invited = self
.client
.store()
.get_invited_user_ids(self.inner.room_id())
.await?;
let members = joined.iter().chain(&invited);
self.client.claim_one_time_keys(members).await?;
};
let response = self.share_group_session().await;
self.client.group_session_locks.remove(self.inner.room_id());
// If one of the responses failed invalidate the group
// session as using it would end up in undecryptable
// messages.
if let Err(r) = response {
self.client
.base_client
.invalidate_group_session(self.inner.room_id())
.await;
return Err(r);
}
}
Ok(())
}
/// Share a group session for a room.
///
/// # Panics
///
/// Panics if the client isn't logged in.
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
#[instrument]
async fn share_group_session(&self) -> Result<()> {
let mut requests = self
.client
.base_client
.share_group_session(self.inner.room_id())
.await?;
for request in requests.drain(..) {
let response = self.client.send_to_device(&request).await?;
self.client
.base_client
.mark_request_as_sent(&request.txn_id, &response)
.await?;
}
Ok(())
}
/// Send a room message to this room.
///
/// Returns the parsed response from the server.
///
/// If the encryption feature is enabled this method will transparently
/// encrypt the room message if this room is encrypted.
///
/// # Arguments
///
/// * `content` - The content of the message event.
///
/// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent`
/// held in its unsigned field as `transaction_id`. If not given one is
/// created for the message.
///
/// # Example
/// ```no_run
/// # use std::sync::{Arc, RwLock};
/// # use matrix_sdk::{Client, SyncSettings};
/// # use url::Url;
/// # use futures::executor::block_on;
/// # use matrix_sdk::identifiers::room_id;
/// # use std::convert::TryFrom;
/// use matrix_sdk::events::{
/// AnyMessageEventContent,
/// room::message::{MessageEventContent, TextMessageEventContent},
/// };
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
/// # let mut client = Client::new(homeserver).unwrap();
/// # let room_id = room_id!("!test:localhost");
/// use matrix_sdk_common::uuid::Uuid;
///
/// let content = AnyMessageEventContent::RoomMessage(
/// MessageEventContent::text_plain("Hello world")
/// );
///
/// let txn_id = Uuid::new_v4();
/// # let room = client
/// # .get_joined_room(&room_id)
/// # .unwrap();
/// # room.send(content, Some(txn_id)).await.unwrap();
/// # })
/// ```
pub async fn send(
&self,
content: impl Into<AnyMessageEventContent>,
txn_id: Option<Uuid>,
) -> Result<send_message_event::Response> {
#[cfg(not(feature = "encryption"))]
let content: AnyMessageEventContent = content.into();
#[cfg(feature = "encryption")]
let content = if self.is_encrypted() {
if !self.are_members_synced() {
self.request_members().await?;
// TODO query keys here?
}
self.preshare_group_session().await?;
AnyMessageEventContent::RoomEncrypted(
self.client
.base_client
.encrypt(self.inner.room_id(), content)
.await?,
)
} else {
content.into()
};
let txn_id = txn_id.unwrap_or_else(Uuid::new_v4).to_string();
let request = send_message_event::Request::new(&self.inner.room_id(), &txn_id, &content);
let response = self.client.send(request, None).await?;
Ok(response)
}
/// Send an attachment to this room.
///
/// This will upload the given data that the reader produces using the
/// [`upload()`](#method.upload) method and post an event to the given room.
/// If the room is encrypted and the encryption feature is enabled the
/// upload will be encrypted.
///
/// This is a convenience method that calls the [`Client::upload()`](#Client::method.upload)
/// and afterwards the [`send()`](#method.send).
///
/// # Arguments
/// * `body` - A textual representation of the media that is going to be
/// uploaded. Usually the file name.
///
/// * `content_type` - The type of the media, this will be used as the
/// content-type header.
///
/// * `reader` - A `Reader` that will be used to fetch the raw bytes of the
/// media.
///
/// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent`
/// held in its unsigned field as `transaction_id`. If not given one is
/// created for the message.
///
/// # Examples
///
/// ```no_run
/// # use std::{path::PathBuf, fs::File, io::Read};
/// # use matrix_sdk::{Client, identifiers::room_id};
/// # use url::Url;
/// # use mime;
/// # use futures::executor::block_on;
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
/// # let mut client = Client::new(homeserver).unwrap();
/// # let room_id = room_id!("!test:localhost");
/// let path = PathBuf::from("/home/example/my-cat.jpg");
/// let mut image = File::open(path).unwrap();
///
/// # let room = client
/// # .get_joined_room(&room_id)
/// # .unwrap();
/// # room.send_attachment("My favorite cat", &mime::IMAGE_JPEG, &mut image, None)
/// .await
/// .expect("Can't upload my cat.");
/// # });
/// ```
pub async fn send_attachment<R: Read>(
&self,
body: &str,
content_type: &Mime,
mut reader: &mut R,
txn_id: Option<Uuid>,
) -> Result<send_message_event::Response> {
let (response, encrypted_file) = if self.is_encrypted() {
#[cfg(feature = "encryption")]
let mut reader = AttachmentEncryptor::new(reader);
#[cfg(feature = "encryption")]
let content_type = mime::APPLICATION_OCTET_STREAM;
let response = self.client.upload(&content_type, &mut reader).await?;
#[cfg(feature = "encryption")]
let keys = {
let keys = reader.finish();
Some(Box::new(EncryptedFile {
url: response.content_uri.clone(),
key: keys.web_key,
iv: keys.iv,
hashes: keys.hashes,
v: keys.version,
}))
};
#[cfg(not(feature = "encryption"))]
let keys: Option<Box<EncryptedFile>> = None;
(response, keys)
} else {
let response = self.client.upload(&content_type, &mut reader).await?;
(response, None)
};
let url = response.content_uri;
let content = match content_type.type_() {
mime::IMAGE => {
// TODO create a thumbnail using the image crate?.
MessageType::Image(ImageMessageEventContent {
body: body.to_owned(),
info: None,
url: Some(url),
file: encrypted_file,
})
}
mime::AUDIO => MessageType::Audio(AudioMessageEventContent {
body: body.to_owned(),
info: None,
url: Some(url),
file: encrypted_file,
}),
mime::VIDEO => MessageType::Video(VideoMessageEventContent {
body: body.to_owned(),
info: None,
url: Some(url),
file: encrypted_file,
}),
_ => MessageType::File(FileMessageEventContent {
filename: None,
body: body.to_owned(),
info: None,
url: Some(url),
file: encrypted_file,
}),
};
self.send(
AnyMessageEventContent::RoomMessage(MessageEventContent::new(content)),
txn_id,
)
.await
}
/// Send a room state event to the homeserver.
///
/// Returns the parsed response from the server.
///
/// # Arguments
///
/// * `room_id` - The id of the room that should receive the message.
///
/// * `content` - The content of the state event.
///
/// * `state_key` - A unique key which defines the overwriting semantics for
/// this piece of room state. This value is often a zero-length string.
///
/// # Example
///
/// ```no_run
/// use matrix_sdk::events::{
/// AnyStateEventContent,
/// room::member::{MemberEventContent, MembershipState},
/// };
/// # futures::executor::block_on(async {
/// # let homeserver = url::Url::parse("http://localhost:8080").unwrap();
/// # let mut client = matrix_sdk::Client::new(homeserver).unwrap();
/// # let room_id = matrix_sdk::identifiers::room_id!("!test:localhost");
///
/// let avatar_url = "https://example.org/avatar";
/// let member_event = MemberEventContent {
/// avatar_url: Some(avatar_url.to_string()),
/// membership: MembershipState::Join,
/// is_direct: None,
/// displayname: None,
/// third_party_invite: None,
/// };
///
/// # let room = client
/// # .get_joined_room(&room_id)
/// # .unwrap();
///
/// let content = AnyStateEventContent::RoomMember(member_event);
/// room.send_state_event(content, "").await.unwrap();
/// # })
/// ```
pub async fn send_state_event(
&self,
content: impl Into<AnyStateEventContent>,
state_key: &str,
) -> Result<send_state_event_for_key::Response> {
let content = content.into();
let request =
send_state_event_for_key::Request::new(self.inner.room_id(), state_key, &content);
self.client.send(request, None).await
}
}

View File

@ -1,6 +1,8 @@
use crate::{room::Common, Client, Room, RoomType};
use crate::{room::Common, Client, Result, Room, RoomType};
use std::ops::Deref;
use matrix_sdk_common::api::r0::membership::forget_room;
/// A room in the left state.
///
/// This struct contains all methodes specific to a `Room` with type `RoomType::Left`.
@ -18,6 +20,7 @@ impl Left {
///
/// * `room` - The underlaying room.
pub fn new(client: Client, room: Room) -> Option<Self> {
// TODO: Make this private
if room.room_type() == RoomType::Left {
Some(Self {
inner: Common::new(client, room),
@ -26,6 +29,21 @@ impl Left {
None
}
}
/// Join this room.
pub async fn join(&self) -> Result<()> {
self.inner.join().await
}
/// Forget this room.
///
/// This communicates to the homeserver that it should forget the room.
pub async fn forget(&self) -> Result<()> {
let request = forget_room::Request::new(self.inner.room_id());
let _response = self.client.send(request, None).await?;
Ok(())
}
}
impl Deref for Left {

View File

@ -16,13 +16,13 @@ use matrix_sdk_base::{
crypto::VerificationRequest as BaseVerificationRequest, events::AnyMessageEventContent,
};
use crate::{Client, Result};
use crate::{room::Joined, Result};
/// An object controling the interactive verification flow.
#[derive(Debug, Clone)]
pub struct VerificationRequest {
pub(crate) inner: BaseVerificationRequest,
pub(crate) client: Client,
pub(crate) room: Joined,
}
impl VerificationRequest {
@ -30,10 +30,7 @@ impl VerificationRequest {
pub async fn accept(&self) -> Result<()> {
if let Some(content) = self.inner.accept() {
let content = AnyMessageEventContent::KeyVerificationReady(content);
self.client
.room_send(self.inner.room_id(), content, None)
.await?;
self.room.send(content, None).await?;
}
Ok(())