feat(#2479): add from/until event-id cursors to read_room
This commit is contained in:
parent
d90504b427
commit
001b1dae37
4 changed files with 189 additions and 46 deletions
|
|
@ -626,51 +626,160 @@ pub async fn invite_user(client: &Client, room_ref: &str, user_id: &str) -> Daem
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn read_room(client: &Client, room_ref: &str, limit: Option<usize>) -> DaemonResponse {
|
||||
/// Map a matrix-sdk timeline event to the JSON DTO, applying the
|
||||
/// encryption-gated UTD sentinel. Returns `None` when the raw event can't
|
||||
/// be deserialized. Shared by `read_room`'s plain-timeline and the
|
||||
/// event-anchored (`from` / `until`) paths.
|
||||
///
|
||||
/// `room.messages()` / `event_with_context` transparently decrypt events in
|
||||
/// encrypted rooms. UTD (unable to decrypt) events surface via
|
||||
/// `ev.kind.is_utd()`, but that flag can transiently fire in a NON-encrypted
|
||||
/// room when the SDK hasn't finished reclassifying a freshly-synced
|
||||
/// `m.room.encrypted` cache row — so gate the sentinel on the room actually
|
||||
/// being encrypted to avoid a false `[unable to decrypt]` on plaintext.
|
||||
fn to_timeline_event(
|
||||
ev: &matrix_sdk::deserialized_responses::TimelineEvent,
|
||||
is_encrypted: bool,
|
||||
) -> Option<TimelineEvent> {
|
||||
let parsed = ev.raw().deserialize().ok()?;
|
||||
let body = if is_encrypted && ev.kind.is_utd() {
|
||||
"[unable to decrypt]".to_owned()
|
||||
} else {
|
||||
extract_body_sync(&parsed)
|
||||
};
|
||||
Some(TimelineEvent {
|
||||
event_id: parsed.event_id().to_string(),
|
||||
sender: parsed.sender().to_string(),
|
||||
origin_server_ts: parsed.origin_server_ts().0.into(),
|
||||
event_type: parsed.event_type().to_string(),
|
||||
body,
|
||||
in_reply_to_event_id: extract_in_reply_to(&parsed),
|
||||
})
|
||||
}
|
||||
|
||||
/// Read room timeline events. With neither cursor set, returns the last
|
||||
/// `limit` events (newest-first) — the default. `from` / `until` anchor the
|
||||
/// read at an event id (mutually exclusive):
|
||||
/// - `until = event_id` → the anchor plus the `limit - 1` events *before* it
|
||||
/// (page into the past from a known event).
|
||||
/// - `from = event_id` → the anchor plus the `limit - 1` events *after* it
|
||||
/// (continue reading forward from a known point).
|
||||
///
|
||||
/// Both cursors resolve the anchor via the `/context` endpoint
|
||||
/// (`event_with_context`) to get a pagination token, then page precisely in
|
||||
/// the requested direction — matrix `/messages` `from` is an opaque token,
|
||||
/// not an event id, so plain `messages()` can't anchor at an event. All
|
||||
/// three modes return events newest-first for a consistent shape.
|
||||
pub async fn read_room(
|
||||
client: &Client,
|
||||
room_ref: &str,
|
||||
limit: Option<usize>,
|
||||
from: Option<String>,
|
||||
until: Option<String>,
|
||||
) -> DaemonResponse {
|
||||
use matrix_sdk::room::MessagesOptions;
|
||||
use matrix_sdk::ruma::UInt;
|
||||
|
||||
let room = match resolve_room(client, room_ref).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let limit_val = u32::try_from(limit.unwrap_or(50).min(200)).unwrap_or(50);
|
||||
let mut opts = MessagesOptions::backward();
|
||||
opts.limit = matrix_sdk::ruma::UInt::from(limit_val);
|
||||
// room.messages() transparently decrypts events in encrypted rooms.
|
||||
// UTD (unable to decrypt) events surface via `ev.kind.is_utd()`, but
|
||||
// that flag can transiently fire in a NON-encrypted room when the SDK
|
||||
// hasn't finished reclassifying a freshly-synced `m.room.encrypted`
|
||||
// cache row — so gate the sentinel on the room actually being
|
||||
// encrypted to avoid a false `[unable to decrypt]` on plaintext.
|
||||
let msgs = match room.messages(opts).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => return DaemonResponse::error(format!("messages: {e}")),
|
||||
};
|
||||
if from.is_some() && until.is_some() {
|
||||
return DaemonResponse::error(
|
||||
"read_room: `from` and `until` are mutually exclusive \
|
||||
(they page opposite directions off the anchor event)"
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
// Clamp to [1, 200]; keep it in u32 so every `UInt` conversion below is
|
||||
// infallible (`limit_val - 1` can't underflow — the clamp floor is 1).
|
||||
let limit_val: u32 = u32::try_from(limit.unwrap_or(50))
|
||||
.unwrap_or(50)
|
||||
.clamp(1, 200);
|
||||
let is_encrypted = room.encryption_state().is_encrypted();
|
||||
let events: Vec<TimelineEvent> = msgs
|
||||
.chunk
|
||||
.iter()
|
||||
.filter_map(|ev| {
|
||||
let parsed = ev.raw().deserialize().ok()?;
|
||||
let event_id = parsed.event_id().to_string();
|
||||
let sender = parsed.sender().to_string();
|
||||
let origin_server_ts: i64 = parsed.origin_server_ts().0.into();
|
||||
let event_type = parsed.event_type().to_string();
|
||||
let body = if is_encrypted && ev.kind.is_utd() {
|
||||
"[unable to decrypt]".to_owned()
|
||||
} else {
|
||||
extract_body_sync(&parsed)
|
||||
};
|
||||
Some(TimelineEvent {
|
||||
event_id,
|
||||
sender,
|
||||
origin_server_ts,
|
||||
event_type,
|
||||
body,
|
||||
in_reply_to_event_id: extract_in_reply_to(&parsed),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let events: Vec<TimelineEvent> = if let Some(anchor_ref) = from.as_deref().or(until.as_deref())
|
||||
{
|
||||
let eid: OwnedEventId = match anchor_ref.parse() {
|
||||
Ok(e) => e,
|
||||
Err(e) => return DaemonResponse::error(format!("invalid event_id {anchor_ref}: {e}")),
|
||||
};
|
||||
// context_size 0: no context events, so the returned tokens point
|
||||
// immediately around the anchor and the follow-up page is contiguous.
|
||||
let ctx = match room
|
||||
.event_with_context(&eid, false, UInt::from(0u32), None)
|
||||
.await
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => return DaemonResponse::error(format!("event_with_context {eid}: {e}")),
|
||||
};
|
||||
let Some(anchor) = ctx.event else {
|
||||
return DaemonResponse::error(format!("event {eid} not found in room"));
|
||||
};
|
||||
let side_limit = UInt::from(limit_val - 1);
|
||||
if from.is_some() {
|
||||
// Forward (newer) from the anchor. The forward chunk is
|
||||
// oldest-first; reverse to newest-first with the anchor (oldest of
|
||||
// the slice) last. When the anchor is at the live edge there is no
|
||||
// forward token — return the anchor alone rather than pass
|
||||
// `from = None` to messages(), which would page from room *start*.
|
||||
let mut out: Vec<TimelineEvent> = Vec::new();
|
||||
if let Some(token) = ctx.next_batch_token {
|
||||
let mut opts = MessagesOptions::forward();
|
||||
opts.from = Some(token);
|
||||
opts.limit = side_limit;
|
||||
let newer = match room.messages(opts).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => return DaemonResponse::error(format!("messages (from): {e}")),
|
||||
};
|
||||
out.extend(
|
||||
newer
|
||||
.chunk
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|ev| to_timeline_event(ev, is_encrypted)),
|
||||
);
|
||||
}
|
||||
out.extend(to_timeline_event(&anchor, is_encrypted));
|
||||
out
|
||||
} else {
|
||||
// Backward (older) to the anchor. Backward chunk is already
|
||||
// newest-first; the anchor is the newest event, so it leads. When
|
||||
// the anchor is the oldest event there is no backward token —
|
||||
// return the anchor alone rather than pass `from = None`, which
|
||||
// would page from room *end* (the newest events).
|
||||
let mut out: Vec<TimelineEvent> = to_timeline_event(&anchor, is_encrypted)
|
||||
.into_iter()
|
||||
.collect();
|
||||
if let Some(token) = ctx.prev_batch_token {
|
||||
let mut opts = MessagesOptions::backward();
|
||||
opts.from = Some(token);
|
||||
opts.limit = side_limit;
|
||||
let older = match room.messages(opts).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => return DaemonResponse::error(format!("messages (until): {e}")),
|
||||
};
|
||||
out.extend(
|
||||
older
|
||||
.chunk
|
||||
.iter()
|
||||
.filter_map(|ev| to_timeline_event(ev, is_encrypted)),
|
||||
);
|
||||
}
|
||||
out
|
||||
}
|
||||
} else {
|
||||
let mut opts = MessagesOptions::backward();
|
||||
opts.limit = UInt::from(limit_val);
|
||||
let msgs = match room.messages(opts).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => return DaemonResponse::error(format!("messages: {e}")),
|
||||
};
|
||||
msgs.chunk
|
||||
.iter()
|
||||
.filter_map(|ev| to_timeline_event(ev, is_encrypted))
|
||||
.collect()
|
||||
};
|
||||
DaemonResponse::ok(&events)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue