Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept any string as a key for m.direct account data #1946

Merged
merged 10 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/ruma-events/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# [unreleased]

Breaking changes:

- Take newly introduced `DirectUserIdentifier(str)` as a key for `DirectEventContent`.
This change allows to have an email or MSISDN phone number as a key for example,
which can be used when issuing invites through third-party systems.
`DirectUserIdentifier` can easily be converted to an `UserId`.

# 0.29.1

Bug fixes:
Expand Down
204 changes: 188 additions & 16 deletions crates/ruma-events/src/direct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,155 @@ use std::{
ops::{Deref, DerefMut},
};

use ruma_common::{OwnedRoomId, OwnedUserId};
use ruma_macros::EventContent;
use ruma_common::{IdParseError, OwnedRoomId, OwnedUserId, UserId};
use ruma_macros::{EventContent, IdZst};
use serde::{Deserialize, Serialize};

/// An user identifier, it can be a [`UserId`] or a third-party identifier
/// like an email or a phone number.
///
/// There is no validation on this type, any string is allowed,
/// but you can use `as_user_id` or `into_user_id` to try to get an [`UserId`].
#[repr(transparent)]
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)]
pub struct DirectUserIdentifier(str);

impl DirectUserIdentifier {
/// Get this `DirectUserIdentifier` as an [`UserId`] if it is one.
pub fn as_user_id(&self) -> Option<&UserId> {
self.0.try_into().ok()
}
}

impl OwnedDirectUserIdentifier {
/// Get this `OwnedDirectUserIdentifier` as an [`UserId`] if it is one.
pub fn as_user_id(&self) -> Option<&UserId> {
self.0.try_into().ok()
}

/// Get this `OwnedDirectUserIdentifier` as an [`OwnedUserId`] if it is one.
pub fn into_user_id(self) -> Option<OwnedUserId> {
OwnedUserId::try_from(self).ok()
}
}

impl TryFrom<OwnedDirectUserIdentifier> for OwnedUserId {
MatMaul marked this conversation as resolved.
Show resolved Hide resolved
type Error = IdParseError;

fn try_from(value: OwnedDirectUserIdentifier) -> Result<Self, Self::Error> {
value.0.try_into()
}
}

impl TryFrom<&OwnedDirectUserIdentifier> for OwnedUserId {
type Error = IdParseError;

fn try_from(value: &OwnedDirectUserIdentifier) -> Result<Self, Self::Error> {
value.0.try_into()
}
}

impl TryFrom<&DirectUserIdentifier> for OwnedUserId {
type Error = IdParseError;

fn try_from(value: &DirectUserIdentifier) -> Result<Self, Self::Error> {
value.0.try_into()
}
}

impl<'a> TryFrom<&'a DirectUserIdentifier> for &'a UserId {
type Error = IdParseError;

fn try_from(value: &'a DirectUserIdentifier) -> Result<Self, Self::Error> {
value.0.try_into()
}
}

impl From<OwnedUserId> for OwnedDirectUserIdentifier {
fn from(value: OwnedUserId) -> Self {
DirectUserIdentifier::from_borrowed(value.as_str()).to_owned()
}
}

impl From<&OwnedUserId> for OwnedDirectUserIdentifier {
fn from(value: &OwnedUserId) -> Self {
DirectUserIdentifier::from_borrowed(value.as_str()).to_owned()
}
}

impl From<&UserId> for OwnedDirectUserIdentifier {
fn from(value: &UserId) -> Self {
DirectUserIdentifier::from_borrowed(value.as_str()).to_owned()
}
}

impl<'a> From<&'a UserId> for &'a DirectUserIdentifier {
fn from(value: &'a UserId) -> Self {
DirectUserIdentifier::from_borrowed(value.as_str())
}
}

impl PartialEq<&UserId> for &DirectUserIdentifier {
MatMaul marked this conversation as resolved.
Show resolved Hide resolved
fn eq(&self, other: &&UserId) -> bool {
self.0.eq(other.as_str())
}
}

impl PartialEq<&DirectUserIdentifier> for &UserId {
fn eq(&self, other: &&DirectUserIdentifier) -> bool {
other.0.eq(self.as_str())
}
}

impl PartialEq<OwnedUserId> for &DirectUserIdentifier {
fn eq(&self, other: &OwnedUserId) -> bool {
self.0.eq(other.as_str())
}
}

impl PartialEq<&DirectUserIdentifier> for OwnedUserId {
fn eq(&self, other: &&DirectUserIdentifier) -> bool {
other.0.eq(self.as_str())
}
}

impl PartialEq<&UserId> for OwnedDirectUserIdentifier {
fn eq(&self, other: &&UserId) -> bool {
self.0.eq(other.as_str())
}
}

impl PartialEq<OwnedDirectUserIdentifier> for &UserId {
fn eq(&self, other: &OwnedDirectUserIdentifier) -> bool {
other.0.eq(self.as_str())
}
}

impl PartialEq<OwnedUserId> for OwnedDirectUserIdentifier {
fn eq(&self, other: &OwnedUserId) -> bool {
self.0.eq(other.as_str())
}
}

impl PartialEq<OwnedDirectUserIdentifier> for OwnedUserId {
fn eq(&self, other: &OwnedDirectUserIdentifier) -> bool {
other.0.eq(self.as_str())
}
}

