atagen

macltok

public
Star0
Watch0
0bfa3f46f0be39529f9541bdfbff51255e54ffba
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/watchers.rsRust391 lines14.0 KB
1
use std::{collections::HashSet, sync::Arc, time::Duration};
2
3
use futures_util::StreamExt;
4
use matrix_sdk::{
5
    Client, Room, SessionChange,
6
    ruma::events::{AnyMessageLikeEventContent, room::message::MessageType},
7
    send_queue::{LocalEchoContent, RoomSendQueueUpdate, SendQueueUpdate},
8
};
9
use matrix_sdk_ui::sync_service::{State as SyncState, SyncService};
10
use ruma::{OwnedDeviceId, OwnedRoomId, OwnedTransactionId, time::Instant};
11
use tokio::sync::{broadcast::error::RecvError, mpsc, watch};
12
13
use super::executor::TaskSet;
14
use crate::{
15
    ConnectionState, Event, Termination, UploadOutcome, encryption::encryption_state,
16
    error::classify_sdk_error, transport::EventSender,
17
};
18
19
const SYNC_RESTART_MIN: Duration = Duration::from_secs(1);
20
const SYNC_RESTART_MAX: Duration = Duration::from_secs(60);
21
const SEND_QUEUE_RETRY: Duration = Duration::from_secs(5);
22
const SYNC_STABLE: Duration = Duration::from_secs(30);
23
24
pub(super) async fn room_subscriptions(
25
    sync: Arc<SyncService>,
26
    mut rooms: watch::Receiver<Vec<ruma::OwnedRoomId>>,
27
) {
28
    while rooms.changed().await.is_ok() {
29
        let room_ids = rooms.borrow_and_update().clone();
30
        let room_ids = room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>();
31
        sync.room_list_service().subscribe_to_rooms(&room_ids).await;
32
    }
33
}
34
35
pub(super) async fn session(
36
    mut changes: tokio::sync::broadcast::Receiver<SessionChange>,
37
    events: EventSender,
38
    device_id: OwnedDeviceId,
39
    stop: mpsc::UnboundedSender<Termination>,
40
) {
41
    loop {
42
        let event = match changes.recv().await {
43
            Ok(SessionChange::TokensRefreshed) => Event::SessionTokensChanged,
44
            Ok(SessionChange::UnknownToken(data)) => {
45
                let _ = events
46
                    .send(Event::SessionAccessRejected {
47
                        soft_logout: data.soft_logout,
48
                    })
49
                    .await;
50
                let _ = stop.send(if data.soft_logout {
51
                    Termination::SoftLoggedOut { device_id }
52
                } else {
53
                    Termination::AccessRevoked
54
                });
55
                return;
56
            }
57
            // a missed refresh costs one needless save, a missed rejection comes round again
58
            Err(RecvError::Lagged(_)) => Event::SessionTokensChanged,
59
            Err(RecvError::Closed) => return,
60
        };
61
        if events.send(event).await.is_err() {
62
            return;
63
        }
64
    }
65
}
66
67
/// `states` must be taken before sync starts, a late subscriber never hears the transition it missed
68
pub(super) async fn connection(
69
    mut states: impl futures_util::Stream<Item = SyncState> + Unpin,
70
    sync: Arc<SyncService>,
71
    client: Client,
72
    events: EventSender,
73
    connection: watch::Sender<ConnectionState>,
74
) {
75
    let mut backoff = SYNC_RESTART_MIN;
76
    let mut online_since: Option<Instant> = None;
77
    while let Some(state) = states.next().await {
78
        // only a run that lasted counts as recovered, or a server that answers /versions
79
        // but refuses sync would have us bounce between offline and online forever
80
        let stable = online_since
81
            .take()
82
            .map(|since| since.elapsed() >= SYNC_STABLE);
83
        if stable == Some(true) {
84
            backoff = SYNC_RESTART_MIN;
85
        }
86
        let (state, restart) = match state {
87
            // only ever us stopping it for a restart, nothing a client needs to hear
88
            SyncState::Idle => continue,
89
            SyncState::Running => {
90
                online_since = Some(Instant::now());
91
                // the sdk parks a room queue on any send error and never unparks it
92
                client.send_queue().set_enabled(true).await;
93
                (ConnectionState::Online, false)
94
            }
95
            SyncState::Offline => (ConnectionState::Offline, stable == Some(false)),
96
            SyncState::Terminated => (ConnectionState::Offline, true),
97
            SyncState::Error(error) => {
98
                tracing::warn!(%error, "sync service reported an error");
99
                (ConnectionState::Offline, true)
100
            }
101
        };
102
        if *connection.borrow() != state {
103
            connection.send_replace(state);
104
            if events.send(Event::ConnectionChanged(state)).await.is_err() {
105
                return;
106
            }
107
        }
108
        if restart {
109
            sync.stop().await;
110
            matrix_sdk::sleep::sleep(backoff).await;
111
            backoff = (backoff * 2).min(SYNC_RESTART_MAX);
112
            sync.start().await;
113
        }
114
    }
115
}
116
117
/// a recoverable failure while we're online is a hiccup, so give the room another go
118
pub(super) async fn send_queue_errors(
119
    client: Client,
120
    mut errors: tokio::sync::broadcast::Receiver<matrix_sdk::send_queue::SendQueueRoomError>,
121
    online: watch::Receiver<ConnectionState>,
122
) {
123
    let mut retries = TaskSet::new();
124
    loop {
125
        let error = tokio::select! {
126
            error = errors.recv() => error,
127
            Some(_) = retries.join_next(), if !retries.is_empty() => continue,
128
        };
129
        match error {
130
            Ok(error) if error.is_recoverable => {
131
                let client = client.clone();
132
                let online = online.clone();
133
                retries.spawn(async move {
134
                    matrix_sdk::sleep::sleep(SEND_QUEUE_RETRY).await;
135
                    if *online.borrow() == ConnectionState::Online
136
                        && let Some(room) = client.get_room(&error.room_id)
137
                    {
138
                        room.send_queue().set_enabled(true);
139
                    }
140
                });
141
            }
142
            Ok(_) | Err(RecvError::Lagged(_)) => {}
143
            Err(RecvError::Closed) => return,
144
        }
145
    }
146
}
147
148
/// upload progress only goes out on the room's own channel, never the client-wide one
149
pub(super) async fn room_uploads(room: Room, events: EventSender, ready: watch::Sender<bool>) {
150
    let room_id = room.room_id().to_owned();
151
    let mut updates = match room.send_queue().subscribe().await {
152
        Ok((_, updates)) => updates,
153
        Err(error) => {
154
            tracing::warn!(%room_id, %error, "reading the media send queue failed");
155
            return;
156
        }
157
    };
158
    ready.send_replace(true);
159
    loop {
160
        let update = match updates.recv().await {
161
            Ok(update) => update,
162
            // a missed tick is fine, the next one says where it's got to
163
            Err(RecvError::Lagged(_)) => continue,
164
            Err(RecvError::Closed) => return,
165
        };
166
        let RoomSendQueueUpdate::MediaUpload {
167
            related_to,
168
            file,
169
            progress,
170
            ..
171
        } = update
172
        else {
173
            continue;
174
        };
175
        let event = Event::MediaUploadProgress {
176
            room_id: room_id.clone(),
177
            transaction_id: related_to,
178
            uploaded_bytes: progress.current as u64,
179
            total_bytes: progress.total as u64,
180
            complete: file.is_some() && progress.current == progress.total,
181
        };
182
        if events.send(event).await.is_err() {
183
            return;
184
        }
185
    }
186
}
187
188
/// progress only goes out on each room's own channel, everything else reaches this
189
/// one, which exists before any upload does
190
pub(super) async fn uploads(
191
    client: Client,
192
    mut updates: tokio::sync::broadcast::Receiver<SendQueueUpdate>,
193
    events: EventSender,
194
) {
195
    let (_, mut rooms) = client.rooms_stream();
196
    let mut uploads = attachment_echoes(&client).await;
197
    loop {
198
        let finished = tokio::select! {
199
            update = updates.recv() => match update {
200
                Ok(update) => match upload_update(&mut uploads, update) {
201
                    Some(event) => vec![event],
202
                    None => continue,
203
                },
204
                Err(RecvError::Lagged(skipped)) => {
205
                    tracing::warn!(skipped, "upload watcher lagged");
206
                    let current = attachment_echoes(&client).await;
207
                    let missed = uploads.difference(&current).cloned().collect::<Vec<_>>();
208
                    uploads = current;
209
                    finished_uploads(missed, UploadOutcome::Missed)
210
                }
211
                Err(RecvError::Closed) => return,
212
            },
213
            update = rooms.next() => {
214
                if update.is_none() {
215
                    return;
216
                }
217
                let forgotten = uploads
218
                    .iter()
219
                    .filter(|(room_id, _)| client.get_room(room_id).is_none())
220
                    .cloned()
221
                    .collect::<Vec<_>>();
222
                for upload in &forgotten {
223
                    uploads.remove(upload);
224
                }
225
                finished_uploads(forgotten, UploadOutcome::RoomForgotten)
226
            }
227
        };
228
        for event in finished {
229
            if events.send(event).await.is_err() {
230
                return;
231
            }
232
        }
233
    }
234
}
235
236
type Upload = (OwnedRoomId, OwnedTransactionId);
237
238
async fn attachment_echoes(client: &Client) -> HashSet<Upload> {
239
    let Ok(echoes) = client.send_queue().local_echoes().await else {
240
        return HashSet::new();
241
    };
242
    echoes
243
        .into_iter()
244
        .flat_map(|(room_id, echoes)| {
245
            echoes
246
                .into_iter()
247
                .filter(|echo| is_attachment_echo(&echo.content))
248
                .map(move |echo| (room_id.clone(), echo.transaction_id))
249
        })
250
        .collect()
251
}
252
253
fn upload_update(uploads: &mut HashSet<Upload>, update: SendQueueUpdate) -> Option<Event> {
254
    let room_id = update.room_id;
255
    let (transaction_id, outcome) = match update.update {
256
        RoomSendQueueUpdate::NewLocalEvent(echo) => {
257
            if is_attachment_echo(&echo.content) {
258
                uploads.insert((room_id, echo.transaction_id));
259
            }
260
            return None;
261
        }
262
        RoomSendQueueUpdate::SendError {
263
            transaction_id,
264
            error,
265
            is_recoverable,
266
        } => {
267
            let upload = (room_id, transaction_id);
268
            if !uploads.contains(&upload) {
269
                return None;
270
            }
271
            let (room_id, transaction_id) = upload;
272
            return Some(Event::MediaUploadFailed {
273
                room_id,
274
                transaction_id,
275
                kind: classify_sdk_error(&error),
276
                will_retry: is_recoverable,
277
            });
278
        }
279
        RoomSendQueueUpdate::SentEvent {
280
            transaction_id,
281
            event_id,
282
        } => (transaction_id, UploadOutcome::Sent { event_id }),
283
        RoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => {
284
            (transaction_id, UploadOutcome::Cancelled)
285
        }
286
        _ => return None,
287
    };
288
    let upload = (room_id, transaction_id);
289
    if !uploads.remove(&upload) {
290
        return None;
291
    }
292
    let (room_id, transaction_id) = upload;
293
    Some(Event::MediaUploadFinished {
294
        room_id,
295
        transaction_id,
296
        outcome,
297
    })
298
}
299
300
fn finished_uploads(uploads: Vec<Upload>, outcome: UploadOutcome) -> Vec<Event> {
301
    uploads
302
        .into_iter()
303
        .map(|(room_id, transaction_id)| Event::MediaUploadFinished {
304
            room_id,
305
            transaction_id,
306
            outcome: outcome.clone(),
307
        })
308
        .collect()
309
}
310
311
fn is_attachment_echo(content: &LocalEchoContent) -> bool {
312
    let LocalEchoContent::Event {
313
        serialized_event, ..
314
    } = content
315
    else {
316
        return false;
317
    };
318
    matches!(
319
        serialized_event.deserialize(),
320
        Ok(AnyMessageLikeEventContent::RoomMessage(content))
321
            if matches!(
322
                content.msgtype,
323
                MessageType::Audio(_) | MessageType::File(_) | MessageType::Image(_) | MessageType::Video(_)
324
            )
325
    )
326
}
327
328
pub(super) async fn encryption(client: Client, events: EventSender) {
329
    let Some(device_id) = client.device_id().map(ToOwned::to_owned) else {
330
        tracing::warn!("encryption watcher stopped because the client has no device id");
331
        return;
332
    };
333
    let recovery = client.encryption().recovery();
334
    let backups = client.encryption().backups();
335
    let mut recovery_stream = recovery.state_stream();
336
    let mut backup_stream = backups.state_stream();
337
    let mut verification_stream = client.encryption().verification_state();
338
    let mut recovery_state = recovery.state();
339
    let mut backup_state = backups.state();
340
    let mut verification_state = verification_stream.get();
341
    loop {
342
        let state = encryption_state(
343
            device_id.clone(),
344
            recovery_state,
345
            verification_state,
346
            backup_state,
347
        );
348
        if events.send(Event::EncryptionChanged(state)).await.is_err() {
349
            return;
350
        }
351
        tokio::select! {
352
            Some(update) = recovery_stream.next() => recovery_state = update,
353
            Some(update) = verification_stream.next() => verification_state = update,
354
            Some(update) = backup_stream.next() => {
355
                backup_state = update.unwrap_or_else(|_| backups.state());
356
            }
357
            else => return,
358
        }
359
    }
360
}
361
362
pub(super) async fn rooms(client: Client, events: EventSender) {
363
    let (_, mut rooms) = client.rooms_stream();
364
    let mut info = client.room_info_notable_update_receiver();
365
    loop {
366
        if events.send(Event::RoomsChanged).await.is_err() {
367
            return;
368
        }
369
        tokio::select! {
370
            update = rooms.next() => if update.is_none() { return },
371
            update = info.recv() => if matches!(update, Err(RecvError::Closed)) { return },
372
        }
373
        // a sync touching fifty rooms is one invalidation, not fifty
374
        while let Ok(_) | Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) =
375
            info.try_recv()
376
        {}
377
    }
378
}
379
380
pub(super) async fn notification_settings(client: Client, events: EventSender) {
381
    let settings = client.notification_settings().await;
382
    let mut changes = settings.subscribe_to_changes();
383
    while let Ok(()) | Err(RecvError::Lagged(_)) = changes.recv().await {
384
        // room summaries carry the effective mode, and push rule changes never touch room info
385
        for event in [Event::NotificationSettingsChanged, Event::RoomsChanged] {
386
            if events.send(event).await.is_err() {
387
                return;
388
            }
389
        }
390
    }
391
}