atagen

macltok

public
Star0
Watch0
002c79e4c6ec3bd46e13dff862d52859b5fc9915
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::{
da8896938cf0atagen2026-10-25 10:04 UTC16
    Error, ErrorKind, EventContext, EventContextCommand, EventContextPage, EventContextSession,
da8896938cf0atagen2026-10-25 10:04 UTC17
    MediaId, Sender, TimelineEvent, TimelineItem,
cbb715e11963atagen2026-10-24 23:24 UTC18
    engine::MediaService,
cb7b3362e1ceatagen2026-10-25 09:08 UTC19
    normalise::{
da8896938cf0atagen2026-10-25 10:04 UTC20
        MediaRegistration, MediaScope, apply_profiles, normalise_context, normalise_observed,
da8896938cf0atagen2026-10-25 10:04 UTC21
        resolve_replies,
cb7b3362e1ceatagen2026-10-25 09:08 UTC22
    },
89bc9f8d246datagen2026-10-24 13:40 UTC23
};
89bc9f8d246datagen2026-10-24 13:40 UTC24
89bc9f8d246datagen2026-10-24 13:40 UTC25
const MAX_CONTEXT_LIMIT: u16 = 50;
da8896938cf0atagen2026-10-25 10:04 UTC26
/// a consumer that never closes its jump views still holds at most this many
da8896938cf0atagen2026-10-25 10:04 UTC27
const MAX_SESSIONS: usize = 32;
da8896938cf0atagen2026-10-25 10:04 UTC28
da8896938cf0atagen2026-10-25 10:04 UTC29
/// open event contexts, oldest first, each owning the media its pages registered
da8896938cf0atagen2026-10-25 10:04 UTC30
#[derive(Default)]
da8896938cf0atagen2026-10-25 10:04 UTC31
pub(crate) struct Sessions {
da8896938cf0atagen2026-10-25 10:04 UTC32
    open: Mutex<VecDeque<Session>>,
da8896938cf0atagen2026-10-25 10:04 UTC33
}
da8896938cf0atagen2026-10-25 10:04 UTC34
da8896938cf0atagen2026-10-25 10:04 UTC35
struct Session {
da8896938cf0atagen2026-10-25 10:04 UTC36
    id: String,
da8896938cf0atagen2026-10-25 10:04 UTC37
    room_id: OwnedRoomId,
da8896938cf0atagen2026-10-25 10:04 UTC38
    scope: MediaScope,
da8896938cf0atagen2026-10-25 10:04 UTC39
}
da8896938cf0atagen2026-10-25 10:04 UTC40
da8896938cf0atagen2026-10-25 10:04 UTC41
impl Sessions {
da8896938cf0atagen2026-10-25 10:04 UTC42
    async fn open(
da8896938cf0atagen2026-10-25 10:04 UTC43
        &self,
da8896938cf0atagen2026-10-25 10:04 UTC44
        media: &MediaService,
da8896938cf0atagen2026-10-25 10:04 UTC45
        room_id: OwnedRoomId,
da8896938cf0atagen2026-10-25 10:04 UTC46
        registrations: Vec<MediaRegistration>,
da8896938cf0atagen2026-10-25 10:04 UTC47
    ) -> String {
da8896938cf0atagen2026-10-25 10:04 UTC48
        let scope = media.context_scope();
da8896938cf0atagen2026-10-25 10:04 UTC49
        let id = format!("context-{}", scope.nonce());
da8896938cf0atagen2026-10-25 10:04 UTC50
        let mut open = self.open.lock().await;
da8896938cf0atagen2026-10-25 10:04 UTC51
        media.register(&scope, registrations).await;
da8896938cf0atagen2026-10-25 10:04 UTC52
        open.push_back(Session {
da8896938cf0atagen2026-10-25 10:04 UTC53
            id: id.clone(),
da8896938cf0atagen2026-10-25 10:04 UTC54
            room_id,
da8896938cf0atagen2026-10-25 10:04 UTC55
            scope,
da8896938cf0atagen2026-10-25 10:04 UTC56
        });
da8896938cf0atagen2026-10-25 10:04 UTC57
        while open.len() > MAX_SESSIONS {
da8896938cf0atagen2026-10-25 10:04 UTC58
            if let Some(oldest) = open.pop_front() {
da8896938cf0atagen2026-10-25 10:04 UTC59
                media.release(&oldest.scope).await;
da8896938cf0atagen2026-10-25 10:04 UTC60
            }
da8896938cf0atagen2026-10-25 10:04 UTC61
        }
da8896938cf0atagen2026-10-25 10:04 UTC62
        id
da8896938cf0atagen2026-10-25 10:04 UTC63
    }
da8896938cf0atagen2026-10-25 10:04 UTC64
da8896938cf0atagen2026-10-25 10:04 UTC65
    async fn room(&self, id: &str) -> Result<OwnedRoomId, Error> {
da8896938cf0atagen2026-10-25 10:04 UTC66
        let open = self.open.lock().await;
da8896938cf0atagen2026-10-25 10:04 UTC67
        let session = open.iter().find(|session| session.id == id);
da8896938cf0atagen2026-10-25 10:04 UTC68
        session
da8896938cf0atagen2026-10-25 10:04 UTC69
            .map(|session| session.room_id.clone())
da8896938cf0atagen2026-10-25 10:04 UTC70
            .ok_or_else(not_open)
da8896938cf0atagen2026-10-25 10:04 UTC71
    }
da8896938cf0atagen2026-10-25 10:04 UTC72
da8896938cf0atagen2026-10-25 10:04 UTC73
    /// under the lock, so a page that lands after its close registers nothing
da8896938cf0atagen2026-10-25 10:04 UTC74
    async fn register(
da8896938cf0atagen2026-10-25 10:04 UTC75
        &self,
da8896938cf0atagen2026-10-25 10:04 UTC76
        media: &MediaService,
da8896938cf0atagen2026-10-25 10:04 UTC77
        id: &str,
da8896938cf0atagen2026-10-25 10:04 UTC78
        registrations: Vec<MediaRegistration>,
da8896938cf0atagen2026-10-25 10:04 UTC79
    ) -> Result<(), Error> {
da8896938cf0atagen2026-10-25 10:04 UTC80
        let open = self.open.lock().await;
da8896938cf0atagen2026-10-25 10:04 UTC81
        let session = open
da8896938cf0atagen2026-10-25 10:04 UTC82
            .iter()
da8896938cf0atagen2026-10-25 10:04 UTC83
            .find(|session| session.id == id)
da8896938cf0atagen2026-10-25 10:04 UTC84
            .ok_or_else(not_open)?;
da8896938cf0atagen2026-10-25 10:04 UTC85
        media.register(&session.scope, registrations).await;
da8896938cf0atagen2026-10-25 10:04 UTC86
        Ok(())
da8896938cf0atagen2026-10-25 10:04 UTC87
    }
da8896938cf0atagen2026-10-25 10:04 UTC88
da8896938cf0atagen2026-10-25 10:04 UTC89
    async fn close(&self, media: &MediaService, id: &str) -> Result<(), Error> {
da8896938cf0atagen2026-10-25 10:04 UTC90
        let mut open = self.open.lock().await;
da8896938cf0atagen2026-10-25 10:04 UTC91
        let index = open
da8896938cf0atagen2026-10-25 10:04 UTC92
            .iter()
da8896938cf0atagen2026-10-25 10:04 UTC93
            .position(|session| session.id == id)
da8896938cf0atagen2026-10-25 10:04 UTC94
            .ok_or_else(not_open)?;
da8896938cf0atagen2026-10-25 10:04 UTC95
        if let Some(session) = open.remove(index) {
da8896938cf0atagen2026-10-25 10:04 UTC96
            media.release(&session.scope).await;
da8896938cf0atagen2026-10-25 10:04 UTC97
        }
da8896938cf0atagen2026-10-25 10:04 UTC98
        Ok(())
da8896938cf0atagen2026-10-25 10:04 UTC99
    }
da8896938cf0atagen2026-10-25 10:04 UTC100
da8896938cf0atagen2026-10-25 10:04 UTC101
    async fn close_left(&self, media: &MediaService, client: &Client) {
da8896938cf0atagen2026-10-25 10:04 UTC102
        let mut open = self.open.lock().await;
da8896938cf0atagen2026-10-25 10:04 UTC103
        let mut kept = VecDeque::with_capacity(open.len());
da8896938cf0atagen2026-10-25 10:04 UTC104
        for session in open.drain(..) {
da8896938cf0atagen2026-10-25 10:04 UTC105
            let left = client
da8896938cf0atagen2026-10-25 10:04 UTC106
                .get_room(&session.room_id)
da8896938cf0atagen2026-10-25 10:04 UTC107
                .is_none_or(|room| matches!(room.state(), RoomState::Left | RoomState::Banned));
da8896938cf0atagen2026-10-25 10:04 UTC108
            if left {
da8896938cf0atagen2026-10-25 10:04 UTC109
                media.release(&session.scope).await;
da8896938cf0atagen2026-10-25 10:04 UTC110
            } else {
da8896938cf0atagen2026-10-25 10:04 UTC111
                kept.push_back(session);
da8896938cf0atagen2026-10-25 10:04 UTC112
            }
da8896938cf0atagen2026-10-25 10:04 UTC113
        }
da8896938cf0atagen2026-10-25 10:04 UTC114
        *open = kept;
da8896938cf0atagen2026-10-25 10:04 UTC115
    }
da8896938cf0atagen2026-10-25 10:04 UTC116
da8896938cf0atagen2026-10-25 10:04 UTC117
    /// the media service is cleared beside this, so nothing is released one by one
da8896938cf0atagen2026-10-25 10:04 UTC118
    pub(crate) async fn clear(&self) {
da8896938cf0atagen2026-10-25 10:04 UTC119
        self.open.lock().await.clear();
da8896938cf0atagen2026-10-25 10:04 UTC120
    }
da8896938cf0atagen2026-10-25 10:04 UTC121
}
da8896938cf0atagen2026-10-25 10:04 UTC122
da8896938cf0atagen2026-10-25 10:04 UTC123
fn not_open() -> Error {
da8896938cf0atagen2026-10-25 10:04 UTC124
    Error::new(
da8896938cf0atagen2026-10-25 10:04 UTC125
        ErrorKind::EventContextNotOpen,
da8896938cf0atagen2026-10-25 10:04 UTC126
        "event context is closed or was never opened",
da8896938cf0atagen2026-10-25 10:04 UTC127
    )
da8896938cf0atagen2026-10-25 10:04 UTC128
}
da8896938cf0atagen2026-10-25 10:04 UTC129
da8896938cf0atagen2026-10-25 10:04 UTC130
/// leaving a room closes its event contexts
da8896938cf0atagen2026-10-25 10:04 UTC131
pub(super) async fn close_on_leave(
da8896938cf0atagen2026-10-25 10:04 UTC132
    client: Client,
da8896938cf0atagen2026-10-25 10:04 UTC133
    media: Arc<MediaService>,
da8896938cf0atagen2026-10-25 10:04 UTC134
    sessions: Arc<Sessions>,
da8896938cf0atagen2026-10-25 10:04 UTC135
) {
da8896938cf0atagen2026-10-25 10:04 UTC136
    let mut info = client.room_info_notable_update_receiver();
da8896938cf0atagen2026-10-25 10:04 UTC137
    loop {
da8896938cf0atagen2026-10-25 10:04 UTC138
        match info.recv().await {
da8896938cf0atagen2026-10-25 10:04 UTC139
            Ok(_) | Err(RecvError::Lagged(_)) => sessions.close_left(&media, &client).await,
da8896938cf0atagen2026-10-25 10:04 UTC140
            Err(RecvError::Closed) => return,
da8896938cf0atagen2026-10-25 10:04 UTC141
        }
da8896938cf0atagen2026-10-25 10:04 UTC142
    }
da8896938cf0atagen2026-10-25 10:04 UTC143
}
89bc9f8d246datagen2026-10-24 13:40 UTC144
89bc9f8d246datagen2026-10-24 13:40 UTC145
pub(crate) async fn execute(
da8896938cf0atagen2026-10-25 10:04 UTC146
    client: Client,
cbb715e11963atagen2026-10-24 23:24 UTC147
    media: Arc<MediaService>,
da8896938cf0atagen2026-10-25 10:04 UTC148
    sessions: Arc<Sessions>,
89bc9f8d246datagen2026-10-24 13:40 UTC149
    command: EventContextCommand,
89bc9f8d246datagen2026-10-24 13:40 UTC150
) -> Result<crate::CommandOutput, Error> {
da8896938cf0atagen2026-10-25 10:04 UTC151
    let current_user_id = client.user_id().map(ToOwned::to_owned);
da8896938cf0atagen2026-10-25 10:04 UTC152
    let room = |room_id: &OwnedRoomId| client.get_room(room_id).ok_or_else(Error::room_unknown);
da8896938cf0atagen2026-10-25 10:04 UTC153
    let (context_id, token, limit, backwards) = match command {
89bc9f8d246datagen2026-10-24 13:40 UTC154
        EventContextCommand::Fetch {
da8896938cf0atagen2026-10-25 10:04 UTC155
            room_id,
89bc9f8d246datagen2026-10-24 13:40 UTC156
            event_id,
89bc9f8d246datagen2026-10-24 13:40 UTC157
            before_limit,
89bc9f8d246datagen2026-10-24 13:40 UTC158
            after_limit,
89bc9f8d246datagen2026-10-24 13:40 UTC159
        } => {
da8896938cf0atagen2026-10-25 10:04 UTC160
            let (context, registrations, _) = capture(
da8896938cf0atagen2026-10-25 10:04 UTC161
                room(&room_id)?,
da8896938cf0atagen2026-10-25 10:04 UTC162
                current_user_id,
da8896938cf0atagen2026-10-25 10:04 UTC163
                event_id,
da8896938cf0atagen2026-10-25 10:04 UTC164
                before_limit,
da8896938cf0atagen2026-10-25 10:04 UTC165
                after_limit,
da8896938cf0atagen2026-10-25 10:04 UTC166
            )
da8896938cf0atagen2026-10-25 10:04 UTC167
            .await?;
da8896938cf0atagen2026-10-25 10:04 UTC168
            let context_id = sessions.open(&media, room_id, registrations).await;
da8896938cf0atagen2026-10-25 10:04 UTC169
            return Ok(crate::CommandOutput::EventContext(EventContextSession {
da8896938cf0atagen2026-10-25 10:04 UTC170
                context_id,
da8896938cf0atagen2026-10-25 10:04 UTC171
                context,
da8896938cf0atagen2026-10-25 10:04 UTC172
            }));
da8896938cf0atagen2026-10-25 10:04 UTC173
        }
da8896938cf0atagen2026-10-25 10:04 UTC174
        EventContextCommand::Close { context_id } => {
da8896938cf0atagen2026-10-25 10:04 UTC175
            sessions.close(&media, &context_id).await?;
da8896938cf0atagen2026-10-25 10:04 UTC176
            return Ok(crate::CommandOutput::Done);
89bc9f8d246datagen2026-10-24 13:40 UTC177
        }
da8896938cf0atagen2026-10-25 10:04 UTC178
        EventContextCommand::Before {
da8896938cf0atagen2026-10-25 10:04 UTC179
            context_id,
da8896938cf0atagen2026-10-25 10:04 UTC180
            token,
da8896938cf0atagen2026-10-25 10:04 UTC181
            limit,
da8896938cf0atagen2026-10-25 10:04 UTC182
        } => (context_id, token, limit, true),
da8896938cf0atagen2026-10-25 10:04 UTC183
        EventContextCommand::After {
da8896938cf0atagen2026-10-25 10:04 UTC184
            context_id,
da8896938cf0atagen2026-10-25 10:04 UTC185
            token,
da8896938cf0atagen2026-10-25 10:04 UTC186
            limit,
da8896938cf0atagen2026-10-25 10:04 UTC187
        } => (context_id, token, limit, false),
89bc9f8d246datagen2026-10-24 13:40 UTC188
    };
cb7b3362e1ceatagen2026-10-25 09:08 UTC189
    if limit == 0 {
cb7b3362e1ceatagen2026-10-25 09:08 UTC190
        return Err(Error::new(
cb7b3362e1ceatagen2026-10-25 09:08 UTC191
            ErrorKind::Invalid,
cb7b3362e1ceatagen2026-10-25 09:08 UTC192
            "context page limit must be at least 1",
cb7b3362e1ceatagen2026-10-25 09:08 UTC193
        ));
cb7b3362e1ceatagen2026-10-25 09:08 UTC194
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC195
    validate_limit(limit)?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC196
    if token.is_empty() {
cb7b3362e1ceatagen2026-10-25 09:08 UTC197
        return Err(Error::new(
cb7b3362e1ceatagen2026-10-25 09:08 UTC198
            ErrorKind::Invalid,
cb7b3362e1ceatagen2026-10-25 09:08 UTC199
            "context pagination token is empty",
cb7b3362e1ceatagen2026-10-25 09:08 UTC200
        ));
cb7b3362e1ceatagen2026-10-25 09:08 UTC201
    }
da8896938cf0atagen2026-10-25 10:04 UTC202
    let room = room(&sessions.room(&context_id).await?)?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC203
    let side = fetch_side(&room, Some(&token), limit, backwards).await?;
913832a327a8atagen2026-10-25 09:33 UTC204
    let mut page = Assembly::new(
913832a327a8atagen2026-10-25 09:33 UTC205
        &room,
913832a327a8atagen2026-10-25 09:33 UTC206
        current_user_id.as_deref(),
913832a327a8atagen2026-10-25 09:33 UTC207
        &side.values,
913832a327a8atagen2026-10-25 09:33 UTC208
        &side.highlights,
913832a327a8atagen2026-10-25 09:33 UTC209
        None,
913832a327a8atagen2026-10-25 09:33 UTC210
    )
913832a327a8atagen2026-10-25 09:33 UTC211
    .await;
cb7b3362e1ceatagen2026-10-25 09:08 UTC212
    let events = page.take(0..side.values.len());
da8896938cf0atagen2026-10-25 10:04 UTC213
    sessions.register(&media, &context_id, page.media).await?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC214
    Ok(crate::CommandOutput::EventContextPage(EventContextPage {
cb7b3362e1ceatagen2026-10-25 09:08 UTC215
        events,
cb7b3362e1ceatagen2026-10-25 09:08 UTC216
        next_token: side.token,
cb7b3362e1ceatagen2026-10-25 09:08 UTC217
    }))
cbb715e11963atagen2026-10-24 23:24 UTC218
}
89bc9f8d246datagen2026-10-24 13:40 UTC219
cbb715e11963atagen2026-10-24 23:24 UTC220
pub(crate) async fn capture(
cbb715e11963atagen2026-10-24 23:24 UTC221
    room: Room,
cbb715e11963atagen2026-10-24 23:24 UTC222
    current_user_id: Option<OwnedUserId>,
cbb715e11963atagen2026-10-24 23:24 UTC223
    event_id: OwnedEventId,
cbb715e11963atagen2026-10-24 23:24 UTC224
    before_limit: u16,
cbb715e11963atagen2026-10-24 23:24 UTC225
    after_limit: u16,
cb7b3362e1ceatagen2026-10-25 09:08 UTC226
) -> Result<(EventContext, Vec<MediaRegistration>, Vec<Value>), Error> {
cbb715e11963atagen2026-10-24 23:24 UTC227
    validate_limit(before_limit)?;
cbb715e11963atagen2026-10-24 23:24 UTC228
    validate_limit(after_limit)?;
cbb715e11963atagen2026-10-24 23:24 UTC229
    let raw = room
cbb715e11963atagen2026-10-24 23:24 UTC230
        .event_with_context(&event_id, false, UInt::from(0_u16), None)
89bc9f8d246datagen2026-10-24 13:40 UTC231
        .await
89bc9f8d246datagen2026-10-24 13:40 UTC232
        .map_err(|source| {
cbb715e11963atagen2026-10-24 23:24 UTC233
            let error = Error::from_sdk("fetching event context failed", source);
cbb715e11963atagen2026-10-24 23:24 UTC234
            if error
cbb715e11963atagen2026-10-24 23:24 UTC235
                .server
cbb715e11963atagen2026-10-24 23:24 UTC236
                .as_ref()
cbb715e11963atagen2026-10-24 23:24 UTC237
                .is_some_and(|server| server.status == 404)
cbb715e11963atagen2026-10-24 23:24 UTC238
            {
cbb715e11963atagen2026-10-24 23:24 UTC239
                Error::new(ErrorKind::EventNotFound, "event was not found")
cbb715e11963atagen2026-10-24 23:24 UTC240
            } else {
cbb715e11963atagen2026-10-24 23:24 UTC241
                error
cbb715e11963atagen2026-10-24 23:24 UTC242
            }
89bc9f8d246datagen2026-10-24 13:40 UTC243
        })?;
913832a327a8atagen2026-10-25 09:33 UTC244
    let (target, target_highlight) = raw
cbb715e11963atagen2026-10-24 23:24 UTC245
        .event
cbb715e11963atagen2026-10-24 23:24 UTC246
        .as_ref()
913832a327a8atagen2026-10-25 09:33 UTC247
        .and_then(raw_event)
cb7b3362e1ceatagen2026-10-25 09:08 UTC248
        .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "event was not found"))?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC249
    let (before, after) = try_join(
cb7b3362e1ceatagen2026-10-25 09:08 UTC250
        fetch_side(&room, raw.prev_batch_token.as_deref(), before_limit, true),
cb7b3362e1ceatagen2026-10-25 09:08 UTC251
        fetch_side(&room, raw.next_batch_token.as_deref(), after_limit, false),
cb7b3362e1ceatagen2026-10-25 09:08 UTC252
    )
