atagen

macltok

public
Star0
Watch0
5572a11e8823a38565ef9d03d2033208a39e95a3
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/context.rsRust224 lines6.8 KB
1
use std::sync::Arc;
2
3
use matrix_sdk::{room::MessagesOptions, Room};
4
use matrix_sdk_ui::timeline::{RoomExt, TimelineEventFocusThreadMode, TimelineFocus};
5
use ruma::{uint, OwnedEventId};
6
7
use crate::{
8
    normalise::{normalise_timeline, MediaScope},
9
    Error, ErrorKind, EventContext, EventContextCommand, EventContextDirection, EventContextPage,
10
    TimelineItem,
11
};
12
13
const MAX_CONTEXT_LIMIT: u16 = 50;
14
15
pub(crate) async fn execute(
16
    room: Room,
17
    current_user_id: Option<ruma::OwnedUserId>,
18
    command: EventContextCommand,
19
) -> Result<crate::CommandOutput, Error> {
20
    if let EventContextCommand::Before { token, limit, .. }
21
    | EventContextCommand::After { token, limit, .. } = &command
22
    {
23
        validate_limit(*limit)?;
24
        if token.is_empty() {
25
            return Err(Error::new(
26
                ErrorKind::Invalid,
27
                "context pagination token is empty",
28
            ));
29
        }
30
        return execute_page(
31
            room,
32
            current_user_id,
33
            token,
34
            *limit,
35
            matches!(command, EventContextCommand::Before { .. }),
36
        )
37
        .await;
38
    }
39
40
    let (event_id, limit) = match &command {
41
        EventContextCommand::Fetch {
42
            event_id,
43
            before_limit,
44
            after_limit,
45
            ..
46
        } => {
47
            validate_limit(*before_limit)?;
48
            validate_limit(*after_limit)?;
49
            (event_id.clone(), (*before_limit).max(*after_limit))
50
        }
51
        EventContextCommand::Before { .. } | EventContextCommand::After { .. } => unreachable!(),
52
    };
53
    validate_limit(limit)?;
54
55
    let raw_context = if matches!(command, EventContextCommand::Fetch { .. }) {
56
        Some(
57
            room.event_with_context(&event_id, false, uint!(0), None)
58
                .await
59
                .map_err(|source| {
60
                    let error = Error::from_sdk("fetching event context failed", source);
61
                    if error
62
                        .server
63
                        .as_ref()
64
                        .is_some_and(|server| server.status == 404)
65
                    {
66
                        Error::new(ErrorKind::EventNotFound, "event was not found")
67
                    } else {
68
                        error
69
                    }
70
                })?,
71
        )
72
    } else {
73
        None
74
    };
75
76
    let focus = TimelineFocus::Event {
77
        target: event_id.clone(),
78
        num_context_events: limit,
79
        thread_mode: TimelineEventFocusThreadMode::Automatic {
80
            hide_threaded_events: true,
81
        },
82
    };
83
    let timeline = room
84
        .timeline_builder()
85
        .with_focus(focus)
86
        .build()
87
        .await
88
        .map_err(|source| {
89
            Error::with_source(ErrorKind::Timeline, "building event context failed", source)
90
        })?;
91
92
    if let EventContextCommand::Fetch { .. } = command {
93
        let raw = raw_context.expect("fetch context response");
94
        let normalized = normalize_items(timeline.items().await, current_user_id.as_deref());
95
        let (before, event, after) = split_items(normalized, &event_id);
96
        if event.is_none() && raw.event.is_none() {
97
            return Err(Error::new(ErrorKind::EventNotFound, "event was not found"));
98
        }
99
        return Ok(crate::CommandOutput::EventContext(EventContext {
100
            events_before: before,
101
            event,
102
            events_after: after,
103
            start_token: raw.prev_batch_token,
104
            end_token: raw.next_batch_token,
105
        }));
106
    }
107
108
    unreachable!("fetch context returned above")
109
}
110
111
async fn execute_page(
112
    room: Room,
113
    current_user_id: Option<ruma::OwnedUserId>,
114
    token: &str,
115
    limit: u16,
116
    backwards: bool,
117
) -> Result<crate::CommandOutput, Error> {
118
    let mut options = if backwards {
119
        MessagesOptions::backward()
120
    } else {
121
        MessagesOptions::forward()
122
    };
123
    options = options.from(token);
124
    options.limit = ruma::UInt::from(limit);
125
    let messages = room
126
        .messages(options)
127
        .await
128
        .map_err(|source| Error::from_sdk("fetching event context page failed", source))?;
129
130
    let ids: Vec<_> = messages
131
        .chunk
132
        .iter()
133
        .filter_map(|event| event.event_id())
134
        .collect();
135
    let normalized = if let Some(target) = ids.first() {
136
        let focus = TimelineFocus::Event {
137
            target: target.clone(),
138
            num_context_events: limit,
139
            thread_mode: TimelineEventFocusThreadMode::Automatic {
140
                hide_threaded_events: false,
141
            },
142
        };
143
        let timeline = room
144
            .timeline_builder()
145
            .with_focus(focus)
146
            .build()
147
            .await
148
            .map_err(|source| {
149
                Error::with_source(
150
                    ErrorKind::Timeline,
151
                    "building event context page failed",
152
                    source,
153
                )
154
            })?;
155
        normalize_items(timeline.items().await, current_user_id.as_deref())
156
    } else {
157
        Vec::new()
158
    };
159
160
    let mut by_id = std::collections::HashMap::new();
161
    for item in normalized {
162
        if let TimelineItem::Event(event) = &item {
163
            if let Some(event_id) = &event.event_id {
164
                by_id.insert(event_id.clone(), item);
165
            }
166
        }
167
    }
168
    let mut events: Vec<_> = ids.iter().filter_map(|id| by_id.remove(id)).collect();
169
    if backwards {
170
        events.reverse();
171
    }
172
    let next_token = messages.end.clone();
173
    let end_reached = next_token.is_none();
174
    let direction = if backwards {
175
        EventContextDirection::Before
176
    } else {
177
        EventContextDirection::After
178
    };
179
    Ok(crate::CommandOutput::EventContextPage(EventContextPage {
180
        direction,
181
        events,
182
        next_token,
183
        end_reached,
184
    }))
185
}
186
187
fn validate_limit(limit: u16) -> Result<(), Error> {
188
    if limit > MAX_CONTEXT_LIMIT {
189
        Err(Error::new(ErrorKind::Invalid, "context limit exceeds 50"))
190
    } else {
191
        Ok(())
192
    }
193
}
194
195
fn normalize_items(
196
    items: imbl::Vector<Arc<matrix_sdk_ui::timeline::TimelineItem>>,
197
    current_user_id: Option<&ruma::UserId>,
198
) -> Vec<TimelineItem> {
199
    let scope = MediaScope::new();
200
    normalise_timeline(items.iter(), current_user_id, &scope)
201
        .items
202
        .into_iter()
203
        .collect()
204
}
205
206
fn split_items(
207
    items: Vec<TimelineItem>,
208
    event_id: &OwnedEventId,
209
) -> (Vec<TimelineItem>, Option<TimelineItem>, Vec<TimelineItem>) {
210
    let items: Vec<_> = items
211
        .into_iter()
212
        .filter(|item| matches!(item, TimelineItem::Event(_)))
213
        .collect();
214
    let target = items.iter().position(|item| {
215
        matches!(item, TimelineItem::Event(event) if event.event_id.as_ref() == Some(event_id))
216
    });
217
    let Some(index) = target else {
218
        return (items, None, Vec::new());
219
    };
220
    let mut before = items;
221
    let after = before.split_off(index + 1);
222
    let event = before.pop();
223
    (before, event, after)
224
}