base: Save profiles independently from membership events.

The sender controls the content of the membership event, since the
content contains profile data (display names, avatar urls) a sender
might incorrectly change the profile of another member inside the room.

This is allowed in the case where the sender is kicking or inviting the
member, this it will self heal once the member re-joins. Still, to
mitigate this a bit we're storing the profile data when we know that the
member sent out the content on their own.
master
Damir Jelić 2020-12-24 16:35:32 +01:00
parent e2225b2700
commit a29d2e39c4
5 changed files with 109 additions and 12 deletions

View File

@ -114,7 +114,11 @@ impl Completer for InspectorHelper {
let args: Vec<&str> = line.split_ascii_whitespace().collect();
let commands = vec![
("get-state", "get a state event in the give room"),
("get-state", "get a state event in the given room"),
(
"get-profiles",
"get all the stored profiles in the given room",
),
("list-rooms", "list all rooms"),
(
"get-members",
@ -131,7 +135,9 @@ impl Completer for InspectorHelper {
if args.is_empty() {
Ok((pos, commands))
} else if args.len() == 1 {
if (args[0] == "get-state" || args[0] == "get-members") && line.ends_with(' ') {
if (args[0] == "get-state" || args[0] == "get-members" || args[0] == "get-profiles")
&& line.ends_with(' ')
{
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
} else {
Ok((
@ -152,7 +158,7 @@ impl Completer for InspectorHelper {
} else {
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
}
} else if args[0] == "get-members" {
} else if args[0] == "get-members" || args[0] == "get-profiles" {
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
} else {
Ok((pos, vec![]))
@ -222,6 +228,13 @@ impl Inspector {
async fn run(&self, matches: ArgMatches<'_>) {
match matches.subcommand() {
("get-profiles", args) => {
let args = args.expect("No args provided for get-state");
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
self.get_profiles(room_id).await;
}
("get-members", args) => {
let args = args.expect("No args provided for get-state");
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
@ -244,6 +257,20 @@ impl Inspector {
self.printer.pretty_print_struct(&rooms);
}
async fn get_profiles(&self, room_id: RoomId) {
let joined: Vec<UserId> = self
.store
.get_joined_user_ids(&room_id)
.await
.collect()
.await;
for member in joined {
let event = self.store.get_profile(&room_id, &member).await;
self.printer.pretty_print_struct(&event);
}
}
async fn get_members(&self, room_id: RoomId) {
let joined: Vec<UserId> = self
.store
@ -278,6 +305,13 @@ impl Inspector {
.map_err(|_| "Invalid room id given".to_owned())
}),
))
.subcommand(SubCommand::with_name("get-profiles").arg(
Arg::with_name("room-id").required(true).validator(|r| {
RoomId::try_from(r)
.map(|_| ())
.map_err(|_| "Invalid room id given".to_owned())
}),
))
.subcommand(
SubCommand::with_name("get-state")
.arg(Arg::with_name("room-id").required(true).validator(|r| {

View File

@ -449,6 +449,18 @@ impl BaseClient {
}
}
// Senders can fake the profile easily so we keep track
// of profiles that the member set themselves to avoid
// having confusing profile changes when a member gets
// kicked/banned.
if member.state_key == member.sender {
changes
.profiles
.entry(room_id.clone())
.or_insert_with(BTreeMap::new)
.insert(member.sender.clone(), member.content.clone());
}
changes
.members
.entry(room_id.clone())
@ -551,6 +563,7 @@ impl BaseClient {
) -> (
State,
BTreeMap<UserId, MemberEvent>,
BTreeMap<UserId, MemberEventContent>,
BTreeMap<String, BTreeMap<String, AnySyncStateEvent>>,
BTreeSet<UserId>,
) {
@ -558,6 +571,7 @@ impl BaseClient {
let mut members = BTreeMap::new();
let mut state_events = BTreeMap::new();
let mut user_ids = BTreeSet::new();
let mut profiles = BTreeMap::new();
let room_id = room_info.room_id.clone();
@ -587,6 +601,15 @@ impl BaseClient {
}
_ => (),
}
// Senders can fake the profile easily so we keep track
// of profiles that the member set themselves to avoid
// having confusing profile changes when a member gets
// kicked/banned.
if m.state_key == m.sender {
profiles.insert(m.sender.clone(), m.content.clone());
}
members.insert(m.state_key.clone(), m);
}
Err(e) => warn!(
@ -602,7 +625,7 @@ impl BaseClient {
}
}
(state, members, state_events, user_ids)
(state, members, profiles, state_events, user_ids)
}
async fn handle_room_account_data(
@ -665,10 +688,11 @@ impl BaseClient {
room_info.update_summary(&new_info.summary);
room_info.set_prev_batch(new_info.timeline.prev_batch.as_deref());
let (state, members, state_events, mut user_ids) =
let (state, members, profiles, state_events, mut user_ids) =
self.handle_state(new_info.state.events, &mut room_info);
changes.members.insert(room_id.clone(), members);
changes.profiles.insert(room_id.clone(), profiles);
changes.state.insert(room_id.clone(), state_events);
if new_info.timeline.limited {
@ -737,10 +761,11 @@ impl BaseClient {
let mut room_info = room.clone_info();
room_info.mark_as_left();
let (state, members, state_events, mut user_ids) =
let (state, members, profiles, state_events, mut user_ids) =
self.handle_state(new_info.state.events, &mut room_info);
changes.members.insert(room_id.clone(), members);
changes.profiles.insert(room_id.clone(), profiles);
changes.state.insert(room_id.clone(), state_events);
let timeline = self

View File

@ -16,7 +16,9 @@ use std::sync::Arc;
use matrix_sdk_common::{
events::{
presence::PresenceEvent, room::power_levels::PowerLevelsEventContent, SyncStateEvent,
presence::PresenceEvent,
room::{member::MemberEventContent, power_levels::PowerLevelsEventContent},
SyncStateEvent,
},
identifiers::UserId,
};
@ -26,6 +28,7 @@ use crate::responses::MemberEvent;
#[derive(Clone, Debug)]
pub struct RoomMember {
pub(crate) event: Arc<MemberEvent>,
pub(crate) profile: Arc<Option<MemberEventContent>>,
pub(crate) presence: Arc<Option<PresenceEvent>>,
pub(crate) power_levles: Arc<Option<SyncStateEvent<PowerLevelsEventContent>>>,
}
@ -36,7 +39,11 @@ impl RoomMember {
}
pub fn display_name(&self) -> Option<&str> {
self.event.content.displayname.as_deref()
if let Some(p) = self.profile.as_ref() {
p.displayname.as_deref()
} else {
self.event.content.displayname.as_deref()
}
}
pub fn power_level(&self) -> i64 {

View File

@ -96,8 +96,9 @@ impl Room {
let joined = self.store.get_joined_user_ids(self.room_id()).await;
let invited = self.store.get_invited_user_ids(self.room_id()).await;
let x = move |u| async move {
let into_member = move |u| async move {
let presence = self.store.get_presence_event(&u).await;
let profile = self.store.get_profile(self.room_id(), &u).await;
let power = self
.store
.get_state_event(self.room_id(), EventType::RoomPowerLevels, "")
@ -116,12 +117,13 @@ impl Room {
.await
.map(|m| RoomMember {
event: m.into(),
profile: profile.into(),
presence: presence.into(),
power_levles: power.into(),
})
};
joined.chain(invited).filter_map(x)
joined.chain(invited).filter_map(into_member)
}
/// Calculate the canonical display name of the room, taking into account
@ -234,6 +236,7 @@ impl Room {
pub async fn get_member(&self, user_id: &UserId) -> Option<RoomMember> {
let presence = self.store.get_presence_event(user_id).await;
let profile = self.store.get_profile(self.room_id(), user_id).await;
let power = self
.store
.get_state_event(self.room_id(), EventType::RoomPowerLevels, "")
@ -252,6 +255,7 @@ impl Room {
.await
.map(|e| RoomMember {
event: e.into(),
profile: profile.into(),
presence: presence.into(),
power_levles: power.into(),
})

View File

@ -6,8 +6,9 @@ use dashmap::DashMap;
use futures::stream::{self, Stream};
use matrix_sdk_common::{
events::{
presence::PresenceEvent, room::member::MembershipState, AnyBasicEvent,
AnyStrippedStateEvent, AnySyncStateEvent, EventContent, EventType,
presence::PresenceEvent,
room::member::{MemberEventContent, MembershipState},
AnyBasicEvent, AnyStrippedStateEvent, AnySyncStateEvent, EventContent, EventType,
},
identifiers::{RoomId, UserId},
locks::RwLock,
@ -126,6 +127,7 @@ pub struct SledStore {
session: Tree,
account_data: Tree,
members: Tree,
profiles: Tree,
joined_user_ids: Tree,
invited_user_ids: Tree,
room_info: Tree,
@ -144,6 +146,7 @@ pub struct StateChanges {
pub presence: BTreeMap<UserId, PresenceEvent>,
pub members: BTreeMap<RoomId, BTreeMap<UserId, MemberEvent>>,
pub profiles: BTreeMap<RoomId, BTreeMap<UserId, MemberEventContent>>,
pub state: BTreeMap<RoomId, BTreeMap<String, BTreeMap<String, AnySyncStateEvent>>>,
pub room_account_data: BTreeMap<RoomId, BTreeMap<String, AnyBasicEvent>>,
pub room_infos: BTreeMap<RoomId, RoomInfo>,
@ -217,6 +220,7 @@ impl SledStore {
let account_data = db.open_tree("account_data").unwrap();
let members = db.open_tree("members").unwrap();
let profiles = db.open_tree("profiles").unwrap();
let joined_user_ids = db.open_tree("joined_user_ids").unwrap();
let invited_user_ids = db.open_tree("invited_user_ids").unwrap();
@ -234,6 +238,7 @@ impl SledStore {
session,
account_data,
members,
profiles,
joined_user_ids,
invited_user_ids,
room_account_data,
@ -279,6 +284,7 @@ impl SledStore {
&self.session,
&self.account_data,
&self.members,
&self.profiles,
&self.joined_user_ids,
&self.invited_user_ids,
&self.room_info,
@ -294,6 +300,7 @@ impl SledStore {
session,
account_data,
members,
profiles,
joined,
invited,
summaries,
@ -334,6 +341,15 @@ impl SledStore {
}
}
for (room, users) in &changes.profiles {
for (user_id, profile) in users {
profiles.insert(
format!("{}{}", room.as_str(), user_id.as_str()).as_str(),
serde_json::to_vec(&profile).unwrap(),
)?;
}
}
for (event_type, event) in &changes.account_data {
account_data
.insert(event_type.as_str(), serde_json::to_vec(&event).unwrap())?;
@ -435,6 +451,17 @@ impl SledStore {
.map(|e| serde_json::from_slice(&e).unwrap())
}
pub async fn get_profile(
&self,
room_id: &RoomId,
user_id: &UserId,
) -> Option<MemberEventContent> {
self.profiles
.get(format!("{}{}", room_id.as_str(), user_id.as_str()))
.unwrap()
.map(|p| serde_json::from_slice(&p).unwrap())
}
pub async fn get_member_event(
&self,
room_id: &RoomId,