cb7b3362e1ceatagen2026-10-25 09:08 UTC253
    .await?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC254
    let target_index = before.values.len();
cb7b3362e1ceatagen2026-10-25 09:08 UTC255
    let mut values = before.values;
cb7b3362e1ceatagen2026-10-25 09:08 UTC256
    values.push(target);
cb7b3362e1ceatagen2026-10-25 09:08 UTC257
    values.extend(after.values);
913832a327a8atagen2026-10-25 09:33 UTC258
    let mut highlights = before.highlights;
913832a327a8atagen2026-10-25 09:33 UTC259
    highlights.push(target_highlight);
913832a327a8atagen2026-10-25 09:33 UTC260
    highlights.extend(after.highlights);
cb7b3362e1ceatagen2026-10-25 09:08 UTC261
    let mut page = Assembly::new(
cb7b3362e1ceatagen2026-10-25 09:08 UTC262
        &room,
cb7b3362e1ceatagen2026-10-25 09:08 UTC263
        current_user_id.as_deref(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC264
        &values,
913832a327a8atagen2026-10-25 09:33 UTC265
        &highlights,
cb7b3362e1ceatagen2026-10-25 09:08 UTC266
        Some(target_index),
cb7b3362e1ceatagen2026-10-25 09:08 UTC267
    )
cb7b3362e1ceatagen2026-10-25 09:08 UTC268
    .await;
cb7b3362e1ceatagen2026-10-25 09:08 UTC269
    let events_before = page.take(0..target_index);
cb7b3362e1ceatagen2026-10-25 09:08 UTC270
    let event = page
cb7b3362e1ceatagen2026-10-25 09:08 UTC271
        .take(target_index..target_index + 1)
cb7b3362e1ceatagen2026-10-25 09:08 UTC272
        .pop()
cb7b3362e1ceatagen2026-10-25 09:08 UTC273
        .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "event could not be read"))?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC274
    let events_after = page.take(target_index + 1..values.len());