/// The content of an `m.direct` event.
///
/// A mapping of `UserId`s to a list of `RoomId`s which are considered *direct* for that particular
/// user.
/// A mapping of `DirectUserIdentifier`s to a list of `RoomId`s which are considered *direct*
/// for that particular user.
///
/// Informs the client about the rooms that are considered direct by a user.
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
#[allow(clippy::exhaustive_structs)]
#[ruma_event(type = "m.direct", kind = GlobalAccountData)]
pub struct DirectEventContent(pub BTreeMap<OwnedUserId, Vec<OwnedRoomId>>);
pub struct DirectEventContent(pub BTreeMap<OwnedDirectUserIdentifier, Vec<OwnedRoomId>>);

impl Deref for DirectEventContent {
type Target = BTreeMap<OwnedUserId, Vec<OwnedRoomId>>;
type Target = BTreeMap<OwnedDirectUserIdentifier, Vec<OwnedRoomId>>;

fn deref(&self) -> &Self::Target {
&self.0
Expand All @@ -37,18 +169,18 @@ impl DerefMut for DirectEventContent {
}

impl IntoIterator for DirectEventContent {
type Item = (OwnedUserId, Vec<OwnedRoomId>);
type IntoIter = btree_map::IntoIter<OwnedUserId, Vec<OwnedRoomId>>;
type Item = (OwnedDirectUserIdentifier, Vec<OwnedRoomId>);
type IntoIter = btree_map::IntoIter<OwnedDirectUserIdentifier, Vec<OwnedRoomId>>;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

impl FromIterator<(OwnedUserId, Vec<OwnedRoomId>)> for DirectEventContent {
impl FromIterator<(OwnedDirectUserIdentifier, Vec<OwnedRoomId>)> for DirectEventContent {
fn from_iter<T>(iter: T) -> Self
where
T: IntoIterator<Item = (OwnedUserId, Vec<OwnedRoomId>)>,
T: IntoIterator<Item = (OwnedDirectUserIdentifier, Vec<OwnedRoomId>)>,
{
Self(BTreeMap::from_iter(iter))
}
Expand All @@ -58,42 +190,82 @@ impl FromIterator<(OwnedUserId, Vec<OwnedRoomId>)> for DirectEventContent {
mod tests {
use std::collections::BTreeMap;

use ruma_common::{owned_room_id, owned_user_id};
use ruma_common::{owned_room_id, user_id, OwnedUserId};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};

use super::{DirectEvent, DirectEventContent};
use crate::direct::{DirectUserIdentifier, OwnedDirectUserIdentifier};

#[test]
fn serialization() {
let mut content = DirectEventContent(BTreeMap::new());
let alice = owned_user_id!("@alice:ruma.io");
let alice = user_id!("@alice:ruma.io");
let alice_mail = "[email protected]";
let rooms = vec![owned_room_id!("!1:ruma.io")];
let mail_rooms = vec![owned_room_id!("!3:ruma.io")];

content.insert(alice.clone(), rooms.clone());
content.insert(alice.into(), rooms.clone());
content.insert(alice_mail.into(), mail_rooms.clone());

let json_data = json!({
alice: rooms,
alice_mail: mail_rooms,
});

assert_eq!(to_json_value(&content).unwrap(), json_data);
}

#[test]
fn deserialization() {
let alice = owned_user_id!("@alice:ruma.io");
let alice = user_id!("@alice:ruma.io");
let alice_mail = "[email protected]";
let rooms = vec![owned_room_id!("!1:ruma.io"), owned_room_id!("!2:ruma.io")];
let mail_rooms = vec![owned_room_id!("!3:ruma.io")];

let json_data = json!({
"content": {
alice.to_string(): rooms,
alice: rooms,
alice_mail: mail_rooms,
},
"type": "m.direct"
});

let event: DirectEvent = from_json_value(json_data).unwrap();
let direct_rooms = event.content.get(&alice).unwrap();

let direct_rooms = event.content.get(<&DirectUserIdentifier>::from(alice)).unwrap();
assert!(direct_rooms.contains(&rooms[0]));
assert!(direct_rooms.contains(&rooms[1]));
MatMaul marked this conversation as resolved.
Show resolved Hide resolved

let email_direct_rooms =
event.content.get(<&DirectUserIdentifier>::from(alice_mail)).unwrap();
assert!(email_direct_rooms.contains(&mail_rooms[0]));
}

#[test]
fn user_id_conversion() {
let alice_direct_uid = DirectUserIdentifier::from_borrowed("@alice:ruma.io");
let alice_owned_user_id: OwnedUserId = alice_direct_uid
.to_owned()
.try_into()
.expect("@alice:ruma.io should be convertible into a Matrix user ID");
assert_eq!(alice_direct_uid, alice_owned_user_id);

let alice_direct_uid_mail = DirectUserIdentifier::from_borrowed("[email protected]");
OwnedUserId::try_from(alice_direct_uid_mail.to_owned())
.expect_err("[email protected] should not be convertible into a Matrix user ID");

let alice_user_id = user_id!("@alice:ruma.io");
let alice_direct_uid_mail: &DirectUserIdentifier = alice_user_id.into();
assert_eq!(alice_direct_uid_mail, alice_user_id);
assert_eq!(alice_direct_uid_mail, alice_user_id.to_owned());
assert_eq!(alice_user_id, alice_direct_uid_mail);
assert_eq!(alice_user_id.to_owned(), alice_direct_uid_mail);

let alice_user_id = user_id!("@alice:ruma.io");
let alice_direct_uid_mail: OwnedDirectUserIdentifier = alice_user_id.into();
assert_eq!(alice_direct_uid_mail, alice_user_id);
assert_eq!(alice_direct_uid_mail, alice_user_id.to_owned());
assert_eq!(alice_user_id, alice_direct_uid_mail);
assert_eq!(alice_user_id.to_owned(), alice_direct_uid_mail);
}
}