atagen

macltok

public
Star0
Watch0
cb7b3362e1cede5ca9a31af3b60ab2f6127c690c
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/context.rsRust306 lines9.6 KB
1
use std::{collections::HashMap, sync::Arc};
2
3
use futures_util::future::{join_all, try_join};
4
use matrix_sdk::{Room, room::MessagesOptions};
5
use ruma::{OwnedEventId, OwnedUserId, UInt};
6
use serde_json::Value;
7
8
use crate::{
9
    Error, ErrorKind, EventContext, EventContextCommand, EventContextPage, MediaId, Sender,
10
    TimelineEvent, TimelineItem,
11
    engine::MediaService,
12
    normalise::{
13
        MediaRegistration, apply_profiles, normalise_context, normalise_observed, resolve_replies,
14
    },
15
};
16
17
const MAX_CONTEXT_LIMIT: u16 = 50;
18
19
pub(crate) async fn execute(
20
    room: Room,
21
    current_user_id: Option<OwnedUserId>,
22
    media: Arc<MediaService>,
23
    command: EventContextCommand,
24
) -> Result<crate::CommandOutput, Error> {
25
    let (token, limit, backwards) = match command {
26
        EventContextCommand::Fetch {
27
            event_id,
28
            before_limit,
29
            after_limit,
30
            ..
31
        } => {
32
            let (context, registrations, _) =
33
                capture(room, current_user_id, event_id, before_limit, after_limit).await?;
34
            media.register(registrations).await;
35
            return Ok(crate::CommandOutput::EventContext(context));
36
        }
37
        EventContextCommand::Before { token, limit, .. } => (token, limit, true),
38
        EventContextCommand::After { token, limit, .. } => (token, limit, false),
39
    };
40
    if limit == 0 {
41
        return Err(Error::new(
42
            ErrorKind::Invalid,
43
            "context page limit must be at least 1",
44
        ));
45
    }
46
    validate_limit(limit)?;
47
    if token.is_empty() {
48
        return Err(Error::new(
49
            ErrorKind::Invalid,
50
            "context pagination token is empty",
51
        ));
52
    }
53
    let side = fetch_side(&room, Some(&token), limit, backwards).await?;
54
    let mut page = Assembly::new(&room, current_user_id.as_deref(), &side.values, None).await;
55
    let events = page.take(0..side.values.len());
56
    media.register(page.media).await;
57
    Ok(crate::CommandOutput::EventContextPage(EventContextPage {
58
        events,
59
        next_token: side.token,
60
    }))
61
}
62
63
pub(crate) async fn capture(
64
    room: Room,
65
    current_user_id: Option<OwnedUserId>,
66
    event_id: OwnedEventId,
67
    before_limit: u16,
68
    after_limit: u16,
69
) -> Result<(EventContext, Vec<MediaRegistration>, Vec<Value>), Error> {
70
    validate_limit(before_limit)?;
71
    validate_limit(after_limit)?;
72
    let raw = room
73
        .event_with_context(&event_id, false, UInt::from(0_u16), None)
74
        .await
75
        .map_err(|source| {
76
            let error = Error::from_sdk("fetching event context failed", source);
77
            if error
78
                .server
79
                .as_ref()
80
                .is_some_and(|server| server.status == 404)
81
            {
82
                Error::new(ErrorKind::EventNotFound, "event was not found")
83
            } else {
84
                error
85
            }
86
        })?;
87
    let target = raw
88
        .event
89
        .as_ref()
90
        .and_then(|event| serde_json::from_str::<Value>(event.raw().json().get()).ok())
91
        .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "event was not found"))?;
92
    let (before, after) = try_join(
93
        fetch_side(&room, raw.prev_batch_token.as_deref(), before_limit, true),
94
        fetch_side(&room, raw.next_batch_token.as_deref(), after_limit, false),
95
    )
96
    .await?;
97
    let target_index = before.values.len();
98
    let mut values = before.values;
99
    values.push(target);
100
    values.extend(after.values);
101
    let mut page = Assembly::new(
102
        &room,
103
        current_user_id.as_deref(),
104
        &values,
105
        Some(target_index),
106
    )
107
    .await;
108
    let events_before = page.take(0..target_index);
109
    let event = page
110
        .take(target_index..target_index + 1)
111
        .pop()
112
        .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "event could not be read"))?;
113
    let events_after = page.take(target_index + 1..values.len());
114
    Ok((
115
        EventContext {
116
            events_before,
117
            event,
118
            events_after,
119
            start_token: before.token,
120
            end_token: after.token,
121
        },
122
        page.media,
123
        values,
124
    ))