cbb715e11963atagen2026-10-24 23:24 UTC275
    Ok((
cbb715e11963atagen2026-10-24 23:24 UTC276
        EventContext {
cbb715e11963atagen2026-10-24 23:24 UTC277
            events_before,
cbb715e11963atagen2026-10-24 23:24 UTC278
            event,
cbb715e11963atagen2026-10-24 23:24 UTC279
            events_after,
cb7b3362e1ceatagen2026-10-25 09:08 UTC280
            start_token: before.token,
cb7b3362e1ceatagen2026-10-25 09:08 UTC281
            end_token: after.token,
cbb715e11963atagen2026-10-24 23:24 UTC282
        },
cb7b3362e1ceatagen2026-10-25 09:08 UTC283
        page.media,
cb7b3362e1ceatagen2026-10-25 09:08 UTC284
        values,
cbb715e11963atagen2026-10-24 23:24 UTC285
    ))
cbb715e11963atagen2026-10-24 23:24 UTC286
}
89bc9f8d246datagen2026-10-24 13:40 UTC287
cb7b3362e1ceatagen2026-10-25 09:08 UTC288
struct Side {
cb7b3362e1ceatagen2026-10-25 09:08 UTC289
    /// in timeline order
cb7b3362e1ceatagen2026-10-25 09:08 UTC290
    values: Vec<Value>,
913832a327a8atagen2026-10-25 09:33 UTC291
    /// beside `values`, from the push actions the sdk worked out as it fetched
913832a327a8atagen2026-10-25 09:33 UTC292
    highlights: Vec<bool>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC293
    token: Option<String>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC294
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC295
913832a327a8atagen2026-10-25 09:33 UTC296
fn raw_event(event: &SdkTimelineEvent) -> Option<(Value, bool)> {
913832a327a8atagen2026-10-25 09:33 UTC297
    let value = serde_json::from_str(event.raw().json().get()).ok()?;
913832a327a8atagen2026-10-25 09:33 UTC298
    let highlight = event
913832a327a8atagen2026-10-25 09:33 UTC299
        .push_actions()
913832a327a8atagen2026-10-25 09:33 UTC300
        .is_some_and(|actions| actions.iter().any(Action::is_highlight));
913832a327a8atagen2026-10-25 09:33 UTC301
    Some((value, highlight))
913832a327a8atagen2026-10-25 09:33 UTC302
}
913832a327a8atagen2026-10-25 09:33 UTC303
cbb715e11963atagen2026-10-24 23:24 UTC304
async fn fetch_side(
cbb715e11963atagen2026-10-24 23:24 UTC305
    room: &Room,
cbb715e11963atagen2026-10-24 23:24 UTC306
    token: Option<&str>,
cbb715e11963atagen2026-10-24 23:24 UTC307
    limit: u16,
cbb715e11963atagen2026-10-24 23:24 UTC308
    backwards: bool,
cb7b3362e1ceatagen2026-10-25 09:08 UTC309
) -> Result<Side, Error> {
cbb715e11963atagen2026-10-24 23:24 UTC310
    let Some(token) = token else {
cb7b3362e1ceatagen2026-10-25 09:08 UTC311
        return Ok(Side {
cb7b3362e1ceatagen2026-10-25 09:08 UTC312
            values: Vec::new(),
913832a327a8atagen2026-10-25 09:33 UTC313
            highlights: Vec::new(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC314
            token: None,
cb7b3362e1ceatagen2026-10-25 09:08 UTC315
        });
cbb715e11963atagen2026-10-24 23:24 UTC316
    };
cb7b3362e1ceatagen2026-10-25 09:08 UTC317
    if limit == 0 {
cb7b3362e1ceatagen2026-10-25 09:08 UTC318
        return Ok(Side {
cb7b3362e1ceatagen2026-10-25 09:08 UTC319
            values: Vec::new(),
913832a327a8atagen2026-10-25 09:33 UTC320
            highlights: Vec::new(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC321
            token: Some(token.to_owned()),
cb7b3362e1ceatagen2026-10-25 09:08 UTC322
        });
cb7b3362e1ceatagen2026-10-25 09:08 UTC323
    }
cbb715e11963atagen2026-10-24 23:24 UTC324
    let mut options = if backwards {
cbb715e11963atagen2026-10-24 23:24 UTC325
        MessagesOptions::backward()
cbb715e11963atagen2026-10-24 23:24 UTC326
    } else {
cbb715e11963atagen2026-10-24 23:24 UTC327
        MessagesOptions::forward()
cbb715e11963atagen2026-10-24 23:24 UTC328
    };
cbb715e11963atagen2026-10-24 23:24 UTC329
    options = options.from(token);
cbb715e11963atagen2026-10-24 23:24 UTC330
    options.limit = UInt::from(limit);
cbb715e11963atagen2026-10-24 23:24 UTC331
    let messages = room
cbb715e11963atagen2026-10-24 23:24 UTC332
        .messages(options)
cbb715e11963atagen2026-10-24 23:24 UTC333
        .await
cbb715e11963atagen2026-10-24 23:24 UTC334
        .map_err(|source| Error::from_sdk("fetching event context events failed", source))?;
913832a327a8atagen2026-10-25 09:33 UTC335
    let mut events: Vec<_> = messages.chunk.iter().filter_map(raw_event).collect();
89bc9f8d246datagen2026-10-24 13:40 UTC336
    if backwards {
913832a327a8atagen2026-10-25 09:33 UTC337
        events.reverse();
89bc9f8d246datagen2026-10-24 13:40 UTC338
    }
913832a327a8atagen2026-10-25 09:33 UTC339
    let (values, highlights) = events.into_iter().unzip();
cb7b3362e1ceatagen2026-10-25 09:08 UTC340
    Ok(Side {
cb7b3362e1ceatagen2026-10-25 09:08 UTC341
        values,
913832a327a8atagen2026-10-25 09:33 UTC342
        highlights,
cb7b3362e1ceatagen2026-10-25 09:08 UTC343
        token: messages.end,
cb7b3362e1ceatagen2026-10-25 09:08 UTC344
    })
89bc9f8d246datagen2026-10-24 13:40 UTC345
}
89bc9f8d246datagen2026-10-24 13:40 UTC346
cb7b3362e1ceatagen2026-10-25 09:08 UTC347
/// a window of raw events normalised together, so relations inside it land on their targets
cb7b3362e1ceatagen2026-10-25 09:08 UTC348
struct Assembly {
cb7b3362e1ceatagen2026-10-25 09:08 UTC349
    items: Vec<(usize, TimelineItem)>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC350
    media: Vec<MediaRegistration>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC351
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC352
cb7b3362e1ceatagen2026-10-25 09:08 UTC353
impl Assembly {
cb7b3362e1ceatagen2026-10-25 09:08 UTC354
    async fn new(
cb7b3362e1ceatagen2026-10-25 09:08 UTC355
        room: &Room,
cb7b3362e1ceatagen2026-10-25 09:08 UTC356
        current_user_id: Option<&ruma::UserId>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC357
        values: &[Value],
913832a327a8atagen2026-10-25 09:33 UTC358
        highlights: &[bool],
cb7b3362e1ceatagen2026-10-25 09:08 UTC359
        keep: Option<usize>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC360
    ) -> Self {
cb7b3362e1ceatagen2026-10-25 09:08 UTC361
        let page = normalise_context(values, keep, current_user_id, room.room_id());
cb7b3362e1ceatagen2026-10-25 09:08 UTC362
        let mut items = page.items;
913832a327a8atagen2026-10-25 09:33 UTC363
        for (index, item) in &mut items {
913832a327a8atagen2026-10-25 09:33 UTC364
            if let TimelineItem::Event(event) = item {
913832a327a8atagen2026-10-25 09:33 UTC365
                event.highlight = highlights[*index];
913832a327a8atagen2026-10-25 09:33 UTC366
            }
913832a327a8atagen2026-10-25 09:33 UTC367
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC368
        let mut media = page.media;
cb7b3362e1ceatagen2026-10-25 09:08 UTC369
        let mut targets: HashMap<OwnedEventId, TimelineEvent> = HashMap::new();
cb7b3362e1ceatagen2026-10-25 09:08 UTC370
        let mut missing = Vec::new();
cb7b3362e1ceatagen2026-10-25 09:08 UTC371
        for (_, item) in &items {
cb7b3362e1ceatagen2026-10-25 09:08 UTC372
            if let TimelineItem::Event(event) = item
cb7b3362e1ceatagen2026-10-25 09:08 UTC373
                && let Some(event_id) = &event.event_id
cb7b3362e1ceatagen2026-10-25 09:08 UTC374
            {
cb7b3362e1ceatagen2026-10-25 09:08 UTC375
                targets.insert(event_id.clone(), (**event).clone());
cb7b3362e1ceatagen2026-10-25 09:08 UTC376
            }
cb7b3362e1ceatagen2026-10-25 09:08 UTC377
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC378
        for event in events(&mut items) {
cb7b3362e1ceatagen2026-10-25 09:08 UTC379
            if let Some(reply) = &event.reply_to
cb7b3362e1ceatagen2026-10-25 09:08 UTC380
                && !targets.contains_key(&reply.event_id)
cb7b3362e1ceatagen2026-10-25 09:08 UTC381
                && !missing.contains(&reply.event_id)
cb7b3362e1ceatagen2026-10-25 09:08 UTC382
            {
cb7b3362e1ceatagen2026-10-25 09:08 UTC383
                missing.push(reply.event_id.clone());
cb7b3362e1ceatagen2026-10-25 09:08 UTC384
            }
cb7b3362e1ceatagen2026-10-25 09:08 UTC385
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC386
        let fetched = join_all(
cb7b3362e1ceatagen2026-10-25 09:08 UTC387
            missing
cb7b3362e1ceatagen2026-10-25 09:08 UTC388
                .into_iter()
cb7b3362e1ceatagen2026-10-25 09:08 UTC389
                .map(|event_id| fetch_reply(room, current_user_id, event_id)),
cb7b3362e1ceatagen2026-10-25 09:08 UTC390
        )
cb7b3362e1ceatagen2026-10-25 09:08 UTC391
        .await;
cb7b3362e1ceatagen2026-10-25 09:08 UTC392
        for (event, registrations) in fetched.into_iter().flatten() {
cbb715e11963atagen2026-10-24 23:24 UTC393
            media.extend(registrations);
cb7b3362e1ceatagen2026-10-25 09:08 UTC394
            if let Some(event_id) = event.event_id.clone() {
cb7b3362e1ceatagen2026-10-25 09:08 UTC395
                targets.insert(event_id, event);
cb7b3362e1ceatagen2026-10-25 09:08 UTC396
            }
cbb715e11963atagen2026-10-24 23:24 UTC397
        }
cb7b3362e1ceatagen2026-10-25 09:08 UTC398
        let profiles = profiles(
cb7b3362e1ceatagen2026-10-25 09:08 UTC399
            room,
cb7b3362e1ceatagen2026-10-25 09:08 UTC400
            items
cb7b3362e1ceatagen2026-10-25 09:08 UTC401
                .iter()
cb7b3362e1ceatagen2026-10-25 09:08 UTC402
                .filter_map(|(_, item)| match item {
cb7b3362e1ceatagen2026-10-25 09:08 UTC403
                    TimelineItem::Event(event) => Some(&event.sender.user_id),
cb7b3362e1ceatagen2026-10-25 09:08 UTC404
                    _ => None,
cb7b3362e1ceatagen2026-10-25 09:08 UTC405
                })
cb7b3362e1ceatagen2026-10-25 09:08 UTC406
                .chain(targets.values().map(|event| &event.sender.user_id)),
cb7b3362e1ceatagen2026-10-25 09:08 UTC407
        )
cb7b3362e1ceatagen2026-10-25 09:08 UTC408
        .await;
cb7b3362e1ceatagen2026-10-25 09:08 UTC409
        apply_profiles(targets.values_mut(), &profiles);
cb7b3362e1ceatagen2026-10-25 09:08 UTC410
        apply_profiles(events(&mut items), &profiles);
cb7b3362e1ceatagen2026-10-25 09:08 UTC411
        resolve_replies(events(&mut items), &targets);
cb7b3362e1ceatagen2026-10-25 09:08 UTC412
        Self { items, media }
89bc9f8d246datagen2026-10-24 13:40 UTC413
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC414
cb7b3362e1ceatagen2026-10-25 09:08 UTC415
    /// the items that came from raw events in `range`
cb7b3362e1ceatagen2026-10-25 09:08 UTC416
    fn take(&mut self, range: std::ops::Range<usize>) -> Vec<TimelineItem> {
cb7b3362e1ceatagen2026-10-25 09:08 UTC417
        let (taken, kept) = std::mem::take(&mut self.items)
cb7b3362e1ceatagen2026-10-25 09:08 UTC418
            .into_iter()
cb7b3362e1ceatagen2026-10-25 09:08 UTC419
            .partition(|(index, _)| range.contains(index));
cb7b3362e1ceatagen2026-10-25 09:08 UTC420
        self.items = kept;
cb7b3362e1ceatagen2026-10-25 09:08 UTC421
        taken.into_iter().map(|(_, item)| item).collect()
cb7b3362e1ceatagen2026-10-25 09:08 UTC422
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC423
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC424
cb7b3362e1ceatagen2026-10-25 09:08 UTC425
fn events(items: &mut [(usize, TimelineItem)]) -> impl Iterator<Item = &mut TimelineEvent> {
cb7b3362e1ceatagen2026-10-25 09:08 UTC426
    items.iter_mut().filter_map(|(_, item)| match item {
cb7b3362e1ceatagen2026-10-25 09:08 UTC427
        TimelineItem::Event(event) => Some(&mut **event),
cb7b3362e1ceatagen2026-10-25 09:08 UTC428
        _ => None,
cb7b3362e1ceatagen2026-10-25 09:08 UTC429
    })
89bc9f8d246datagen2026-10-24 13:40 UTC430
}
89bc9f8d246datagen2026-10-24 13:40 UTC431
cb7b3362e1ceatagen2026-10-25 09:08 UTC432
async fn fetch_reply(
cb7b3362e1ceatagen2026-10-25 09:08 UTC433
    room: &Room,
89bc9f8d246datagen2026-10-24 13:40 UTC434
    current_user_id: Option<&ruma::UserId>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC435
    event_id: OwnedEventId,
cb7b3362e1ceatagen2026-10-25 09:08 UTC436
) -> Option<(TimelineEvent, Vec<MediaRegistration>)> {
cb7b3362e1ceatagen2026-10-25 09:08 UTC437
    let event = room
cb7b3362e1ceatagen2026-10-25 09:08 UTC438
        .load_or_fetch_event(&event_id, None)
cb7b3362e1ceatagen2026-10-25 09:08 UTC439
        .await
cb7b3362e1ceatagen2026-10-25 09:08 UTC440
        .inspect_err(|error| tracing::debug!(%event_id, %error, "fetching a reply target failed"))
cb7b3362e1ceatagen2026-10-25 09:08 UTC441
        .ok()?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC442
    let value: Value = serde_json::from_str(event.raw().json().get()).ok()?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC443
    let identity = format!("{}\n{event_id}", room.room_id());
cb7b3362e1ceatagen2026-10-25 09:08 UTC444
    match normalise_observed(&value, current_user_id, &identity)? {
cb7b3362e1ceatagen2026-10-25 09:08 UTC445
        (TimelineItem::Event(event), media) => Some((*event, media)),
cb7b3362e1ceatagen2026-10-25 09:08 UTC446
        _ => None,
cb7b3362e1ceatagen2026-10-25 09:08 UTC447
    }
cb7b3362e1ceatagen2026-10-25 09:08 UTC448
}
cb7b3362e1ceatagen2026-10-25 09:08 UTC449
cb7b3362e1ceatagen2026-10-25 09:08 UTC450
/// under lazy-loaded members a sender may be unknown locally, and `get_member` fetches the
cb7b3362e1ceatagen2026-10-25 09:08 UTC451
/// member list once for the room when it is
cb7b3362e1ceatagen2026-10-25 09:08 UTC452
async fn profiles<'a>(
cb7b3362e1ceatagen2026-10-25 09:08 UTC453
    room: &Room,
cb7b3362e1ceatagen2026-10-25 09:08 UTC454
    user_ids: impl Iterator<Item = &'a OwnedUserId>,
cb7b3362e1ceatagen2026-10-25 09:08 UTC455
) -> HashMap<OwnedUserId, Sender> {
cb7b3362e1ceatagen2026-10-25 09:08 UTC456
    let mut unique: Vec<&OwnedUserId> = user_ids.collect();
cb7b3362e1ceatagen2026-10-25 09:08 UTC457
    unique.sort();
cb7b3362e1ceatagen2026-10-25 09:08 UTC458
    unique.dedup();
cb7b3362e1ceatagen2026-10-25 09:08 UTC459
    join_all(unique.into_iter().map(|user_id| async move {
cb7b3362e1ceatagen2026-10-25 09:08 UTC460
        let member = room.get_member(user_id).await.ok().flatten()?;
cb7b3362e1ceatagen2026-10-25 09:08 UTC461
        Some((
cb7b3362e1ceatagen2026-10-25 09:08 UTC462
            user_id.clone(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC463
            Sender {
cb7b3362e1ceatagen2026-10-25 09:08 UTC464
                user_id: user_id.clone(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC465
                display_name: member.display_name().map(str::to_owned),
cb7b3362e1ceatagen2026-10-25 09:08 UTC466
                avatar: member.avatar_url().map(MediaId::plain),
cb7b3362e1ceatagen2026-10-25 09:08 UTC467
                name_ambiguous: member.name_ambiguous(),
cb7b3362e1ceatagen2026-10-25 09:08 UTC468
            },
cb7b3362e1ceatagen2026-10-25 09:08 UTC469
        ))
cb7b3362e1ceatagen2026-10-25 09:08 UTC470
    }))
cb7b3362e1ceatagen2026-10-25 09:08 UTC471
    .await
cb7b3362e1ceatagen2026-10-25 09:08 UTC472
    .into_iter()
cb7b3362e1ceatagen2026-10-25 09:08 UTC473
    .flatten()
cb7b3362e1ceatagen2026-10-25 09:08 UTC474
    .collect()
89bc9f8d246datagen2026-10-24 13:40 UTC475
}
89bc9f8d246datagen2026-10-24 13:40 UTC476
cbb715e11963atagen2026-10-24 23:24 UTC477
fn validate_limit(limit: u16) -> Result<(), Error> {
cbb715e11963atagen2026-10-24 23:24 UTC478
    if limit > MAX_CONTEXT_LIMIT {
cbb715e11963atagen2026-10-24 23:24 UTC479
        Err(Error::new(ErrorKind::Invalid, "context limit exceeds 50"))
cbb715e11963atagen2026-10-24 23:24 UTC480
    } else {
cbb715e11963atagen2026-10-24 23:24 UTC481
        Ok(())
cbb715e11963atagen2026-10-24 23:24 UTC482
    }
89bc9f8d246datagen2026-10-24 13:40 UTC483
}