add fetch_event tool and backfill get_room_history beyond event cache

This commit is contained in:
Damocles 2026-05-01 03:33:04 +02:00
parent e538be2c3a
commit 829a60854f
4 changed files with 251 additions and 41 deletions

View file

@ -77,6 +77,11 @@ async fn handle_request(request: DaemonRequest, client: &Client) -> DaemonRespon
DaemonRequest::GetRoomHistory { room_id, limit } => {
get_room_history(client, &room_id, limit).await
}
DaemonRequest::FetchEvent {
room_id,
event_id,
context_before,
} => fetch_event(client, &room_id, &event_id, context_before).await,
}
}
@ -251,45 +256,202 @@ async fn get_room_history(
};
let limit = limit.unwrap_or(20).min(100);
let tl = match timeline::load_timeline(&room, limit, &own_user).await {
Ok(t) => t,
Err(e) => return DaemonResponse::err(format!("failed to load timeline: {e}")),
// Backfill via /messages if cache is short
if let Ok((cache, _)) = room.event_cache().await {
let mut tl = match timeline::load_timeline(&room, limit, &own_user).await {
Ok(t) => t,
Err(e) => return DaemonResponse::err(format!("failed to load timeline: {e}")),
};
let mut tries = 0;
while tl.len() < limit && tries < 5 {
tries += 1;
match cache.pagination().run_backwards_once((limit - tl.len()) as u16).await {
Ok(outcome) => {
if outcome.reached_start {
break;
}
}
Err(e) => {
tracing::warn!("backfill failed: {e}");
break;
}
}
tl = match timeline::load_timeline(&room, limit, &own_user).await {
Ok(t) => t,
Err(e) => return DaemonResponse::err(format!("reload after backfill failed: {e}")),
};
}
let items: Vec<_> = tl.iter().map(timeline_item_to_json).collect();
DaemonResponse::ok(items)
} else {
DaemonResponse::err("event cache not available".to_owned())
}
}
fn timeline_item_to_json(item: &crate::types::TimelineItem) -> serde_json::Value {
match item {
crate::types::TimelineItem::Message {
event_id,
sender,
body,
is_self,
ts,
in_reply_to,
} => json!({
"kind": "message",
"event_id": event_id.as_str(),
"sender": sender.as_str(),
"body": body,
"is_self": is_self,
"ts": ts,
"in_reply_to": in_reply_to.as_ref().map(|e| e.as_str()),
}),
crate::types::TimelineItem::Reaction {
sender,
target_event_id,
key,
is_self,
ts,
} => json!({
"kind": "reaction",
"sender": sender.as_str(),
"target_event_id": target_event_id.as_str(),
"key": key,
"is_self": is_self,
"ts": ts,
}),
}
}
/// Fetch a specific event by ID via the homeserver `/context` endpoint.
/// Returns the event plus `context_before` events before it. Includes one
/// extra event as `earlier_handle` so the shard can page further backward
/// by calling fetch_event again with that id.
async fn fetch_event(
client: &Client,
room_id: &str,
event_id: &str,
context_before: Option<u32>,
) -> DaemonResponse {
use matrix_sdk::ruma::events::AnySyncTimelineEvent;
use matrix_sdk::ruma::events::room::message::MessageType;
let rid = match room_id.parse::<OwnedRoomId>() {
Ok(r) => r,
Err(e) => return DaemonResponse::err(format!("invalid room_id: {e}")),
};
let Some(room) = client.get_room(&rid) else {
return DaemonResponse::err(format!("room {rid} not found"));
};
let own_user = match client.user_id() {
Some(u) => u.to_owned(),
None => return DaemonResponse::err("not logged in".to_owned()),
};
let items: Vec<_> = tl
.iter()
.map(|item| match item {
crate::types::TimelineItem::Message {
event_id,
sender,
body,
is_self,
ts,
in_reply_to,
} => json!({
"kind": "message",
"event_id": event_id.as_str(),
"sender": sender.as_str(),
"body": body,
"is_self": is_self,
"ts": ts,
"in_reply_to": in_reply_to.as_ref().map(|e| e.as_str()),
}),
crate::types::TimelineItem::Reaction {
sender,
target_event_id,
key,
is_self,
ts,
} => json!({
"kind": "reaction",
"sender": sender.as_str(),
"target_event_id": target_event_id.as_str(),
"key": key,
"is_self": is_self,
"ts": ts,
}),
})
.collect();
DaemonResponse::ok(items)
// Resolve possibly-shortened event id against recent timeline first;
// fall back to parsing as a full id.
let resolve_tl = timeline::load_timeline(&room, 50, &own_user)
.await
.unwrap_or_default();
let full_eid = match timeline::resolve_event_id(&resolve_tl, event_id) {
Some(eid) => eid,
None => match event_id.parse::<matrix_sdk::ruma::OwnedEventId>() {
Ok(eid) => eid,
Err(e) => {
return DaemonResponse::err(format!("invalid or unknown event_id: {e}"));
}
},
};
// Request one extra event so we can split it off as the paging handle
let context_before = context_before.unwrap_or(0).min(50);
let request_size = context_before + 1;
let response = match room
.event_with_context(
&full_eid,
false,
matrix_sdk::ruma::UInt::from(request_size),
None,
)
.await
{
Ok(r) => r,
Err(e) => return DaemonResponse::err(format!("event_with_context failed: {e}")),
};
let render = |raw: &matrix_sdk::deserialized_responses::TimelineEvent| -> Option<serde_json::Value> {
let deserialized = raw.raw().deserialize().ok()?;
let AnySyncTimelineEvent::MessageLike(msg) = deserialized else {
return None;
};
match msg {
matrix_sdk::ruma::events::AnySyncMessageLikeEvent::RoomMessage(
matrix_sdk::ruma::events::SyncMessageLikeEvent::Original(orig),
) => {
let MessageType::Text(text) = &orig.content.msgtype else {
return None;
};
let ms: u64 = orig.origin_server_ts.0.into();
let ts = (ms / 1000) as i64;
let in_reply_to = match &orig.content.relates_to {
Some(matrix_sdk::ruma::events::room::message::Relation::Reply { in_reply_to }) => {
Some(in_reply_to.event_id.as_str().to_owned())
}
_ => None,
};
Some(json!({
"kind": "message",
"event_id": orig.event_id.as_str(),
"sender": orig.sender.as_str(),
"body": text.body,
"is_self": orig.sender == own_user,
"ts": ts,
"in_reply_to": in_reply_to,
}))
}
matrix_sdk::ruma::events::AnySyncMessageLikeEvent::Reaction(
matrix_sdk::ruma::events::SyncMessageLikeEvent::Original(orig),
) => {
let ms: u64 = orig.origin_server_ts.0.into();
let ts = (ms / 1000) as i64;
Some(json!({
"kind": "reaction",
"sender": orig.sender.as_str(),
"target_event_id": orig.content.relates_to.event_id.as_str(),
"key": orig.content.relates_to.key,
"is_self": orig.sender == own_user,
"ts": ts,
}))
}
_ => None,
}
};
// events_before is newest-first per matrix /context spec - reverse for chronological
let mut before: Vec<_> = response.events_before.iter().filter_map(render).collect();
before.reverse();
// The "earlier handle" is the oldest event we got, if we asked for more than 0
// It's used by the shard to page further back via another fetch_event call
let earlier_handle = if context_before > 0 && before.len() > context_before as usize {
before.first().and_then(|e| e.get("event_id").and_then(|v| v.as_str()).map(String::from))
} else {
None
};
let context_events: Vec<_> = if earlier_handle.is_some() {
// First event is the handle - skip it from the main events list
before.into_iter().skip(1).collect()
} else {
before
};
let target = response.event.as_ref().and_then(render);
DaemonResponse::ok(json!({
"event": target,
"context_before": context_events,
"earlier_handle": earlier_handle,
}))
}