atagen

macltok

public
Star0
Watch0
ff7ae2e70f2420a6bb4768fc18ada8571f98cc29
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/engine/context.rs

da8896938cf0atagen2026-10-25 10:04 UTC1
use std::{
da8896938cf0atagen2026-10-25 10:04 UTC2
    collections::{HashMap, VecDeque},
da8896938cf0atagen2026-10-25 10:04 UTC3
    sync::Arc,
da8896938cf0atagen2026-10-25 10:04 UTC4
};
89bc9f8d246datagen2026-10-24 13:40 UTC5
cb7b3362e1ceatagen2026-10-25 09:08 UTC6
use futures_util::future::{join_all, try_join};
913832a327a8atagen2026-10-25 09:33 UTC7
use matrix_sdk::{
da8896938cf0atagen2026-10-25 10:04 UTC8
    Client, Room, RoomState, deserialized_responses::TimelineEvent as SdkTimelineEvent,
da8896938cf0atagen2026-10-25 10:04 UTC9
    room::MessagesOptions,
913832a327a8atagen2026-10-25 09:33 UTC10
};
da8896938cf0atagen2026-10-25 10:04 UTC11
use ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, UInt, push::Action};
cb7b3362e1ceatagen2026-10-25 09:08 UTC12
use serde_json::Value;
da8896938cf0atagen2026-10-25 10:04 UTC13
use tokio::sync::{Mutex, broadcast::error::RecvError};
89bc9f8d246datagen2026-10-24 13:40 UTC14
89bc9f8d246datagen2026-10-24 13:40 UTC15
use crate::{
1f875b103b27atagen2026-10-25 10:44 UTC16
    Error, ErrorKind, Event, EventContext, EventContextCloseReason, EventContextCommand,
21b9227729a4atagen2026-10-25 11:03 UTC17
    EventContextPage, EventContextSession, Sender, TimelineEvent, TimelineItem,
cbb715e11963atagen2026-10-24 23:24 UTC18
    engine::MediaService,
cb7b3362e1ceatagen2026-10-25 09:08 UTC19
    normalise::{
21b9227729a4atagen2026-10-25 11:03 UTC20
        MediaRegistration, MediaScope, apply_profiles, member_sender, normalise_context,
6b04099c763catagen2026-10-25 14:17 UTC21
        normalise_observed, resolve_replies,
cb7b3362e1ceatagen2026-10-25 09:08 UTC22
    },
1f875b103b27atagen2026-10-25 10:44 UTC23
    transport::EventSender,
89bc9f8d246datagen2026-10-24 13:40 UTC24
};
89bc9f8d246datagen2026-10-24 13:40 UTC25
89bc9f8d246datagen2026-10-24 13:40 UTC26
const MAX_CONTEXT_LIMIT: u16 = 50;
da8896938cf0atagen2026-10-25 10:04 UTC27
/// a consumer that never closes its jump views still holds at most this many
da8896938cf0atagen2026-10-25 10:04 UTC28
const MAX_SESSIONS: usize = 32;
da8896938cf0atagen2026-10-25 10:04 UTC29
da8896938cf0atagen2026-10-25 10:04 UTC30
/// open event contexts, oldest first, each owning the media its pages registered
da8896938cf0atagen2026-10-25 10:04 UTC31
pub(crate) struct Sessions {
da8896938cf0atagen2026-10-25 10:04 UTC32
    open: Mutex<VecDeque<Session>>,
1f875b103b27atagen2026-10-25 10:44 UTC33
    events: EventSender,
da8896938cf0atagen2026-10-25 10:04 UTC34
}
da8896938cf0atagen2026-10-25 10:04 UTC35
da8896938cf0atagen2026-10-25 10:04 UTC36
struct Session {
da8896938cf0atagen2026-10-25 10:04 UTC37
    id: String,
da8896938cf0atagen2026-10-25 10:04 UTC38
    room_id: OwnedRoomId,
da8896938cf0atagen2026-10-25 10:04 UTC39
    scope: MediaScope,
da8896938cf0atagen2026-10-25 10:04 UTC40
}
da8896938cf0atagen2026-10-25 10:04 UTC41
1f875b103b27atagen2026-10-25 10:44 UTC42
impl Session {
1f875b103b27atagen2026-10-25 10:44 UTC43
    /// media ids carry the session, so two owners of one event never share an entry
1f875b103b27atagen2026-10-25 10:44 UTC44
    fn media_prefix(scope: &MediaScope) -> String {
1f875b103b27atagen2026-10-25 10:44 UTC45
        format!("{}#", scope.nonce())
1f875b103b27atagen2026-10-25 10:44 UTC46
    }
1f875b103b27atagen2026-10-25 10:44 UTC47
}
1f875b103b27atagen2026-10-25 10:44 UTC48
da8896938cf0atagen2026-10-25 10:04 UTC49
impl Sessions {
1f875b103b27atagen2026-10-25 10:44 UTC50
    pub(crate) fn new(events: EventSender) -> Self {
1f875b103b27atagen2026-10-25 10:44 UTC51
        Self {
1f875b103b27atagen2026-10-25 10:44 UTC52
            open: Mutex::default(),
1f875b103b27atagen2026-10-25 10:44 UTC53
            events,
1f875b103b27atagen2026-10-25 10:44 UTC54
        }
1f875b103b27atagen2026-10-25 10:44 UTC55
    }
1f875b103b27atagen2026-10-25 10:44 UTC56
da8896938cf0atagen2026-10-25 10:04 UTC57
    async fn open(
da8896938cf0atagen2026-10-25 10:04 UTC58
        &self,
da8896938cf0atagen2026-10-25 10:04 UTC59
        media: &MediaService,
da8896938cf0atagen2026-10-25 10:04 UTC60
        room_id: OwnedRoomId,
1f875b103b27atagen2026-10-25 10:44 UTC61
        scope: MediaScope,
da8896938cf0atagen2026-10-25 10:04 UTC62
        registrations: Vec<MediaRegistration>,
da8896938cf0atagen2026-10-25 10:04 UTC63
    ) -> String {
da8896938cf0atagen2026-10-25 10:04 UTC64
        let id = format!("context-{}", scope.nonce());
1f875b103b27atagen2026-10-25 10:44 UTC65
        let mut evicted = Vec::new();
1f875b103b27atagen2026-10-25 10:44 UTC66
        {
1f875b103b27atagen2026-10-25 10:44 UTC67
            let mut open = self.open.lock().await;
1f875b103b27atagen2026-10-25 10:44 UTC68
            media.register(&scope, registrations).await;
1f875b103b27atagen2026-10-25 10:44 UTC69
            open.push_back(Session {
1f875b103b27atagen2026-10-25 10:44 UTC70
                id: id.clone(),
1f875b103b27atagen2026-10-25 10:44 UTC71
                room_id,
1f875b103b27atagen2026-10-25 10:44 UTC72
                scope,
1f875b103b27atagen2026-10-25 10:44 UTC73
            });
1f875b103b27atagen2026-10-25 10:44 UTC74
            while open.len() > MAX_SESSIONS {
1f875b103b27atagen2026-10-25 10:44 UTC75
                if let Some(oldest) = open.pop_front() {
1f875b103b27atagen2026-10-25 10:44 UTC76
                    media.release(&oldest.scope).await;
1f875b103b27atagen2026-10-25 10:44 UTC77
                    evicted.push(oldest.id);
1f875b103b27atagen2026-10-25 10:44 UTC78
                }
da8896938cf0atagen2026-10-25 10:04 UTC79
            }
da8896938cf0atagen2026-10-25 10:04 UTC80
        }
1f875b103b27atagen2026-10-25 10:44 UTC81
        self.announce(evicted, EventContextCloseReason::Evicted)
1f875b103b27atagen2026-10-25 10:44 UTC82
            .await;
da8896938cf0atagen2026-10-25 10:04 UTC83
        id
da8896938cf0atagen2026-10-25 10:04 UTC84
    }
da8896938cf0atagen2026-10-25 10:04 UTC85
1f875b103b27atagen2026-10-25 10:44 UTC86
    /// the room and the media prefix the session's pages use
1f875b103b27atagen2026-10-25 10:44 UTC87
    async fn find(&self, id: &str) -> Result<(OwnedRoomId, String), Error> {
da8896938cf0atagen2026-10-25 10:04 UTC88
        let open = self.open.lock().await;
1f875b103b27atagen2026-10-25 10:44 UTC89
        let session = open
1f875b103b27atagen2026-10-25 10:44 UTC90
            .iter()
1f875b103b27atagen2026-10-25 10:44 UTC91
            .find(|session| session.id == id)
1f875b103b27atagen2026-10-25 10:44 UTC92
            .ok_or_else(not_open)?;
1f875b103b27atagen2026-10-25 10:44 UTC93
        Ok((
1f875b103b27atagen2026-10-25 10:44 UTC94
            session.room_id.clone(),
1f875b103b27atagen2026-10-25 10:44 UTC95
            Session::media_prefix(&session.scope),
1f875b103b27atagen2026-10-25 10:44 UTC96
        ))
da8896938cf0atagen2026-10-25 10:04 UTC97
    }
da8896938cf0atagen2026-10-25 10:04 UTC98
da8896938cf0atagen2026-10-25 10:04 UTC99
    /// under the lock, so a page that lands after its close registers nothing
da8896938cf0atagen2026-10-25 10:04 UTC100
    async fn register(
da8896938cf0atagen2026-10-25 10:04 UTC101
        &self,
da8896938cf0atagen2026-10-25 10:04 UTC102
        media: &MediaService,
da8896938cf0atagen2026-10-25 10:04 UTC103
        id: &str,
da8896938cf0atagen2026-10-25 10:04 UTC104
        registrations: Vec<MediaRegistration>,
da8896938cf0atagen2026-10-25 10:04 UTC105
    ) -> Result<(), Error> {
da8896938cf0atagen2026-10-25 10:04 UTC106
        let open = self.open.lock().await;
da8896938cf0atagen2026-10-25 10:04 UTC107
        let session = open
da8896938cf0atagen2026-10-25 10:04 UTC108
            .iter()
da8896938cf0atagen2026-10-25 10:04 UTC109
            .find(|session| session.id == id)
da8896938cf0atagen2026-10-25 10:04 UTC110
            .ok_or_else(not_open)?;
da8896938cf0atagen2026-10-25 10:04 UTC111
        media.register(&session.scope, registrations).await;
da8896938cf0atagen2026-10-25 10:04 UTC112
        Ok(())
da8896938cf0atagen2026-10-25 10:04 UTC113
    }
da8896938cf0atagen2026-10-25 10:04 UTC114
da8896938cf0atagen2026-10-25 10:04 UTC115
    async fn close(&self, media: &MediaService, id: &str) -> Result<(), Error> {
da8896938cf0atagen2026-10-25 10:04 UTC116
        let mut open = self.open.lock().await;
da8896938cf0atagen2026-10-25 10:04 UTC117
        let index = open
da8896938cf0atagen2026-10-25 10:04 UTC118
            .iter()
da8896938cf0atagen2026-10-25 10:04 UTC119
            .position(|session| session.id == id)
da8896938cf0atagen2026-10-25 10:04 UTC120
            .ok_or_else(not_open)?;
da8896938cf0atagen2026-10-25 10:04 UTC121
        if let Some(session) = open.remove(index) {
da8896938cf0atagen2026-10-25 10:04 UTC122
            media.release(&session.scope).await;
da8896938cf0atagen2026-10-25 10:04 UTC123
        }
da8896938cf0atagen2026-10-25 10:04 UTC124
        Ok(())
da8896938cf0atagen2026-10-25 10:04 UTC125
    }
da8896938cf0atagen2026-10-25 10:04 UTC126
da8896938cf0atagen2026-10-25 10:04 UTC127
    async fn close_left(&self, media: &MediaService, client: &Client) {
1f875b103b27atagen2026-10-25 10:44 UTC128
        let mut closed = Vec::new();
1f875b103b27atagen2026-10-25 10:44 UTC129
        {
1f875b103b27atagen2026-10-25 10:44 UTC130
            let mut open = self.open.lock().await;
1f875b103b27atagen2026-10-25 10:44 UTC131
            let mut kept = VecDeque::with_capacity(open.len());
1f875b103b27atagen2026-10-25 10:44 UTC132
            for session in open.drain(..) {
fbeaaa098107atagen2026-10-25 12:10 UTC133
                let left = client
fbeaaa098107atagen2026-10-25 12:10 UTC134
                    .get_room(&session.room_id)
fbeaaa098107atagen2026-10-25 12:10 UTC135
                    .is_none_or(|room| matches!(room.state(), RoomState::Left | RoomState::Banned));
1f875b103b27atagen2026-10-25 10:44 UTC136
                if left {
1f875b103b27atagen2026-10-25 10:44 UTC137
                    media.release(&session.scope).await;
1f875b103b27atagen2026-10-25 10:44 UTC138
                    closed.push(session.id);
1f875b103b27atagen2026-10-25 10:44 UTC139
                } else {
1f875b103b27atagen2026-10-25 10:44 UTC140
                    kept.push_back(session);
1f875b103b27atagen2026-10-25 10:44 UTC141
                }
1f875b103b27atagen2026-10-25 10:44 UTC142
            }
1f875b103b27atagen2026-10-25 10:44 UTC143
            *open = kept;
1f875b103b27atagen2026-10-25 10:44 UTC144
        }
1f875b103b27atagen2026-10-25 10:44 UTC145
        self.announce(closed, EventContextCloseReason::RoomLeft)
1f875b103b27atagen2026-10-25 10:44 UTC146
            .await;
1f875b103b27atagen2026-10-25 10:44 UTC147
    }
1f875b103b27atagen2026-10-25 10:44 UTC148
1f875b103b27atagen2026-10-25 10:44 UTC149
    async fn announce(&self, ids: Vec<String>, reason: EventContextCloseReason) {
1f875b103b27atagen2026-10-25 10:44 UTC150
        for context_id in ids {
1f875b103b27atagen2026-10-25 10:44 UTC151
            let event = Event::EventContextClosed {
1f875b103b27atagen2026-10-25 10:44 UTC152
                context_id,
1f875b103b27atagen2026-10-25 10:44 UTC153
                reason: reason.clone(),
1f875b103b27atagen2026-10-25 10:44 UTC154
            };
1f875b103b27atagen2026-10-25 10:44 UTC155
            if self.events.send(event).await.is_err() {
1f875b103b27atagen2026-10-25 10:44 UTC156
                return;
da8896938cf0atagen2026-10-25 10:04 UTC157
            }
da8896938cf0atagen2026-10-25 10:04 UTC158
        }
da8896938cf0atagen2026-10-25 10:04 UTC159
    }
da8896938cf0atagen2026-10-25 10:04 UTC160
da8896938cf0atagen2026-10-25 10:04 UTC161
    /// the media service is cleared beside this, so nothing is released one by one
da8896938cf0atagen2026-10-25 10:04 UTC162
    pub(crate) async fn clear(&self) {
da8896938cf0atagen2026-10-25 10:04 UTC163
        self.open.lock().await.clear();
da8896938cf0atagen2026-10-25 10:04 UTC164
    }
da8896938cf0atagen2026-10-25 10:04 UTC165
}
da8896938cf0atagen2026-10-25 10:04 UTC166
da8896938cf0atagen2026-10-25 10:04 UTC167
fn not_open() -> Error {
da8896938cf0atagen2026-10-25 10:04 UTC168
    Error::new(
da8896938cf0atagen2026-10-25 10:04 UTC169
        ErrorKind::EventContextNotOpen,
da8896938cf0atagen2026-10-25 10:04 UTC170
        "event context is closed or was never opened",
da8896938cf0atagen2026-10-25 10:04 UTC171
    )
da8896938cf0atagen2026-10-25 10:04 UTC172
}
da8896938cf0atagen2026-10-25 10:04 UTC173
da8896938cf0atagen2026-10-25 10:04 UTC174
/// leaving a room closes its event contexts
da8896938cf0atagen2026-10-25 10:04 UTC175
pub(super) async fn close_on_leave(
da8896938cf0atagen2026-10-25 10:04 UTC176
    client: Client,
da8896938cf0atagen2026-10-25 10:04 UTC177
    media: Arc<MediaService>,
da8896938cf0atagen2026-10-25 10:04 UTC178
    sessions: Arc<Sessions>,
da8896938cf0atagen2026-10-25 10:04 UTC179
) {
da8896938cf0atagen2026-10-25 10:04 UTC180
    let mut info = client.room_info_notable_update_receiver();
da8896938cf0atagen2026-10-25 10:04 UTC181
    loop {
da8896938cf0atagen2026-10-25 10:04 UTC182
        match info.recv().await {
da8896938cf0atagen2026-10-25 10:04 UTC183
            Ok(_) | Err(RecvError::Lagged(_)) => sessions.close_left(&media, &client).await,
da8896938cf0atagen2026-10-25 10:04 UTC184
            Err(RecvError::Closed) => return,
da8896938cf0atagen2026-10-25 10:04 UTC185
        }
da8896938cf0atagen2026-10-25 10:04 UTC186
    }
da8896938cf0atagen2026-10-25 10:04 UTC187
}
89bc9f8d246datagen2026-10-24 13:40 UTC188
89bc9f8d246datagen2026-10-24 13:40 UTC189
pub(crate) async fn execute(
da8896938cf0atagen2026-10-25 10:04 UTC190
    client: Client,
cbb715e11963atagen2026-10-24 23:24 UTC191
    media: Arc<MediaService>,
da8896938cf0atagen2026-10-25 10:04 UTC192
    sessions: Arc<Sessions>,
89bc9f8d246datagen2026-10-24 13:40 UTC193
    command: EventContextCommand,
89bc9f8d246datagen2026-10-24 13:40 UTC194
) -> Result<crate::CommandOutput, Error> {
da8896938cf0atagen2026-10-25 10:04 UTC195
    let current_user_id = client.user_id().map(ToOwned::to_owned);
da8896938cf0atagen2026-10-25 10:04 UTC196
    let room = |room_id: &OwnedRoomId| client.get_room(room_id).ok_or_else(Error::room_unknown);
da8896938cf0atagen2026-10-25 10:04 UTC197
    let (context_id, token, limit, backwards) = match command {
89bc9f8d246datagen2026-10-24 13:40 UTC198
        EventContextCommand::Fetch {
da8896938cf0atagen2026-10-25 10:04 UTC199
            room_id,
89bc9f8d246datagen2026-10-24 13:40 UTC200
            event_id,
89bc9f8d246datagen2026-10-24 13:40 UTC201
            before_limit,
89bc9f8d246datagen2026-10-24 13:40 UTC202
            after_limit,
89bc9f8d246datagen2026-10-24 13:40 UTC203
        } => {
1f875b103b27atagen2026-10-25 10:44 UTC204
            let scope = media.context_scope();
1f875b103b27atagen2026-10-25 10:44 UTC205
            let (context, registrations, _) = capture_in(
da8896938cf0atagen2026-10-25 10:04 UTC206
                room(&room_id)?,
da8896938cf0atagen2026-10-25 10:04 UTC207
                current_user_id,
da8896938cf0atagen2026-10-25 10:04 UTC208
                event_id,
da8896938cf0atagen2026-10-25 10:04 UTC209
                before_limit,
da8896938cf0atagen2026-10-25 10:04 UTC210
                after_limit,
1f875b103b27atagen2026-10-25 10:44 UTC211
                &Session::media_prefix(&scope),
da8896938cf0atagen2026-10-25 10:04 UTC212
            )
da8896938cf0atagen2026-10-25 10:04 UTC213
            .await?;
1f875b103b27atagen2026-10-25 10:44 UTC214
            let context_id = sessions.open(&media, room_id, scope, registrations).await;
da8896938cf0atagen2026-10-25 10:04 UTC215
            return Ok(crate::CommandOutput::EventContext(EventContextSession {
da8896938cf0atagen2026-10-25 10:04 UTC216
                context_id,
da8896938cf0atagen2026-10-25 10:04 UTC217
                context,
da8896938cf0atagen2026-10-25 10:04 UTC218
            }));
da8896938cf0atagen2026-10-25 10:04 UTC219
        }
da8896938cf0atagen2026-10-25 10:04 UTC220
        EventContextCommand::Close { context_id } => {
da8896938cf0atagen2026-10-25 10:04 UTC221
            sessions.close(&media, &context_id).await?;
da8896938cf0atagen2026-10-25 10:04 UTC222
            return Ok(crate::CommandOutput::Done);
89bc9f8d246datagen2026-10-24 13:40 UTC223
        }
da8896938cf0atagen2026-10-25 10:04 UTC224
        EventContextCommand::Before {
da8896938cf0atagen2026-10-25 10:04 UTC225
            context_id,
da8896938cf0atagen2026-10-25 10:04 UTC226
            token,
da8896938cf0atagen2026-10-25 10:04 UTC227
            limit,
da8896938cf0atagen2026-10-25 10:04 UTC228
        } => (context_id, token, limit, true),
da8896938cf0atagen2026-10-25 10:04 UTC229
        EventContextCommand::After {
da8896938cf0atagen2026-10-25 10:04 UTC230
            context_id,
da8896938cf0atagen2026-10-25 10:04 UTC231
            token,
da8896938cf0atagen2026-10-25 10:04 UTC232
            limit,
da8896938cf0atagen2026-10-25 10:04 UTC233
        } => (context_id, token, limit, false),
89bc9f8d246datagen2026-10-24 13:40 UTC234
    };
cb7b3362e1ceatagen2026-10-25 09:08 UTC235
    if limit == 0 {
cb7b3362e1ceatagen2026-10-25 09:08 UTC236
        return Err(Error::new(
cb7b3362e1ceatagen2026-10-25 09:08 UTC237
            ErrorKind::Invalid,
cb7b3362e1ceatagen2026-10-25 09:08 UTC238
            "context page limit must be at least 1",
cb7b3362e1ceatagen2026-10-25 09:08 UTC239
        ));
cb7b3362e1ceatagen2026-10-25 09:08 UTC240
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC241
    validate_limit(limit)?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC242
    if token.is_empty() {
cb7b3362e1ceatagen2026-10-25 09:08 UTC243
        return Err(Error::new(
cb7b3362e1ceatagen2026-10-25 09:08 UTC244
            ErrorKind::Invalid,
cb7b3362e1ceatagen2026-10-25 09:08 UTC245
            "context pagination token is empty",
cb7b3362e1ceatagen2026-10-25 09:08 UTC246
        ));
cb7b3362e1ceatagen2026-10-25 09:08 UTC247
    }
1f875b103b27atagen2026-10-25 10:44 UTC248
    let (room_id, media_prefix) = sessions.find(&context_id).await?;
1f875b103b27atagen2026-10-25 10:44 UTC249
    let room = room(&room_id)?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC250
    let side = fetch_side(&room, Some(&token), limit, backwards).await?;
913832a327a8atagen2026-10-25 09:33 UTC251
    let mut page = Assembly::new(
913832a327a8atagen2026-10-25 09:33 UTC252
        &room,
913832a327a8atagen2026-10-25 09:33 UTC253
        current_user_id.as_deref(),
913832a327a8atagen2026-10-25 09:33 UTC254
        &side.values,
913832a327a8atagen2026-10-25 09:33 UTC255
        &side.highlights,
913832a327a8atagen2026-10-25 09:33 UTC256
        None,
1f875b103b27atagen2026-10-25 10:44 UTC257
        &media_prefix,
913832a327a8atagen2026-10-25 09:33 UTC258
    )
913832a327a8atagen2026-10-25 09:33 UTC259
    .await;
cb7b3362e1ceatagen2026-10-25 09:08 UTC260
    let events = page.take(0..side.values.len());
da8896938cf0atagen2026-10-25 10:04 UTC261
    sessions.register(&media, &context_id, page.media).await?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC262
    Ok(crate::CommandOutput::EventContextPage(EventContextPage {
cb7b3362e1ceatagen2026-10-25 09:08 UTC263
        events,
cb7b3362e1ceatagen2026-10-25 09:08 UTC264
        next_token: side.token,
cb7b3362e1ceatagen2026-10-25 09:08 UTC265
    }))
cbb715e11963atagen2026-10-24 23:24 UTC266
}
89bc9f8d246datagen2026-10-24 13:40 UTC267
cbb715e11963atagen2026-10-24 23:24 UTC268
pub(crate) async fn capture(
cbb715e11963atagen2026-10-24 23:24 UTC269
    room: Room,
cbb715e11963atagen2026-10-24 23:24 UTC270
    current_user_id: Option<OwnedUserId>,
cbb715e11963atagen2026-10-24 23:24 UTC271
    event_id: OwnedEventId,
cbb715e11963atagen2026-10-24 23:24 UTC272
    before_limit: u16,
cbb715e11963atagen2026-10-24 23:24 UTC273
    after_limit: u16,
cb7b3362e1ceatagen2026-10-25 09:08 UTC274
) -> Result<(EventContext, Vec<MediaRegistration>, Vec<Value>), Error> {
fbeaaa098107atagen2026-10-25 12:10 UTC275
    capture_in(
fbeaaa098107atagen2026-10-25 12:10 UTC276
        room,
fbeaaa098107atagen2026-10-25 12:10 UTC277
        current_user_id,
fbeaaa098107atagen2026-10-25 12:10 UTC278
        event_id,
fbeaaa098107atagen2026-10-25 12:10 UTC279
        before_limit,
fbeaaa098107atagen2026-10-25 12:10 UTC280
        after_limit,
fbeaaa098107atagen2026-10-25 12:10 UTC281
        "",
fbeaaa098107atagen2026-10-25 12:10 UTC282
    )
fbeaaa098107atagen2026-10-25 12:10 UTC283
    .await
1f875b103b27atagen2026-10-25 10:44 UTC284
}
1f875b103b27atagen2026-10-25 10:44 UTC285
1f875b103b27atagen2026-10-25 10:44 UTC286
async fn capture_in(
1f875b103b27atagen2026-10-25 10:44 UTC287
    room: Room,
1f875b103b27atagen2026-10-25 10:44 UTC288
    current_user_id: Option<OwnedUserId>,
1f875b103b27atagen2026-10-25 10:44 UTC289
    event_id: OwnedEventId,
1f875b103b27atagen2026-10-25 10:44 UTC290
    before_limit: u16,
1f875b103b27atagen2026-10-25 10:44 UTC291
    after_limit: u16,
1f875b103b27atagen2026-10-25 10:44 UTC292
    media_prefix: &str,
1f875b103b27atagen2026-10-25 10:44 UTC293
) -> Result<(EventContext, Vec<MediaRegistration>, Vec<Value>), Error> {
cbb715e11963atagen2026-10-24 23:24 UTC294
    validate_limit(before_limit)?;
cbb715e11963atagen2026-10-24 23:24 UTC295
    validate_limit(after_limit)?;
cbb715e11963atagen2026-10-24 23:24 UTC296
    let raw = room
cbb715e11963atagen2026-10-24 23:24 UTC297
        .event_with_context(&event_id, false, UInt::from(0_u16), None)
89bc9f8d246datagen2026-10-24 13:40 UTC298
        .await
89bc9f8d246datagen2026-10-24 13:40 UTC299
        .map_err(|source| {
cbb715e11963atagen2026-10-24 23:24 UTC300
            let error = Error::from_sdk("fetching event context failed", source);
cbb715e11963atagen2026-10-24 23:24 UTC301
            if error
cbb715e11963atagen2026-10-24 23:24 UTC302
                .server
cbb715e11963atagen2026-10-24 23:24 UTC303
                .as_ref()
cbb715e11963atagen2026-10-24 23:24 UTC304
                .is_some_and(|server| server.status == 404)
cbb715e11963atagen2026-10-24 23:24 UTC305
            {
cbb715e11963atagen2026-10-24 23:24 UTC306
                Error::new(ErrorKind::EventNotFound, "event was not found")
cbb715e11963atagen2026-10-24 23:24 UTC307
            } else {
cbb715e11963atagen2026-10-24 23:24 UTC308
                error
cbb715e11963atagen2026-10-24 23:24 UTC309
            }
89bc9f8d246datagen2026-10-24 13:40 UTC310
        })?;
913832a327a8atagen2026-10-25 09:33 UTC311
    let (target, target_highlight) = raw
cbb715e11963atagen2026-10-24 23:24 UTC312
        .event
cbb715e11963atagen2026-10-24 23:24 UTC313
        .as_ref()
913832a327a8atagen2026-10-25 09:33 UTC314
        .and_then(raw_event)
cb7b3362e1ceatagen2026-10-25 09:08 UTC315
        .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "event was not found"))?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC316
    let (before, after) = try_join(
cb7b3362e1ceatagen2026-10-25 09:08 UTC317
        fetch_side(&room, raw.prev_batch_token.as_deref(), before_limit, true),
cb7b3362e1ceatagen2026-10-25 09:08 UTC318
        fetch_side(&room, raw.next_batch_token.as_deref(), after_limit, false),
cb7b3362e1ceatagen2026-10-25 09:08 UTC319
    )