125
}
126
127
struct Side {
128
    /// in timeline order
129
    values: Vec<Value>,
130
    token: Option<String>,
131
}
132
133
async fn fetch_side(
134
    room: &Room,
135
    token: Option<&str>,
136
    limit: u16,
137
    backwards: bool,
138
) -> Result<Side, Error> {
139
    let Some(token) = token else {
140
        return Ok(Side {
141
            values: Vec::new(),
142
            token: None,
143
        });
144
    };
145
    if limit == 0 {
146
        return Ok(Side {
147
            values: Vec::new(),
148
            token: Some(token.to_owned()),
149
        });
150
    }
151
    let mut options = if backwards {
152
        MessagesOptions::backward()
153
    } else {
154
        MessagesOptions::forward()
155
    };
156
    options = options.from(token);
157
    options.limit = UInt::from(limit);
158
    let messages = room
159
        .messages(options)
160
        .await
161
        .map_err(|source| Error::from_sdk("fetching event context events failed", source))?;
162
    let mut values: Vec<Value> = messages
163
        .chunk
164
        .iter()
165
        .filter_map(|event| serde_json::from_str(event.raw().json().get()).ok())
166
        .collect();
167
    if backwards {
168
        values.reverse();
169
    }
170
    Ok(Side {
171
        values,
172
        token: messages.end,
173
    })
174
}
175
176
/// a window of raw events normalised together, so relations inside it land on their targets
177
struct Assembly {
178
    items: Vec<(usize, TimelineItem)>,
179
    media: Vec<MediaRegistration>,
180
}
181
182
impl Assembly {
183
    async fn new(
184
        room: &Room,
185
        current_user_id: Option<&ruma::UserId>,
186
        values: &[Value],
187
        keep: Option<usize>,
188
    ) -> Self {
189
        let page = normalise_context(values, keep, current_user_id, room.room_id());
190
        let mut items = page.items;
191
        let mut media = page.media;
192
        let mut targets: HashMap<OwnedEventId, TimelineEvent> = HashMap::new();
193
        let mut missing = Vec::new();
194
        for (_, item) in &items {
195
            if let TimelineItem::Event(event) = item
196
                && let Some(event_id) = &event.event_id
197
            {
198
                targets.insert(event_id.clone(), (**event).clone());
199
            }
200
        }
201
        for event in events(&mut items) {
202
            if let Some(reply) = &event.reply_to
203
                && !targets.contains_key(&reply.event_id)
204
                && !missing.contains(&reply.event_id)
205
            {
206
                missing.push(reply.event_id.clone());
207
            }
208
        }
209
        let fetched = join_all(
210
            missing
211
                .into_iter()
212
                .map(|event_id| fetch_reply(room, current_user_id, event_id)),
213
        )
214
        .await;
215
        for (event, registrations) in fetched.into_iter().flatten() {
216
            media.extend(registrations);
217
            if let Some(event_id) = event.event_id.clone() {
218
                targets.insert(event_id, event);
219
            }
220
        }
221
        let profiles = profiles(
222
            room,
223
            items
224
                .iter()
225
                .filter_map(|(_, item)| match item {
226
                    TimelineItem::Event(event) => Some(&event.sender.user_id),
227
                    _ => None,
228
                })
229
                .chain(targets.values().map(|event| &event.sender.user_id)),
230
        )
231
        .await;
232
        apply_profiles(targets.values_mut(), &profiles);
233
        apply_profiles(events(&mut items), &profiles);
234
        resolve_replies(events(&mut items), &targets);
235
        Self { items, media }
236
    }
237
238
    /// the items that came from raw events in `range`
239
    fn take(&mut self, range: std::ops::Range<usize>) -> Vec<TimelineItem> {
240
        let (taken, kept) = std::mem::take(&mut self.items)
241
            .into_iter()
242
            .partition(|(index, _)| range.contains(index));
243
        self.items = kept;
244
        taken.into_iter().map(|(_, item)| item).collect()
245
    }
246
}
247
248
fn events(items: &mut [(usize, TimelineItem)]) -> impl Iterator<Item = &mut TimelineEvent> {
249
    items.iter_mut().filter_map(|(_, item)| match item {
250
        TimelineItem::Event(event) => Some(&mut **event),
251
        _ => None,
252
    })
253
}
254
255
async fn fetch_reply(
256
    room: &Room,
257
    current_user_id: Option<&ruma::UserId>,
258
    event_id: OwnedEventId,
259
) -> Option<(TimelineEvent, Vec<MediaRegistration>)> {
260
    let event = room
261
        .load_or_fetch_event(&event_id, None)
262
        .await
263
        .inspect_err(|error| tracing::debug!(%event_id, %error, "fetching a reply target failed"))
264
        .ok()?;
265
    let value: Value = serde_json::from_str(event.raw().json().get()).ok()?;
266
    let identity = format!("{}\n{event_id}", room.room_id());
267
    match normalise_observed(&value, current_user_id, &identity)? {
268
        (TimelineItem::Event(event), media) => Some((*event, media)),
269
        _ => None,
270
    }
271
}
272
273
/// under lazy-loaded members a sender may be unknown locally, and `get_member` fetches the
274
/// member list once for the room when it is
275
async fn profiles<'a>(
276
    room: &Room,
277
    user_ids: impl Iterator<Item = &'a OwnedUserId>,
278
) -> HashMap<OwnedUserId, Sender> {
279
    let mut unique: Vec<&OwnedUserId> = user_ids.collect();
280
    unique.sort();
281
    unique.dedup();
282
    join_all(unique.into_iter().map(|user_id| async move {
283
        let member = room.get_member(user_id).await.ok().flatten()?;
284
        Some((
285
            user_id.clone(),
286
            Sender {
287
                user_id: user_id.clone(),
288
                display_name: member.display_name().map(str::to_owned),
289
                avatar: member.avatar_url().map(MediaId::plain),
290
                name_ambiguous: member.name_ambiguous(),
291
            },
292
        ))
293
    }))
294
    .await
295
    .into_iter()
296
    .flatten()
297
    .collect()
298
}
299
300
fn validate_limit(limit: u16) -> Result<(), Error> {
301
    if limit > MAX_CONTEXT_LIMIT {
302
        Err(Error::new(ErrorKind::Invalid, "context limit exceeds 50"))
303
    } else {
304
        Ok(())
305
    }
306
}