Commit 3eee54c8ed63
tarball3 files changed+232-9
@@ -552,22 +552,40 @@ async fn fetch_reply(
552552
553553
/// under lazy-loaded members a sender may be unknown locally, and `get_member` fetches the554554
/// member list once for the room when it is555
-async fn profiles<'a>(555
+pub(crate) async fn profiles<'a>(556556
room: &Room,557557
user_ids: impl Iterator<Item = &'a OwnedUserId>,558558
) -> HashMap<OwnedUserId, Sender> {559559
let mut unique: Vec<&OwnedUserId> = user_ids.collect();560560
unique.sort();561561
unique.dedup();562
- join_all(563
- unique.into_iter().map(|user_id| async move {562
+ let lookup = |users: Vec<&'a OwnedUserId>| {563
+ join_all(users.into_iter().map(|user_id| async move {564564
Some((user_id.clone(), member_sender(room, user_id).await?))565
- }),566
- )567
- .await568
- .into_iter()569
- .flatten()570
- .collect()565
+ }))566
+ };567
+ let mut found: HashMap<_, _> = lookup(unique.clone()).await.into_iter().flatten().collect();568
+ let missing: Vec<_> = unique569
+ .into_iter()570
+ .filter(|user| !found.contains_key(*user))571
+ .collect();572
+ // stores have been seen with members_synced persisted over a partial roster, after which573
+ // the sdk never asks /members again, so a sender it can't find marks it stale once a run574
+ if !missing.is_empty() && members_rechecked(room) {575
+ room.mark_members_missing();576
+ found.extend(lookup(missing).await.into_iter().flatten());577
+ }578
+ found579
+}580
+581
+fn members_rechecked(room: &Room) -> bool {582
+ static CHECKED: std::sync::LazyLock<583
+ std::sync::Mutex<std::collections::HashSet<(Option<OwnedUserId>, ruma::OwnedRoomId)>>,584
+ > = std::sync::LazyLock::new(Default::default);585
+ CHECKED.lock().unwrap().insert((586
+ room.client().user_id().map(ToOwned::to_owned),587
+ room.room_id().to_owned(),588
+ ))571589
}572590
573591
fn validate_limit(limit: u16) -> Result<(), Error> {@@ -118,6 +118,10 @@ pub(crate) struct Service {
118118
recovery_budget: usize,119119
latest_scan: bool,120120
recovery_retry: u64,121
+ /// senders a profile pass already looked up, found or not, so one without a profile isn't122
+ /// asked for again every second123
+ profiled: BTreeSet<(OwnedRoomId, OwnedUserId)>,124
+ profiling: bool,121125
}122126
123127
enum Work {@@ -132,6 +136,11 @@ enum Work {
132136
Option<String>,133137
Result<ruma::api::client::push::get_notifications::v3::Response, Error>,134138
),139
+ Profiles(140
+ OwnedRoomId,141
+ Vec<OwnedUserId>,142
+ HashMap<OwnedUserId, crate::Sender>,143
+ ),135144
}136145
137146
impl Work {@@ -148,6 +157,7 @@ impl Work {
148157
.map(|notification| notification.room_id.clone())149158
.collect(),150159
Self::History(..) => BTreeSet::new(),160
+ Self::Profiles(room_id, ..) => BTreeSet::from([room_id.clone()]),151161
}152162
}153163
}@@ -326,6 +336,8 @@ impl Service {
326336
recovery_budget: 20,327337
latest_scan: true,328338
recovery_retry: 0,339
+ profiled: BTreeSet::new(),340
+ profiling: false,329341
};330342
service.ignored = service.ignored_users().await;331343
Ok((@@ -1208,6 +1220,7 @@ impl Service {
12081220
}12091221
() = &mut tick => {12101222
tick = Box::pin(matrix_sdk::sleep::sleep(Duration::from_secs(1)));1223
+ self.schedule_profiles(&mut work);12111224
if now() >= next_scan && !history { self.recovery_budget = 20; self.latest_scan = true; next_scan = now() + 60_000; }12121225
let direct = direct_rooms(&self.client);12131226
if direct != self.direct { self.direct = direct; self.dirty = true; self.unpublished = true; }@@ -1306,6 +1319,47 @@ impl Service {
13061319
}13071320
}13081321
}1322
+ Work::Profiles(room_id, users, found) => {1323
+ self.profiling = false;1324
+ self.profiled1325
+ .extend(users.into_iter().map(|user| (room_id.clone(), user)));1326
+ let bare = |item: &TimelineItem| match item {1327
+ TimelineItem::Event(event)1328
+ if event.sender.display_name.is_none() && event.sender.avatar.is_none() =>1329
+ {1330
+ found.get(&event.sender.user_id).cloned()1331
+ }1332
+ _ => None,1333
+ };1334
+ let prefix = format!("{room_id}\n");1335
+ let mut repaired = Vec::new();1336
+ for (key, saved) in next.events.range_mut(prefix.clone()..) {1337
+ if !key.starts_with(&prefix) {1338
+ break;1339
+ }1340
+ if let Some(sender) = bare(&saved.item)1341
+ && let TimelineItem::Event(event) = &mut saved.item1342
+ {1343
+ event.sender = sender;1344
+ repaired.push(key.clone());1345
+ }1346
+ }1347
+ let revision = next.revision + 1;1348
+ for (key, entry) in next.entries.range_mut(prefix.clone()..) {1349
+ if !key.starts_with(&prefix) {1350
+ break;1351
+ }1352
+ if let Some(sender) = bare(&entry.event)1353
+ && let TimelineItem::Event(event) = &mut entry.event1354
+ {1355
+ event.sender = sender;1356
+ entry.revision = revision;1357
+ }1358
+ }1359
+ for key in &repaired {1360
+ next.touch_contexts(key);1361
+ }1362
+ }13091363
Work::History(latest, from, result) => {13101364
*history = false;13111365
self.recovery_budget = self.recovery_budget.saturating_sub(1);@@ -1387,6 +1441,72 @@ impl Service {
13871441
true13881442
}13891443
1444
+ /// rows observed after their window's capture never got a profile, so one room at a time1445
+ /// looks its bare senders up the way a capture does, outside the actor1446
+ fn schedule_profiles(&mut self, work: &mut FuturesUnordered<EngineBoxFuture<'static, Work>>) {1447
+ if self.profiling {1448
+ return;1449
+ }1450
+ let mut wanted: Option<(OwnedRoomId, BTreeSet<OwnedUserId>)> = None;1451
+ let items = self1452
+ .data1453
+ .events1454
+ .iter()1455
+ .map(|(key, saved)| (key, &saved.item))1456
+ .chain(1457
+ self.data1458
+ .entries1459
+ .iter()1460
+ .map(|(key, entry)| (key, &entry.event)),1461
+ );1462
+ for (key, item) in items {1463
+ let TimelineItem::Event(event) = item else {1464
+ continue;1465
+ };1466
+ if event.sender.display_name.is_some() || event.sender.avatar.is_some() {1467
+ continue;1468
+ }1469
+ let Some(room_id) = key1470
+ .split_once('\n')1471
+ .and_then(|(room, _)| OwnedRoomId::try_from(room).ok())1472
+ else {1473
+ continue;1474
+ };1475
+ if self.direct.contains(&room_id)1476
+ || self1477
+ .profiled1478
+ .contains(&(room_id.clone(), event.sender.user_id.clone()))1479
+ {1480
+ continue;1481
+ }1482
+ match &mut wanted {1483
+ None => wanted = Some((room_id, BTreeSet::from([event.sender.user_id.clone()]))),1484
+ Some((room, users)) if *room == room_id && users.len() < 50 => {1485
+ users.insert(event.sender.user_id.clone());1486
+ }1487
+ Some(_) => {}1488
+ }1489
+ }1490
+ let Some((room_id, users)) = wanted else {1491
+ return;1492
+ };1493
+ let Some(room) = self1494
+ .client1495
+ .get_room(&room_id)1496
+ .filter(|room| room.state() == RoomState::Joined)1497
+ else {1498
+ self.profiled1499
+ .extend(users.into_iter().map(|user| (room_id.clone(), user)));1500
+ return;1501
+ };1502
+ self.profiling = true;1503
+ let users: Vec<_> = users.into_iter().collect();1504
+ work.push(Box::pin(async move {1505
+ let found = context::profiles(&room, users.iter()).await;1506
+ Work::Profiles(room_id, users, found)1507
+ }));1508
+ }1509
+13901510
fn schedule(13911511
&mut self,13921512
work: &mut FuturesUnordered<EngineBoxFuture<'static, Work>>,@@ -778,3 +778,88 @@ async fn observing_again_keeps_the_profiles_a_capture_fetched() {
778778
);779779
assert_eq!(data.entries[&ping].capture, capture);780780
}781
+782
+#[tokio::test]783
+async fn a_profile_pass_names_rows_observed_after_their_capture() {784
+ let server = MatrixMockServer::new().await;785
+ let room_id = room_id!("!mentions:localhost");786
+ let (client, own_user) = joined_client(&server, room_id).await;787
+ let bob = user_id!("@bob:localhost");788
+ server789
+ .sync_room(790
+ &client,791
+ JoinedRoomBuilder::new(room_id).add_state_event(792
+ EventFactory::new()793
+ .room(room_id)794
+ .member(bob)795
+ .display_name("Bob"),796
+ ),797
+ )798
+ .await;799
+ let raw = |event: matrix_sdk::ruma::serde::Raw<_>, timestamp: u64| {800
+ let mut value: serde_json::Value = serde_json::from_str(event.json().get()).unwrap();801
+ value["origin_server_ts"] = json!(timestamp);802
+ value803
+ };804
+ let factory = EventFactory::new().room(room_id).sender(bob);805
+ let mention = raw(806
+ factory807
+ .text_msg("ping")808
+ .mentions(Mentions::with_user_ids([own_user]))809
+ .event_id(event_id!("$ping:localhost"))810
+ .into_raw_sync(),811
+ 10,812
+ );813
+ let following = raw(814
+ factory815
+ .text_msg("later")816
+ .event_id(event_id!("$later:localhost"))817
+ .into_raw_sync(),818
+ 11,819
+ );820
+ let mut service = service(&client, None).await;821
+ let room_id = room_id.to_owned();822
+ let mut data = Data::default();823
+ service.observe(&mut data, &room_id, mention).await;824
+ let ping = format!("{room_id}\n$ping:localhost");825
+ let revision = data.entries[&ping].revision;826
+ let target = data.entries[&ping].event.clone();827
+ service.captured(828
+ &mut data,829
+ ping.clone(),830
+ revision,831
+ Ok((832
+ crate::EventContext {833
+ events_before: Vec::new(),834
+ event: target,835
+ events_after: Vec::new(),836
+ start_token: None,837
+ end_token: None,838
+ },839
+ Vec::new(),840
+ Vec::new(),841
+ )),842
+ );843
+ service.observe(&mut data, &room_id, following).await;844
+ let later = format!("{room_id}\n$later:localhost");845
+ let name = |item: &TimelineItem| match item {846
+ TimelineItem::Event(event) => event.sender.display_name.clone(),847
+ _ => None,848
+ };849
+ assert_eq!(name(&data.events[&later].item), None);850
+851
+ service.data = data;852
+ let mut work = FuturesUnordered::new();853
+ service.schedule_profiles(&mut work);854
+ let result = work.next().await.expect("a profile pass was scheduled");855
+ let mut next = std::mem::take(&mut service.data);856
+ let (mut captures, mut retries, mut history) = (BTreeSet::new(), BTreeSet::new(), false);857
+ service858
+ .complete(&mut next, result, &mut captures, &mut retries, &mut history)859
+ .await;860
+ assert_eq!(name(&next.events[&later].item).as_deref(), Some("Bob"));861
+ assert_eq!(name(&next.entries[&ping].event).as_deref(), Some("Bob"));862
+ service.data = next;863
+ service.schedule_profiles(&mut work);864
+ assert!(work.is_empty());865
+}