cb7b3362e1ceatagen2026-10-25 09:08 UTC320
    .await?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC321
    let target_index = before.values.len();
cb7b3362e1ceatagen2026-10-25 09:08 UTC322
    let mut values = before.values;
cb7b3362e1ceatagen2026-10-25 09:08 UTC323
    values.push(target);
cb7b3362e1ceatagen2026-10-25 09:08 UTC324
    values.extend(after.values);
913832a327a8atagen2026-10-25 09:33 UTC325
    let mut highlights = before.highlights;
913832a327a8atagen2026-10-25 09:33 UTC326
    highlights.push(target_highlight);
913832a327a8atagen2026-10-25 09:33 UTC327
    highlights.extend(after.highlights);
cb7b3362e1ceatagen2026-10-25 09:08 UTC328
    let mut page = Assembly::new(
cb7b3362e1ceatagen2026-10-25 09:08 UTC329
        &room,
cb7b3362e1ceatagen2026-10-25 09:08 UTC330
        current_user_id.as_deref(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC331
        &values,
913832a327a8atagen2026-10-25 09:33 UTC332
        &highlights,
cb7b3362e1ceatagen2026-10-25 09:08 UTC333
        Some(target_index),
1f875b103b27atagen2026-10-25 10:44 UTC334
        media_prefix,
cb7b3362e1ceatagen2026-10-25 09:08 UTC335
    )
cb7b3362e1ceatagen2026-10-25 09:08 UTC336
    .await;
cb7b3362e1ceatagen2026-10-25 09:08 UTC337
    let events_before = page.take(0..target_index);
cb7b3362e1ceatagen2026-10-25 09:08 UTC338
    let event = page
cb7b3362e1ceatagen2026-10-25 09:08 UTC339
        .take(target_index..target_index + 1)
cb7b3362e1ceatagen2026-10-25 09:08 UTC340
        .pop()
cb7b3362e1ceatagen2026-10-25 09:08 UTC341
        .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "event could not be read"))?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC342
    let events_after = page.take(target_index + 1..values.len());
