From d330930bb50becf61bc2c545d0ea6a9da8997c09 Mon Sep 17 00:00:00 2001 From: Erwan Leboucher Date: Thu, 27 Aug 2026 12:27:46 +0200 Subject: [PATCH] feat: Add timestamp to event endpoint --- changelog.d/+timestamp-to-event.feature.md | 1 + src/api/client/room/mod.rs | 3 +- src/api/client/room/timestamp_to_event.rs | 62 ++++++++++++++++++ src/api/router.rs | 1 + src/service/rooms/timeline/mod.rs | 76 ++++++++++++++++++++-- 5 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 changelog.d/+timestamp-to-event.feature.md create mode 100644 src/api/client/room/timestamp_to_event.rs diff --git a/changelog.d/+timestamp-to-event.feature.md b/changelog.d/+timestamp-to-event.feature.md new file mode 100644 index 000000000..c1f903968 --- /dev/null +++ b/changelog.d/+timestamp-to-event.feature.md @@ -0,0 +1 @@ +Added the `GET /_matrix/client/v1/rooms/{roomId}/timestamp_to_event` endpoint as per MSC3030, letting clients find the event closest to a given timestamp. Contributed by @eleboucher. diff --git a/src/api/client/room/mod.rs b/src/api/client/room/mod.rs index e249e18b6..ba497afce 100644 --- a/src/api/client/room/mod.rs +++ b/src/api/client/room/mod.rs @@ -3,10 +3,11 @@ mod event; mod initial_sync; mod summary; +mod timestamp_to_event; mod upgrade; pub(crate) use self::{ aliases::get_room_aliases_route, create::create_room_route, event::get_room_event_route, initial_sync::room_initial_sync_route, summary::get_room_summary, - upgrade::upgrade_room_route, + timestamp_to_event::get_event_by_timestamp_route, upgrade::upgrade_room_route, }; diff --git a/src/api/client/room/timestamp_to_event.rs b/src/api/client/room/timestamp_to_event.rs new file mode 100644 index 000000000..1ad09e40f --- /dev/null +++ b/src/api/client/room/timestamp_to_event.rs @@ -0,0 +1,62 @@ +use axum::extract::State; +use conduwuit::{Err, Event, Result, err}; +use ruma::api::{Direction, client::room::get_event_by_timestamp}; + +use crate::Ruma; + +/// # `GET /_matrix/client/v1/rooms/{roomId}/timestamp_to_event` +/// +/// Gets the ID of the event closest to the given timestamp, in the direction +/// specified by `dir`. +pub(crate) async fn get_event_by_timestamp_route( + State(ref services): State, + body: Ruma, +) -> Result { + let sender_user = body.identity.expect_sender_user()?; + let room_id = &body.room_id; + + if !services + .rooms + .state_accessor + .user_can_see_state_events(sender_user, room_id) + .await + { + return Err!(Request(Forbidden("You don't have permission to view this room."))); + } + + let dir = match body.dir { + | Direction::Forward => "f", + | Direction::Backward => "b", + }; + + let not_found = || { + err!(Request(NotFound( + "Unable to find event from {} in direction {}", + body.ts.get(), + dir + ))) + }; + + let Some((_, event)) = services + .rooms + .timeline + .event_by_timestamp(room_id, body.ts, body.dir) + .await + else { + return Err(not_found()); + }; + + if !services + .rooms + .state_accessor + .user_can_see_event(sender_user, room_id, event.event_id()) + .await + { + return Err!(Request(Forbidden("You don't have permission to view this event."))); + } + + Ok(get_event_by_timestamp::v1::Response::new( + event.event_id().to_owned(), + event.origin_server_ts(), + )) +} diff --git a/src/api/router.rs b/src/api/router.rs index 6b972043d..58c6d188f 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -52,6 +52,7 @@ pub fn build(router: Router, state: State) -> Router { .ruma_route(&client::set_pushrule_actions_route) .ruma_route(&client::delete_pushrule_route) .ruma_route(&client::get_room_event_route) + .ruma_route(&client::get_event_by_timestamp_route) .ruma_route(&client::get_room_aliases_route) .ruma_route(&client::get_filter_route) .ruma_route(&client::create_filter_route) diff --git a/src/service/rooms/timeline/mod.rs b/src/service/rooms/timeline/mod.rs index 715d52855..c8e20de06 100644 --- a/src/service/rooms/timeline/mod.rs +++ b/src/service/rooms/timeline/mod.rs @@ -16,13 +16,13 @@ event::Event, pdu::{PduCount, PduEvent}, }, - utils::{MutexMap, MutexMapGuard, future::TryExtExt, stream::TryIgnore}, + utils::{MutexMap, MutexMapGuard, ReadyExt, future::TryExtExt, stream::TryIgnore}, warn, }; -use futures::{Future, Stream, TryStreamExt, pin_mut}; +use futures::{Future, Stream, StreamExt, TryStreamExt, pin_mut}; use ruma::{ - CanonicalJsonObject, EventId, OwnedEventId, OwnedRoomId, RoomId, - events::room::encrypted::Relation, + CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, RoomId, + api::Direction, events::room::encrypted::Relation, }; use serde::Deserialize; @@ -272,4 +272,72 @@ pub fn pdus<'a>( ) -> impl Stream> + Send + 'a { self.db.pdus(room_id, from.unwrap_or_else(PduCount::min)) } + + async fn pdu_at_or_after(&self, room_id: &RoomId, count: PduCount) -> Option { + let pdus = self + .pdus(room_id, Some(count.saturating_sub(1))) + .ignore_err() + .ready_filter(move |(at, _)| *at >= count); + + pin_mut!(pdus); + pdus.next().await + } + + async fn pdu_at_or_before(&self, room_id: &RoomId, count: PduCount) -> Option { + let pdus = self + .pdus_rev(room_id, Some(count.saturating_add(1))) + .ignore_err() + .ready_filter(move |(at, _)| *at <= count); + + pin_mut!(pdus); + pdus.next().await + } + + pub async fn event_by_timestamp( + &self, + room_id: &RoomId, + ts: MilliSecondsSinceUnixEpoch, + dir: Direction, + ) -> Option { + let first = self.pdu_at_or_after(room_id, PduCount::min()).await?.0; + let last = self.pdu_at_or_before(room_id, PduCount::max()).await?.0; + let mut low = first.into_signed(); + let mut high = last.into_signed(); + let mut result = None; + + match dir { + | Direction::Forward => + while low <= high { + let count = PduCount::from_signed(low.midpoint(high)); + let Some((at, pdu)) = self.pdu_at_or_after(room_id, count).await else { + high = count.into_signed().saturating_sub(1); + continue; + }; + + if pdu.origin_server_ts() >= ts { + result = Some((at, pdu)); + high = at.into_signed().saturating_sub(1); + } else { + low = at.into_signed().saturating_add(1); + } + }, + | Direction::Backward => + while low <= high { + let count = PduCount::from_signed(low.midpoint(high)); + let Some((at, pdu)) = self.pdu_at_or_before(room_id, count).await else { + low = count.into_signed().saturating_add(1); + continue; + }; + + if pdu.origin_server_ts() <= ts { + result = Some((at, pdu)); + low = at.into_signed().saturating_add(1); + } else { + high = at.into_signed().saturating_sub(1); + } + }, + } + + result + } }