use std::collections::BTreeMap;
use ruma_events_macros::ruma_event;
use ruma_identifiers::UserId;
use serde::{Deserialize, Serialize};
ruma_event! {
MemberEvent {
kind: StateEvent,
event_type: "m.room.member",
content: {
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub displayname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_direct: Option<bool>,
pub membership: MembershipState,
#[serde(skip_serializing_if = "Option::is_none")]
pub third_party_invite: Option<ThirdPartyInvite>,
},
}
}
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum MembershipState {
Ban,
Invite,
Join,
Knock,
Leave,
}
impl_enum! {
MembershipState {
Ban => "ban",
Invite => "invite",
Join => "join",
Knock => "knock",
Leave => "leave",
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ThirdPartyInvite {
pub display_name: String,
pub signed: SignedContent,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SignedContent {
pub mxid: UserId,
pub signatures: BTreeMap<String, BTreeMap<String, String>>,
pub token: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub enum MembershipChange {
None,
Error,
Joined,
Left,
Banned,
Unbanned,
Kicked,
Invited,
KickedAndBanned,
InvitationRejected,
InvitationRevoked,
ProfileChanged,
NotImplemented,
}
impl MemberEvent {
pub fn membership_change(&self) -> MembershipChange {
use MembershipState::*;
let prev_membership = if let Some(prev_content) = &self.prev_content {
prev_content.membership
} else {
Leave
};
match (prev_membership, &self.content.membership) {
(Invite, Invite) | (Leave, Leave) | (Ban, Ban) => MembershipChange::None,
(Invite, Join) | (Leave, Join) => MembershipChange::Joined,
(Invite, Leave) => {
if self.sender == self.state_key {
MembershipChange::InvitationRevoked
} else {
MembershipChange::InvitationRejected
}
}
(Invite, Ban) | (Leave, Ban) => MembershipChange::Banned,
(Join, Invite) | (Ban, Invite) | (Ban, Join) => MembershipChange::Error,
(Join, Join) => MembershipChange::ProfileChanged,
(Join, Leave) => {
if self.sender == self.state_key {
MembershipChange::Left
} else {
MembershipChange::Kicked
}
}
(Join, Ban) => MembershipChange::KickedAndBanned,
(Leave, Invite) => MembershipChange::Invited,
(Ban, Leave) => MembershipChange::Unbanned,
(Knock, _) | (_, Knock) => MembershipChange::NotImplemented,
}
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, UNIX_EPOCH};
use maplit::btreemap;
use matches::assert_matches;
use serde_json::{from_value as from_json_value, json};
use super::{
MemberEvent, MemberEventContent, MembershipState, SignedContent, ThirdPartyInvite,
};
use crate::EventJson;
#[test]
fn serde_with_no_prev_content() {
let json = json!({
"type": "m.room.member",
"content": {
"membership": "join"
},
"event_id": "$h29iv0s8:example.com",
"origin_server_ts": 1,
"room_id": "!n8f893n9:example.com",
"sender": "@carl:example.com",
"state_key": "example.com"
});
assert_matches!(
from_json_value::<EventJson<MemberEvent>>(json)
.unwrap()
.deserialize()
.unwrap(),
MemberEvent {
content: MemberEventContent {
avatar_url: None,
displayname: None,
is_direct: None,
membership: MembershipState::Join,
third_party_invite: None,
},
event_id,
origin_server_ts,
room_id: Some(room_id),
sender,
state_key,
unsigned,
prev_content: None,
} if event_id == "$h29iv0s8:example.com"
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(1)
&& room_id == "!n8f893n9:example.com"
&& sender == "@carl:example.com"
&& state_key == "example.com"
&& unsigned.is_empty()
);
}
#[test]
fn serde_with_prev_content() {
let json = json!({
"type": "m.room.member",
"content": {
"membership": "join"
},
"event_id": "$h29iv0s8:example.com",
"origin_server_ts": 1,
"prev_content": {
"membership": "join"
},
"room_id": "!n8f893n9:example.com",
"sender": "@carl:example.com",
"state_key": "example.com"
});
assert_matches!(
from_json_value::<EventJson<MemberEvent>>(json)
.unwrap()
.deserialize()
.unwrap(),
MemberEvent {
content: MemberEventContent {
avatar_url: None,
displayname: None,
is_direct: None,
membership: MembershipState::Join,
third_party_invite: None,
},
event_id,
origin_server_ts,
room_id: Some(room_id),
sender,
state_key,
unsigned,
prev_content: Some(MemberEventContent {
avatar_url: None,
displayname: None,
is_direct: None,
membership: MembershipState::Join,
third_party_invite: None,
}),
} if event_id == "$h29iv0s8:example.com"
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(1)
&& room_id == "!n8f893n9:example.com"
&& sender == "@carl:example.com"
&& state_key == "example.com"
&& unsigned.is_empty()
);
}
#[test]
fn serde_with_content_full() {
let json = json!({
"type": "m.room.member",
"content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"is_direct": true,
"membership": "invite",
"third_party_invite": {
"display_name": "alice",
"signed": {
"mxid": "@alice:example.org",
"signatures": {
"magic.forest": {
"ed25519:3": "foobar"
}
},
"token": "abc123"
}
}
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 233,
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:example.org",
"state_key": "@alice:example.org"
});
assert_matches!(
from_json_value::<EventJson<MemberEvent>>(json)
.unwrap()
.deserialize()
.unwrap(),
MemberEvent {
content: MemberEventContent {
avatar_url: Some(avatar_url),
displayname: Some(displayname),
is_direct: Some(true),
membership: MembershipState::Invite,
third_party_invite: Some(ThirdPartyInvite {
display_name: third_party_displayname,
signed: SignedContent { mxid, signatures, token },
}),
},
event_id,
origin_server_ts,
room_id: Some(room_id),
sender,
state_key,
unsigned,
prev_content: None,
} if avatar_url == "mxc://example.org/SEsfnsuifSDFSSEF"
&& displayname == "Alice Margatroid"
&& third_party_displayname == "alice"
&& mxid == "@alice:example.org"
&& signatures == btreemap! {
"magic.forest".to_owned() => btreemap! {
"ed25519:3".to_owned() => "foobar".to_owned()
}
}
&& token == "abc123"
&& event_id == "$143273582443PhrSn:example.org"
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(233)
&& room_id == "!jEsUZKDJdhlrceRyVU:example.org"
&& sender == "@alice:example.org"
&& state_key == "@alice:example.org"
&& unsigned.is_empty()
)
}
#[test]
fn serde_with_prev_content_full() {
let json = json!({
"type": "m.room.member",
"content": {
"membership": "join"
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 233,
"prev_content": {
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"is_direct": true,
"membership": "invite",
"third_party_invite": {
"display_name": "alice",
"signed": {
"mxid": "@alice:example.org",
"signatures": {
"magic.forest": {
"ed25519:3": "foobar"
}
},
"token": "abc123"
}
}
},
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
"sender": "@alice:example.org",
"state_key": "@alice:example.org"
});
assert_matches!(
from_json_value::<EventJson<MemberEvent>>(json)
.unwrap()
.deserialize()
.unwrap(),
MemberEvent {
content: MemberEventContent {
avatar_url: None,
displayname: None,
is_direct: None,
membership: MembershipState::Join,
third_party_invite: None,
},
event_id,
origin_server_ts,
room_id: Some(room_id),
sender,
state_key,
unsigned,
prev_content: Some(MemberEventContent {
avatar_url: Some(avatar_url),
displayname: Some(displayname),
is_direct: Some(true),
membership: MembershipState::Invite,
third_party_invite: Some(ThirdPartyInvite {
display_name: third_party_displayname,
signed: SignedContent { mxid, signatures, token },
}),
}),
} if event_id == "$143273582443PhrSn:example.org"
&& origin_server_ts == UNIX_EPOCH + Duration::from_millis(233)
&& room_id == "!jEsUZKDJdhlrceRyVU:example.org"
&& sender == "@alice:example.org"
&& state_key == "@alice:example.org"
&& unsigned.is_empty()
&& avatar_url == "mxc://example.org/SEsfnsuifSDFSSEF"
&& displayname == "Alice Margatroid"
&& third_party_displayname == "alice"
&& mxid == "@alice:example.org"
&& signatures == btreemap! {
"magic.forest".to_owned() => btreemap! {
"ed25519:3".to_owned() => "foobar".to_owned()
}
}
&& token == "abc123"
);
}
}