cbb715e11963atagen2026-10-24 23:24 UTC343
    Ok((
cbb715e11963atagen2026-10-24 23:24 UTC344
        EventContext {
cbb715e11963atagen2026-10-24 23:24 UTC345
            events_before,
cbb715e11963atagen2026-10-24 23:24 UTC346
            event,
cbb715e11963atagen2026-10-24 23:24 UTC347
            events_after,
cb7b3362e1ceatagen2026-10-25 09:08 UTC348
            start_token: before.token,
cb7b3362e1ceatagen2026-10-25 09:08 UTC349
            end_token: after.token,
cbb715e11963atagen2026-10-24 23:24 UTC350
        },
cb7b3362e1ceatagen2026-10-25 09:08 UTC351
        page.media,
cb7b3362e1ceatagen2026-10-25 09:08 UTC352
        values,
cbb715e11963atagen2026-10-24 23:24 UTC353
    ))
cbb715e11963atagen2026-10-24 23:24 UTC354
}
89bc9f8d246datagen2026-10-24 13:40 UTC355
cb7b3362e1ceatagen2026-10-25 09:08 UTC356
struct Side {
cb7b3362e1ceatagen2026-10-25 09:08 UTC357
    /// in timeline order
cb7b3362e1ceatagen2026-10-25 09:08 UTC358
    values: Vec<Value>,
913832a327a8atagen2026-10-25 09:33 UTC359
    /// beside `values`, from the push actions the sdk worked out as it fetched
913832a327a8atagen2026-10-25 09:33 UTC360
    highlights: Vec<bool>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC361
    token: Option<String>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC362
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC363
913832a327a8atagen2026-10-25 09:33 UTC364
fn raw_event(event: &SdkTimelineEvent) -> Option<(Value, bool)> {
913832a327a8atagen2026-10-25 09:33 UTC365
    let value = serde_json::from_str(event.raw().json().get()).ok()?;
913832a327a8atagen2026-10-25 09:33 UTC366
    let highlight = event
913832a327a8atagen2026-10-25 09:33 UTC367
        .push_actions()
913832a327a8atagen2026-10-25 09:33 UTC368
        .is_some_and(|actions| actions.iter().any(Action::is_highlight));
913832a327a8atagen2026-10-25 09:33 UTC369
    Some((value, highlight))
913832a327a8atagen2026-10-25 09:33 UTC370
}
913832a327a8atagen2026-10-25 09:33 UTC371
cbb715e11963atagen2026-10-24 23:24 UTC372
async fn fetch_side(
cbb715e11963atagen2026-10-24 23:24 UTC373
    room: &Room,
cbb715e11963atagen2026-10-24 23:24 UTC374
    token: Option<&str>,
cbb715e11963atagen2026-10-24 23:24 UTC375
    limit: u16,
cbb715e11963atagen2026-10-24 23:24 UTC376
    backwards: bool,
cb7b3362e1ceatagen2026-10-25 09:08 UTC377
) -> Result<Side, Error> {
cbb715e11963atagen2026-10-24 23:24 UTC378
    let Some(token) = token else {
cb7b3362e1ceatagen2026-10-25 09:08 UTC379
        return Ok(Side {
cb7b3362e1ceatagen2026-10-25 09:08 UTC380
            values: Vec::new(),
913832a327a8atagen2026-10-25 09:33 UTC381
            highlights: Vec::new(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC382
            token: None,
cb7b3362e1ceatagen2026-10-25 09:08 UTC383
        });
cbb715e11963atagen2026-10-24 23:24 UTC384
    };
cb7b3362e1ceatagen2026-10-25 09:08 UTC385
    if limit == 0 {
cb7b3362e1ceatagen2026-10-25 09:08 UTC386
        return Ok(Side {
cb7b3362e1ceatagen2026-10-25 09:08 UTC387
            values: Vec::new(),
913832a327a8atagen2026-10-25 09:33 UTC388
            highlights: Vec::new(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC389
            token: Some(token.to_owned()),
cb7b3362e1ceatagen2026-10-25 09:08 UTC390
        });
cb7b3362e1ceatagen2026-10-25 09:08 UTC391
    }
cbb715e11963atagen2026-10-24 23:24 UTC392
    let mut options = if backwards {
cbb715e11963atagen2026-10-24 23:24 UTC393
        MessagesOptions::backward()
cbb715e11963atagen2026-10-24 23:24 UTC394
    } else {
cbb715e11963atagen2026-10-24 23:24 UTC395
        MessagesOptions::forward()
cbb715e11963atagen2026-10-24 23:24 UTC396
    };
cbb715e11963atagen2026-10-24 23:24 UTC397
    options = options.from(token);
cbb715e11963atagen2026-10-24 23:24 UTC398
    options.limit = UInt::from(limit);
cbb715e11963atagen2026-10-24 23:24 UTC399
    let messages = room
cbb715e11963atagen2026-10-24 23:24 UTC400
        .messages(options)
cbb715e11963atagen2026-10-24 23:24 UTC401
        .await
cbb715e11963atagen2026-10-24 23:24 UTC402
        .map_err(|source| Error::from_sdk("fetching event context events failed", source))?;
913832a327a8atagen2026-10-25 09:33 UTC403
    let mut events: Vec<_> = messages.chunk.iter().filter_map(raw_event).collect();
89bc9f8d246datagen2026-10-24 13:40 UTC404
    if backwards {
913832a327a8atagen2026-10-25 09:33 UTC405
        events.reverse();
89bc9f8d246datagen2026-10-24 13:40 UTC406
    }
0e081dee5e28atagen2026-10-25 13:57 UTC407
    let (values, highlights): (Vec<_>, _) = events.into_iter().unzip();
0e081dee5e28atagen2026-10-25 13:57 UTC408
    let token = page_token(backwards, &values, messages.end);
cb7b3362e1ceatagen2026-10-25 09:08 UTC409
    Ok(Side {
cb7b3362e1ceatagen2026-10-25 09:08 UTC410
        values,
913832a327a8atagen2026-10-25 09:33 UTC411
        highlights,
0e081dee5e28atagen2026-10-25 13:57 UTC412
        token,
cb7b3362e1ceatagen2026-10-25 09:08 UTC413
    })
89bc9f8d246datagen2026-10-24 13:40 UTC414
}
89bc9f8d246datagen2026-10-24 13:40 UTC415
0e081dee5e28atagen2026-10-25 13:57 UTC416
/// going back, a page that reaches the room's creation or comes back empty is the start, even
0e081dee5e28atagen2026-10-25 13:57 UTC417
/// when the server still hands out a token after it, as continuwuity does
0e081dee5e28atagen2026-10-25 13:57 UTC418
fn page_token(backwards: bool, values: &[Value], end: Option<String>) -> Option<String> {
0e081dee5e28atagen2026-10-25 13:57 UTC419
    let created = values
0e081dee5e28atagen2026-10-25 13:57 UTC420
        .iter()
0e081dee5e28atagen2026-10-25 13:57 UTC421
        .any(|value| value.get("type").and_then(Value::as_str) == Some("m.room.create"));
0e081dee5e28atagen2026-10-25 13:57 UTC422
    end.filter(|_| !(backwards && (values.is_empty() || created)))
0e081dee5e28atagen2026-10-25 13:57 UTC423
}
0e081dee5e28atagen2026-10-25 13:57 UTC424
c9effeac326aatagen2026-10-25 10:32 UTC425
/// a search hit as event context would show it, `values` its original and then its latest edit
c9effeac326aatagen2026-10-25 10:32 UTC426
pub(crate) async fn preview(
c9effeac326aatagen2026-10-25 10:32 UTC427
    room: &Room,
c9effeac326aatagen2026-10-25 10:32 UTC428
    current_user_id: Option<&ruma::UserId>,
c9effeac326aatagen2026-10-25 10:32 UTC429
    values: &[Value],
c9effeac326aatagen2026-10-25 10:32 UTC430
    highlight: bool,
74befbc72bafatagen2026-10-25 11:18 UTC431
    media_prefix: &str,
c9effeac326aatagen2026-10-25 10:32 UTC432
) -> Option<(TimelineEvent, Vec<MediaRegistration>)> {
c9effeac326aatagen2026-10-25 10:32 UTC433
    let mut page = Assembly::new(
c9effeac326aatagen2026-10-25 10:32 UTC434
        room,
c9effeac326aatagen2026-10-25 10:32 UTC435
        current_user_id,
c9effeac326aatagen2026-10-25 10:32 UTC436
        values,
c9effeac326aatagen2026-10-25 10:32 UTC437
        &vec![highlight; values.len()],
c9effeac326aatagen2026-10-25 10:32 UTC438
        Some(0),
74befbc72bafatagen2026-10-25 11:18 UTC439
        media_prefix,
c9effeac326aatagen2026-10-25 10:32 UTC440
    )
c9effeac326aatagen2026-10-25 10:32 UTC441
    .await;
c9effeac326aatagen2026-10-25 10:32 UTC442
    match page.take(0..1).pop()? {
c9effeac326aatagen2026-10-25 10:32 UTC443
        TimelineItem::Event(event) => Some((*event, page.media)),
c9effeac326aatagen2026-10-25 10:32 UTC444
        _ => None,
c9effeac326aatagen2026-10-25 10:32 UTC445
    }
c9effeac326aatagen2026-10-25 10:32 UTC446
}
c9effeac326aatagen2026-10-25 10:32 UTC447
cb7b3362e1ceatagen2026-10-25 09:08 UTC448
/// a window of raw events normalised together, so relations inside it land on their targets
cb7b3362e1ceatagen2026-10-25 09:08 UTC449
struct Assembly {
cb7b3362e1ceatagen2026-10-25 09:08 UTC450
    items: Vec<(usize, TimelineItem)>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC451
    media: Vec<MediaRegistration>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC452
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC453
cb7b3362e1ceatagen2026-10-25 09:08 UTC454
impl Assembly {
cb7b3362e1ceatagen2026-10-25 09:08 UTC455
    async fn new(
cb7b3362e1ceatagen2026-10-25 09:08 UTC456
        room: &Room,
cb7b3362e1ceatagen2026-10-25 09:08 UTC457
        current_user_id: Option<&ruma::UserId>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC458
        values: &[Value],
913832a327a8atagen2026-10-25 09:33 UTC459
        highlights: &[bool],
cb7b3362e1ceatagen2026-10-25 09:08 UTC460
        keep: Option<usize>,
1f875b103b27atagen2026-10-25 10:44 UTC461
        media_prefix: &str,
cb7b3362e1ceatagen2026-10-25 09:08 UTC462
    ) -> Self {
fbeaaa098107atagen2026-10-25 12:10 UTC463
        let page = normalise_context(values, keep, current_user_id, room.room_id(), media_prefix);
cb7b3362e1ceatagen2026-10-25 09:08 UTC464
        let mut items = page.items;
913832a327a8atagen2026-10-25 09:33 UTC465
        for (index, item) in &mut items {
913832a327a8atagen2026-10-25 09:33 UTC466
            if let TimelineItem::Event(event) = item {
913832a327a8atagen2026-10-25 09:33 UTC467
                event.highlight = highlights[*index];
913832a327a8atagen2026-10-25 09:33 UTC468
            }
913832a327a8atagen2026-10-25 09:33 UTC469
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC470
        let mut media = page.media;
cb7b3362e1ceatagen2026-10-25 09:08 UTC471
        let mut targets: HashMap<OwnedEventId, TimelineEvent> = HashMap::new();
cb7b3362e1ceatagen2026-10-25 09:08 UTC472
        let mut missing = Vec::new();
cb7b3362e1ceatagen2026-10-25 09:08 UTC473
        for (_, item) in &items {
cb7b3362e1ceatagen2026-10-25 09:08 UTC474
            if let TimelineItem::Event(event) = item
cb7b3362e1ceatagen2026-10-25 09:08 UTC475
                && let Some(event_id) = &event.event_id
cb7b3362e1ceatagen2026-10-25 09:08 UTC476
            {
cb7b3362e1ceatagen2026-10-25 09:08 UTC477
                targets.insert(event_id.clone(), (**event).clone());
cb7b3362e1ceatagen2026-10-25 09:08 UTC478
            }
cb7b3362e1ceatagen2026-10-25 09:08 UTC479
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC480
        for event in events(&mut items) {
cb7b3362e1ceatagen2026-10-25 09:08 UTC481
            if let Some(reply) = &event.reply_to
cb7b3362e1ceatagen2026-10-25 09:08 UTC482
                && !targets.contains_key(&reply.event_id)
cb7b3362e1ceatagen2026-10-25 09:08 UTC483
                && !missing.contains(&reply.event_id)
cb7b3362e1ceatagen2026-10-25 09:08 UTC484
            {
cb7b3362e1ceatagen2026-10-25 09:08 UTC485
                missing.push(reply.event_id.clone());
cb7b3362e1ceatagen2026-10-25 09:08 UTC486
            }
cb7b3362e1ceatagen2026-10-25 09:08 UTC487
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC488
        let fetched = join_all(
cb7b3362e1ceatagen2026-10-25 09:08 UTC489
            missing
cb7b3362e1ceatagen2026-10-25 09:08 UTC490
                .into_iter()
1f875b103b27atagen2026-10-25 10:44 UTC491
                .map(|event_id| fetch_reply(room, current_user_id, event_id, media_prefix)),
cb7b3362e1ceatagen2026-10-25 09:08 UTC492
        )
cb7b3362e1ceatagen2026-10-25 09:08 UTC493
        .await;
cb7b3362e1ceatagen2026-10-25 09:08 UTC494
        for (event, registrations) in fetched.into_iter().flatten() {
cbb715e11963atagen2026-10-24 23:24 UTC495
            media.extend(registrations);
cb7b3362e1ceatagen2026-10-25 09:08 UTC496
            if let Some(event_id) = event.event_id.clone() {
cb7b3362e1ceatagen2026-10-25 09:08 UTC497
                targets.insert(event_id, event);
cb7b3362e1ceatagen2026-10-25 09:08 UTC498
            }
cbb715e11963atagen2026-10-24 23:24 UTC499
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC500
        let profiles = profiles(
cb7b3362e1ceatagen2026-10-25 09:08 UTC501
            room,
cb7b3362e1ceatagen2026-10-25 09:08 UTC502
            items
cb7b3362e1ceatagen2026-10-25 09:08 UTC503
                .iter()
cb7b3362e1ceatagen2026-10-25 09:08 UTC504
                .filter_map(|(_, item)| match item {
cb7b3362e1ceatagen2026-10-25 09:08 UTC505
                    TimelineItem::Event(event) => Some(&event.sender.user_id),
cb7b3362e1ceatagen2026-10-25 09:08 UTC506
                    _ => None,
cb7b3362e1ceatagen2026-10-25 09:08 UTC507
                })
cb7b3362e1ceatagen2026-10-25 09:08 UTC508
                .chain(targets.values().map(|event| &event.sender.user_id)),
cb7b3362e1ceatagen2026-10-25 09:08 UTC509
        )
cb7b3362e1ceatagen2026-10-25 09:08 UTC510
        .await;
cb7b3362e1ceatagen2026-10-25 09:08 UTC511
        apply_profiles(targets.values_mut(), &profiles);
cb7b3362e1ceatagen2026-10-25 09:08 UTC512
        apply_profiles(events(&mut items), &profiles);
cb7b3362e1ceatagen2026-10-25 09:08 UTC513
        resolve_replies(events(&mut items), &targets);
cb7b3362e1ceatagen2026-10-25 09:08 UTC514
        Self { items, media }
89bc9f8d246datagen2026-10-24 13:40 UTC515
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC516
cb7b3362e1ceatagen2026-10-25 09:08 UTC517
    /// the items that came from raw events in `range`
cb7b3362e1ceatagen2026-10-25 09:08 UTC518
    fn take(&mut self, range: std::ops::Range<usize>) -> Vec<TimelineItem> {
cb7b3362e1ceatagen2026-10-25 09:08 UTC519
        let (taken, kept) = std::mem::take(&mut self.items)
cb7b3362e1ceatagen2026-10-25 09:08 UTC520
            .into_iter()
cb7b3362e1ceatagen2026-10-25 09:08 UTC521
            .partition(|(index, _)| range.contains(index));
cb7b3362e1ceatagen2026-10-25 09:08 UTC522
        self.items = kept;
cb7b3362e1ceatagen2026-10-25 09:08 UTC523
        taken.into_iter().map(|(_, item)| item).collect()
cb7b3362e1ceatagen2026-10-25 09:08 UTC524
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC525
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC526
cb7b3362e1ceatagen2026-10-25 09:08 UTC527
fn events(items: &mut [(usize, TimelineItem)]) -> impl Iterator<Item = &mut TimelineEvent> {
cb7b3362e1ceatagen2026-10-25 09:08 UTC528
    items.iter_mut().filter_map(|(_, item)| match item {
cb7b3362e1ceatagen2026-10-25 09:08 UTC529
        TimelineItem::Event(event) => Some(&mut **event),
cb7b3362e1ceatagen2026-10-25 09:08 UTC530
        _ => None,
cb7b3362e1ceatagen2026-10-25 09:08 UTC531
    })
89bc9f8d246datagen2026-10-24 13:40 UTC532
}
89bc9f8d246datagen2026-10-24 13:40 UTC533
cb7b3362e1ceatagen2026-10-25 09:08 UTC534
async fn fetch_reply(
cb7b3362e1ceatagen2026-10-25 09:08 UTC535
    room: &Room,
89bc9f8d246datagen2026-10-24 13:40 UTC536
    current_user_id: Option<&ruma::UserId>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC537
    event_id: OwnedEventId,
1f875b103b27atagen2026-10-25 10:44 UTC538
    media_prefix: &str,
cb7b3362e1ceatagen2026-10-25 09:08 UTC539
) -> Option<(TimelineEvent, Vec<MediaRegistration>)> {
cb7b3362e1ceatagen2026-10-25 09:08 UTC540
    let event = room
cb7b3362e1ceatagen2026-10-25 09:08 UTC541
        .load_or_fetch_event(&event_id, None)
cb7b3362e1ceatagen2026-10-25 09:08 UTC542
        .await
cb7b3362e1ceatagen2026-10-25 09:08 UTC543
        .inspect_err(|error| tracing::debug!(%event_id, %error, "fetching a reply target failed"))
cb7b3362e1ceatagen2026-10-25 09:08 UTC544
        .ok()?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC545
    let value: Value = serde_json::from_str(event.raw().json().get()).ok()?;
1f875b103b27atagen2026-10-25 10:44 UTC546
    let identity = format!("{media_prefix}{}\n{event_id}", room.room_id());
cb7b3362e1ceatagen2026-10-25 09:08 UTC547
    match normalise_observed(&value, current_user_id, &identity)? {
cb7b3362e1ceatagen2026-10-25 09:08 UTC548
        (TimelineItem::Event(event), media) => Some((*event, media)),
cb7b3362e1ceatagen2026-10-25 09:08 UTC549
        _ => None,
cb7b3362e1ceatagen2026-10-25 09:08 UTC550
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC551
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC552
cb7b3362e1ceatagen2026-10-25 09:08 UTC553
/// under lazy-loaded members a sender may be unknown locally, and `get_member` fetches the
cb7b3362e1ceatagen2026-10-25 09:08 UTC554
/// member list once for the room when it is
cb7b3362e1ceatagen2026-10-25 09:08 UTC555
async fn profiles<'a>(
cb7b3362e1ceatagen2026-10-25 09:08 UTC556
    room: &Room,
cb7b3362e1ceatagen2026-10-25 09:08 UTC557
    user_ids: impl Iterator<Item = &'a OwnedUserId>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC558
) -> HashMap<OwnedUserId, Sender> {
cb7b3362e1ceatagen2026-10-25 09:08 UTC559
    let mut unique: Vec<&OwnedUserId> = user_ids.collect();
cb7b3362e1ceatagen2026-10-25 09:08 UTC560
    unique.sort();
cb7b3362e1ceatagen2026-10-25 09:08 UTC561
    unique.dedup();
fbeaaa098107atagen2026-10-25 12:10 UTC562
    join_all(
fbeaaa098107atagen2026-10-25 12:10 UTC563
        unique.into_iter().map(|user_id| async move {
fbeaaa098107atagen2026-10-25 12:10 UTC564
            Some((user_id.clone(), member_sender(room, user_id).await?))
fbeaaa098107atagen2026-10-25 12:10 UTC565
        }),
fbeaaa098107atagen2026-10-25 12:10 UTC566
    )
cb7b3362e1ceatagen2026-10-25 09:08 UTC567
    .await
cb7b3362e1ceatagen2026-10-25 09:08 UTC568
    .into_iter()
cb7b3362e1ceatagen2026-10-25 09:08 UTC569
    .flatten()
cb7b3362e1ceatagen2026-10-25 09:08 UTC570
    .collect()
89bc9f8d246datagen2026-10-24 13:40 UTC571
}
89bc9f8d246datagen2026-10-24 13:40 UTC572
cbb715e11963atagen2026-10-24 23:24 UTC573
fn validate_limit(limit: u16) -> Result<(), Error> {
cbb715e11963atagen2026-10-24 23:24 UTC574
    if limit > MAX_CONTEXT_LIMIT {
cbb715e11963atagen2026-10-24 23:24 UTC575
        Err(Error::new(ErrorKind::Invalid, "context limit exceeds 50"))
cbb715e11963atagen2026-10-24 23:24 UTC576
    } else {
cbb715e11963atagen2026-10-24 23:24 UTC577
        Ok(())
cbb715e11963atagen2026-10-24 23:24 UTC578
    }
89bc9f8d246datagen2026-10-24 13:40 UTC579
}
0e081dee5e28atagen2026-10-25 13:57 UTC580
0e081dee5e28atagen2026-10-25 13:57 UTC581
#[cfg(test)]
0e081dee5e28atagen2026-10-25 13:57 UTC582
mod tests {
0e081dee5e28atagen2026-10-25 13:57 UTC583
    use serde_json::json;
0e081dee5e28atagen2026-10-25 13:57 UTC584
0e081dee5e28atagen2026-10-25 13:57 UTC585
    use super::page_token;
0e081dee5e28atagen2026-10-25 13:57 UTC586
0e081dee5e28atagen2026-10-25 13:57 UTC587
    #[test]
0e081dee5e28atagen2026-10-25 13:57 UTC588
    fn going_back_past_creation_or_an_empty_page_is_the_start() {
0e081dee5e28atagen2026-10-25 13:57 UTC589
        let token = || Some("more".to_owned());
0e081dee5e28atagen2026-10-25 13:57 UTC590
        let message = json!({ "type": "m.room.message" });
0e081dee5e28atagen2026-10-25 13:57 UTC591
        let create = json!({ "type": "m.room.create" });
0e081dee5e28atagen2026-10-25 13:57 UTC592
        assert_eq!(page_token(true, &[message.clone()], token()), token());
0e081dee5e28atagen2026-10-25 13:57 UTC593
        assert_eq!(
0e081dee5e28atagen2026-10-25 13:57 UTC594
            page_token(true, &[message.clone(), create.clone()], token()),
0e081dee5e28atagen2026-10-25 13:57 UTC595
            None
0e081dee5e28atagen2026-10-25 13:57 UTC596
        );
0e081dee5e28atagen2026-10-25 13:57 UTC597
        assert_eq!(page_token(true, &[], token()), None);
0e081dee5e28atagen2026-10-25 13:57 UTC598
        assert_eq!(page_token(true, &[message.clone()], None), None);
0e081dee5e28atagen2026-10-25 13:57 UTC599
        assert_eq!(page_token(false, &[], token()), token());
0e081dee5e28atagen2026-10-25 13:57 UTC600
        assert_eq!(page_token(false, &[create], token()), token());
0e081dee5e28atagen2026-10-25 13:57 UTC601
    }
0e081dee5e28atagen2026-10-25 13:57 UTC602
}