atagen

macltok

public
Star0
Watch0
0291516d40b5b2bc402e7bbb1e7f41a962827bcb
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/media.rsRust743 lines24.8 KB
1
use std::{
2
    borrow::Cow,
3
    collections::{HashMap, VecDeque},
4
    io::Read as _,
5
    panic::AssertUnwindSafe,
6
    sync::Arc,
7
    time::Duration,
8
};
9
10
use futures_util::{
11
    FutureExt, StreamExt,
12
    future::{Shared, WeakShared},
13
};
14
use matrix_sdk::{
15
    Client, Room, SendOutsideWasm, SyncOutsideWasm,
16
    media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
17
    room::reply::{EnforceThread, Reply as SdkReply},
18
    ruma::{
19
        api::{
20
            Metadata, OutgoingRequest,
21
            auth_scheme::SendAccessToken,
22
            client::{authenticated_media, media},
23
        },
24
        events::room::{
25
            EncryptedFileHash, EncryptedFileHashAlgorithm, EncryptedFileInfo, MediaSource,
26
            message::{AddMentions, ReplyWithinThread},
27
        },
28
    },
29
};
30
use matrix_sdk_crypto::{AttachmentDecryptor, MediaEncryptionInfo};
31
use ruma::MxcUri;
32
use tokio::sync::Mutex;
33
34
use super::{executor::EngineBoxFuture, url_preview::UrlPreviews};
35
use crate::{
36
    AttachmentEncryption, AttachmentUpload, Error, ErrorKind, LinkPreview, Media, MediaFile,
37
    MediaId, MediaKind, MediaLocation, MediaSize, TimelineId,
38
    normalise::{MediaRegistration, MediaScope, MediaSourceInfo},
39
    upload::prepare_upload,
40
};
41
42
/// a stalled download gives up after this long without a byte
43
const DOWNLOAD_IDLE: Duration = Duration::from_secs(60);
44
/// the sdk client gives up on any request after thirty seconds, far too soon for a video
45
const DOWNLOAD_TOTAL: Duration = Duration::from_secs(60 * 60);
46
47
type FlightFuture = EngineBoxFuture<'static, Result<Media, Error>>;
48
type Flight = Shared<FlightFuture>;
49
50
/// owns everything needed to turn a media id or an mxc uri into bytes, so the
51
/// handle can answer media queries without a trip through the engine
52
pub(crate) struct MediaService {
53
    client: Client,
54
    registry: Mutex<MediaRegistry>,
55
    flights: Mutex<HashMap<String, WeakShared<FlightFuture>>>,
56
    max_bytes: usize,
57
    url_previews: UrlPreviews,
58
}
59
60
impl MediaService {
61
    pub(crate) fn new(client: Client, max_bytes: usize, max_sources: usize) -> Self {
62
        Self {
63
            url_previews: UrlPreviews::new(client.clone()),
64
            client,
65
            registry: Mutex::new(MediaRegistry::new(max_sources)),
66
            flights: Mutex::new(HashMap::new()),
67
            max_bytes,
68
        }
69
    }
70
71
    pub(crate) async fn url_preview(&self, url: &str) -> Result<Option<LinkPreview>, Error> {
72
        self.url_previews.preview(url).await
73
    }
74
75
    pub(crate) async fn register(&self, media: Vec<MediaRegistration>) {
76
        if !media.is_empty() {
77
            self.registry.lock().await.insert_all(media);
78
        }
79
    }
80
81
    pub(crate) async fn release(&self, scope: &MediaScope) {
82
        self.registry.lock().await.release(scope);
83
    }
84
85
    pub(crate) async fn clear(&self) {
86
        self.registry.lock().await.clear();
87
    }
88
89
    pub(crate) async fn timeline_media(
90
        &self,
91
        media_id: &MediaId,
92
        size: MediaSize,
93
    ) -> Result<Media, Error> {
94
        let chosen = choose_source(self.registered(media_id).await?, size)?;
95
        if chosen
96
            .declared_size
97
            .is_some_and(|size| size > self.max_bytes as u64)
98
        {
99
            return Err(too_large("media exceeds the configured byte limit"));
100
        }
101
        self.fetch_or_file(chosen.request, chosen.mimetype).await
102
    }
103
104
    pub(crate) async fn media_location(
105
        &self,
106
        media_id: &MediaId,
107
        size: MediaSize,
108
    ) -> Result<MediaLocation, Error> {
109
        let media = self.registered(media_id).await?;
110
        let chosen = choose_source(media.clone(), size)?;
111
        if is_local(&chosen.request.source) {
112
            return Ok(MediaLocation::Local);
113
        }
114
        if !matches!(chosen.request.format, MediaFormat::Thumbnail(_)) {
115
            return Ok(MediaLocation::File(
116
                media_file(
117
                    &self.client,
118
                    chosen.request,
119
                    chosen.mimetype,
120
                    chosen.declared_size,
121
                )
122
                .await?,
123
            ));
124
        }
125
        let url = media_http_request(&self.client, &chosen.request)
126
            .await?
127
            .uri()
128
            .to_string();
129
        let fallback = MediaRequestParameters {
130
            source: media.full.source,
131
            format: MediaFormat::File,
132
        };
133
        Ok(MediaLocation::ServerThumbnail {
134
            url,
135
            fallback: media_file(
136
                &self.client,
137
                fallback,
138
                media.full.mimetype,
139
                media.full.declared_size,
140
            )
141
            .await?,
142
        })
143
    }
144
145
    async fn registered(&self, media_id: &MediaId) -> Result<MediaRegistration, Error> {
146
        if let Some(uri) = media_id.as_plain() {
147
            return plain_registration(media_id, uri);
148
        }
149
        self.registry
150
            .lock()
151
            .await
152
            .get(media_id)
153
            .cloned()
154
            .ok_or_else(|| {
155
                Error::new(
156
                    ErrorKind::MediaUnknown,
157
                    "media is not registered in a retained timeline",
158
                )
159
            })
160
    }
161
162
    /// plenty of servers won't thumbnail everything, and the file itself is the next best
163
    /// thing. the byte cap still holds either way
164
    async fn fetch_or_file(
165
        &self,
166
        request: MediaRequestParameters,
167
        mimetype: Option<String>,
168
    ) -> Result<Media, Error> {
169
        if !matches!(request.format, MediaFormat::Thumbnail(_)) {
170
            return self.fetch(request, mimetype).await;
171
        }
172
        match self.fetch(request.clone(), mimetype.clone()).await {
173
            Err(error) if error.kind == ErrorKind::MediaUnavailable => {
174
                let file = MediaRequestParameters {
175
                    source: request.source,
176
                    format: MediaFormat::File,
177
                };
178
                self.fetch(file, mimetype).await
179
            }
180
            result => result,
181
        }
182
    }
183
184
    async fn fetch(
185
        &self,
186
        request: MediaRequestParameters,
187
        mimetype: Option<String>,
188
    ) -> Result<Media, Error> {
189
        let key = flight_key(&request);
190
        let flight = {
191
            let mut flights = self.flights.lock().await;
192
            flights.retain(|_, flight| flight.upgrade().is_some());
193
            match flights.get(&key).and_then(WeakShared::upgrade) {
194
                Some(flight) => flight,
195
                None => {
196
                    let prepared = AssertUnwindSafe(fetch(
197
                        self.client.clone(),
198
                        request,
199
                        mimetype,
200
                        self.max_bytes,
201
                    ))
202
                    .catch_unwind()
203
                    .map(|result| {
204
                        result.unwrap_or_else(|_| {
205
                            Err(Error::new(ErrorKind::Other, "media fetch panicked"))
206
                        })
207
                    });
208
                    let boxed: FlightFuture = Box::pin(prepared);
209
                    let flight: Flight = boxed.shared();
210
                    if let Some(weak) = flight.downgrade() {
211
                        flights.insert(key, weak);
212
                    }
213
                    flight
214
                }
215
            }
216
        };
217
        flight.await
218
    }
219
}
220
221
struct ChosenSource {
222
    request: MediaRequestParameters,
223
    mimetype: Option<String>,
224
    declared_size: Option<u64>,
225
}
226
227
fn plain_registration(media_id: &MediaId, uri: &MxcUri) -> Result<MediaRegistration, Error> {
228
    if !uri.is_valid() {
229
        return Err(Error::new(ErrorKind::Invalid, "mxc uri is malformed"));
230
    }
231
    Ok(MediaRegistration {
232
        id: media_id.clone(),
233
        kind: MediaKind::Image,
234
        full: MediaSourceInfo {
235
            source: MediaSource::Plain(uri.to_owned()),
236
            mimetype: None,
237
            declared_size: None,
238
        },
239
        thumbnail: None,
240
    })
241
}
242
243
fn choose_source(media: MediaRegistration, size: MediaSize) -> Result<ChosenSource, Error> {
244
    let (source, format, mimetype, declared_size) = match size {
245
        MediaSize::Full => (
246
            media.full.source,
247
            MediaFormat::File,
248
            media.full.mimetype,
249
            media.full.declared_size,
250
        ),
251
        MediaSize::Thumbnail { width, height } => match media.thumbnail {
252
            Some(thumbnail) => (
253
                thumbnail.source,
254
                MediaFormat::File,
255
                thumbnail.mimetype,
256
                thumbnail.declared_size,
257
            ),
258
            // the server can only resize what it can read
259
            None if matches!(media.kind, MediaKind::Image | MediaKind::Video)
260
                && matches!(media.full.source, MediaSource::Plain(_)) =>
261
            {
262
                // the server picks the thumbnail's type, so the original's would be a lie
263
                let format = if is_local(&media.full.source) {
264
                    MediaFormat::File
265
                } else {
266
                    thumbnail_format(width, height)
267
                };
268
                (media.full.source, format, None, None)
269
            }
270
            None => {
271
                return Err(Error::new(
272
                    ErrorKind::MediaUnavailable,
273
                    "the requested media size is unavailable",
274
                ));
275
            }
276
        },
277
    };
278
    Ok(ChosenSource {
279
        request: MediaRequestParameters { source, format },
280
        mimetype,
281
        declared_size,
282
    })
283
}
284
285
async fn media_file(
286
    client: &Client,
287
    request: MediaRequestParameters,
288
    mimetype: Option<String>,
289
    declared_size: Option<u64>,
290
) -> Result<MediaFile, Error> {
291
    let url = media_http_request(client, &request)
292
        .await?
293
        .uri()
294
        .to_string();
295
    let encryption = match &request.source {
296
        MediaSource::Plain(_) => None,
297
        MediaSource::Encrypted(file) => {
298
            let (EncryptedFileInfo::V2(info), Some(EncryptedFileHash::Sha256(sha256))) = (
299
                &file.info,
300
                file.hashes.get(&EncryptedFileHashAlgorithm::Sha256),
301
            ) else {
302
                return Err(Error::new(
303
                    ErrorKind::MediaUnavailable,
304
                    "encrypted media is not v2 with a sha256 hash",
305
                ));
306
            };
307
            Some(AttachmentEncryption {
308
                key: info.k.encode(),
309
                iv: info.iv.encode(),
310
                sha256: sha256.encode(),
311
            })
312
        }
313
    };
314
    Ok(MediaFile {
315
        url,
316
        mimetype,
317
        declared_size,
318
        encryption,
319
    })
320
}
321
322
fn thumbnail_format(width: u32, height: u32) -> MediaFormat {
323
    MediaFormat::Thumbnail(MediaThumbnailSettings::new(width.into(), height.into()))
324
}
325
326
fn is_local(source: &MediaSource) -> bool {
327
    let uri = match source {
328
        MediaSource::Plain(uri) => uri,
329
        MediaSource::Encrypted(file) => &file.url,
330
    };
331
    uri.server_name()
332
        .is_ok_and(|server| server.as_str() == "send-queue.localhost")
333
}
334
335
/// plain and encrypted sources share a cache key in the sdk, but must never share a flight
336
fn flight_key(request: &MediaRequestParameters) -> String {
337
    let uri = match &request.source {
338
        MediaSource::Plain(uri) => format!("plain|{uri}"),
339
        MediaSource::Encrypted(file) => format!("encrypted|{}", file.url),
340
    };
341
    match &request.format {
342
        MediaFormat::File => uri.to_owned(),
343
        MediaFormat::Thumbnail(settings) => format!(
344
            "{uri}|{}|{}x{}|{}",
345
            settings.method, settings.width, settings.height, settings.animated
346
        ),
347
    }
348
}
349
350
fn too_large(message: &'static str) -> Error {
351
    Error::new(ErrorKind::MediaTooLarge, message)
352
}
353
354
async fn fetch(
355
    client: Client,
356
    request: MediaRequestParameters,
357
    mimetype: Option<String>,
358
    max_bytes: usize,
359
) -> Result<Media, Error> {
360
    if let Some(cached) = cached_media(&client, &request).await? {
361
        if cached.len() > max_bytes {
362
            if let Ok(store) = client.media_store().lock().await {
363
                let _ = store.remove_media_content(&request).await;
364
            }
365
            return Err(too_large("cached media exceeds the configured byte limit"));
366
        }
367
        // plain and encrypted share a cache key, so a plain fetch of an encrypted file's uri
368
        // leaves ciphertext here. its hash gives it away
369
        if is_ciphertext(&cached, &request.source) {
370
            let bytes = maybe_decrypt(cached, &request, max_bytes)?;
371
            cache(&client, &request, &bytes).await;
372
            return Ok(media(bytes, mimetype));
373
        }
374
        return Ok(media(cached, mimetype));
375
    }
376
    let (bytes, served_mimetype) = download_bounded(&client, &request, max_bytes).await?;
377
    // the sdk caches plaintext under the encrypted key, so we do too or nobody can read anything
378
    let bytes = maybe_decrypt(bytes, &request, max_bytes)?;
379
    cache(&client, &request, &bytes).await;
380
    Ok(media(bytes, mimetype.or(served_mimetype)))
381
}
382
383
async fn cache(client: &Client, request: &MediaRequestParameters, bytes: &[u8]) {
384
    if let Ok(store) = client.media_store().lock().await {
385
        let _ = store
386
            .add_media_content(
387
                request,
388
                bytes.to_vec(),
389
                matrix_sdk::media::store::IgnoreMediaRetentionPolicy::No,
390
            )
391
            .await;
392
    }
393
}
394
395
fn is_ciphertext(bytes: &[u8], source: &MediaSource) -> bool {
396
    use sha2::{Digest, Sha256};
397
    let MediaSource::Encrypted(file) = source else {
398
        return false;
399
    };
400
    file.hashes
401
        .get(&ruma::events::room::EncryptedFileHashAlgorithm::Sha256)
402
        .is_some_and(|hash| hash.as_bytes() == Sha256::digest(bytes).as_slice())
403
}
404
405
fn media(bytes: Vec<u8>, mimetype: Option<String>) -> Media {
406
    let mimetype = mimetype
407
        .or_else(|| sniff_image(&bytes).map(str::to_owned))
408
        .unwrap_or_else(|| "application/octet-stream".to_owned());
409
    Media {
410
        bytes: Arc::from(bytes),
411
        mimetype,
412
    }
413
}
414
415
/// the cache forgets content types, and avatars never declare one. images are the common case
416
fn sniff_image(bytes: &[u8]) -> Option<&'static str> {
417
    match bytes {
418
        [0x89, b'P', b'N', b'G', ..] => Some("image/png"),
419
        [0xff, 0xd8, 0xff, ..] => Some("image/jpeg"),
420
        [b'G', b'I', b'F', b'8', ..] => Some("image/gif"),
421
        [
422
            b'R',
423
            b'I',
424
            b'F',
425
            b'F',
426
            _,
427
            _,
428
            _,
429
            _,
430
            b'W',
431
            b'E',
432
            b'B',
433
            b'P',
434
            ..,
435
        ] => Some("image/webp"),
436
        _ => None,
437
    }
438
}
439
440
async fn cached_media(
441
    client: &Client,
442
    request: &MediaRequestParameters,
443
) -> Result<Option<Vec<u8>>, Error> {
444
    fn cache_failed(
445
        source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
446
    ) -> Error {
447
        Error::with_source(ErrorKind::Store, "reading media cache failed", source)
448
    }
449
    client
450
        .media_store()
451
        .lock()
452
        .await
453
        .map_err(cache_failed)?
454
        .get_media_content(request)
455
        .await
456
        .map_err(cache_failed)
457
}
458
459
fn maybe_decrypt(
460
    bytes: Vec<u8>,
461
    request: &MediaRequestParameters,
462
    max_bytes: usize,
463
) -> Result<Vec<u8>, Error> {
464
    let MediaSource::Encrypted(file) = &request.source else {
465
        return Ok(bytes);
466
    };
467
    fn decrypt_failed(
468
        source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
469
    ) -> Error {
470
        Error::with_source(
471
            ErrorKind::MediaUnavailable,
472
            "media decryption failed",
473
            source,
474
        )
475
    }
476
    let info: MediaEncryptionInfo = (**file).clone().into();
477
    let mut cursor = std::io::Cursor::new(bytes);
478
    let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).map_err(decrypt_failed)?;
479
    let mut decrypted = Vec::new();
480
    let mut chunk = [0_u8; 8192];
481
    loop {
482
        let read = decryptor.read(&mut chunk).map_err(decrypt_failed)?;
483
        if read == 0 {
484
            return Ok(decrypted);
485
        }
486
        if read > max_bytes.saturating_sub(decrypted.len()) {
487
            return Err(too_large(
488
                "decrypted media exceeds the configured byte limit",
489
            ));
490
        }
491
        decrypted.extend_from_slice(&chunk[..read]);
492
    }
493
}
494
495
/// the sdk's own download has no size cap, so we do it by hand
496
async fn download_bounded(
497
    client: &Client,
498
    request: &MediaRequestParameters,
499
    max_bytes: usize,
500
) -> Result<(Vec<u8>, Option<String>), Error> {
501
    if is_local(&request.source) {
502
        return Err(Error::new(
503
            ErrorKind::MediaUnavailable,
504
            "local media is not in the media cache",
505
        ));
506
    }
507
    let network_failed =
508
        |source| Error::with_source(ErrorKind::Network, "media download failed", source);
509
    let mut refreshed = false;
510
    let response = loop {
511
        let http_request = media_http_request(client, request).await?;
512
        let (parts, _) = http_request.into_parts();
513
        let mut builder = client
514
            .http_client()
515
            .get(parts.uri.to_string())
516
            .timeout(DOWNLOAD_TOTAL);
517
        for (name, value) in &parts.headers {
518
            builder = builder.header(name.as_str(), value.as_bytes());
519
        }
520
        let response = builder.send().await.map_err(network_failed)?;
521
        if response.status() != matrix_sdk::reqwest::StatusCode::UNAUTHORIZED {
522
            break response;
523
        }
524
        if refreshed
525
            || client
526
                .session_tokens()
527
                .is_none_or(|tokens| tokens.refresh_token.is_none())
528
        {
529
            return Err(Error::new(
530
                ErrorKind::Unauthenticated,
531
                "media download was not authorized",
532
            ));
533
        }
534
        client.refresh_access_token().await.map_err(|source| {
535
            Error::from_refresh_token("refreshing media access token failed", source)
536
        })?;
537
        refreshed = true;
538
    };
539
    let status = response.status();
540
    if !status.is_success() {
541
        return Err(Error::new(
542
            ErrorKind::MediaUnavailable,
543
            format!("media download failed with status {status}"),
544
        ));
545
    }
546
    if response
547
        .content_length()
548
        .is_some_and(|length| length > max_bytes as u64)
549
    {
550
        return Err(too_large("media exceeds the configured byte limit"));
551
    }
552
    let mimetype = response
553
        .headers()
554
        .get(http::header::CONTENT_TYPE)
555
        .and_then(|value| value.to_str().ok())
556
        .map(str::to_owned);
557
    let mut stream = response.bytes_stream();
558
    let mut bytes = Vec::new();
559
    loop {
560
        let chunk = matrix_sdk::timeout::timeout(stream.next(), DOWNLOAD_IDLE)
561
            .await
562
            .map_err(|_| Error::new(ErrorKind::Network, "media download stalled"))?;
563
        let Some(chunk) = chunk else { break };
564
        let chunk = chunk.map_err(network_failed)?;
565
        if chunk.len() > max_bytes.saturating_sub(bytes.len()) {
566
            return Err(too_large(
567
                "downloaded media exceeds the configured byte limit",
568
            ));
569
        }
570
        bytes.extend_from_slice(&chunk);
571
    }
572
    Ok((bytes, mimetype))
573
}
574
575
async fn media_http_request(
576
    client: &Client,
577
    request: &MediaRequestParameters,
578
) -> Result<http::Request<Vec<u8>>, Error> {
579
    let versions = client.supported_versions().await.map_err(|source| {
580
        Error::with_source(
581
            ErrorKind::Network,
582
            "reading supported server versions failed",
583
            source,
584
        )
585
    })?;
586
    let authenticated =
587
        authenticated_media::get_content::v1::Request::PATH_BUILDER.is_supported(&versions);
588
    let base = client.homeserver().to_string();
589
    let token = client.access_token();
590
    let invalid_uri =
591
        |source| Error::with_source(ErrorKind::Invalid, "media URI is invalid", source);
592
    let (uri, thumbnail) = match (&request.source, &request.format) {
593
        (MediaSource::Encrypted(file), _) => (&file.url, None),
594
        (MediaSource::Plain(uri), MediaFormat::File) => (uri, None),
595
        (MediaSource::Plain(uri), MediaFormat::Thumbnail(settings)) => (uri, Some(settings)),
596
    };
597
    // each endpoint has its own auth input type, so no generic helper can take them all
598
    macro_rules! into_http {
599
        ($endpoint:expr, $token:expr) => {
600
            $endpoint
601
                .try_into_http_request::<Vec<u8>>(&base, $token, Cow::Borrowed(&versions))
602
                .map_err(|source| {
603
                    Error::with_source(ErrorKind::Other, "media request is invalid", source)
604
                })?
605
        };
606
    }
607
    let authed = SendAccessToken::IfRequired(token.as_deref().unwrap_or(""));
608
    #[allow(deprecated)]
609
    let mut http_request = match (thumbnail, authenticated) {
610
        (None, true) => into_http!(
611
            authenticated_media::get_content::v1::Request::from_uri(uri).map_err(invalid_uri)?,
612
            authed
613
        ),
614
        (None, false) => into_http!(
615
            media::get_content::v3::Request::from_url(uri).map_err(invalid_uri)?,
616
            SendAccessToken::None
617
        ),
618
        (Some(settings), true) => {
619
            let mut endpoint = authenticated_media::get_content_thumbnail::v1::Request::from_uri(
620
                uri,
621
                settings.width,
622
                settings.height,
623
            )
624
            .map_err(invalid_uri)?;
625
            endpoint.method = Some(settings.method.clone());
626
            endpoint.animated = Some(settings.animated);
627
            into_http!(endpoint, authed)
628
        }
629
        (Some(settings), false) => {
630
            let mut endpoint = media::get_content_thumbnail::v3::Request::from_url(
631
                uri,
632
                settings.width,
633
                settings.height,
634
            )
635
            .map_err(invalid_uri)?;
636
            endpoint.method = Some(settings.method.clone());
637
            endpoint.animated = Some(settings.animated);
638
            into_http!(endpoint, SendAccessToken::None)
639
        }
640
    };
641
    // legacy endpoints don't ask for a token but plenty of servers want one anyway
642
    if let Some(token) = token.as_deref() {
643
        let value = format!("Bearer {token}").parse().map_err(|_| {
644
            Error::new(ErrorKind::Unauthenticated, "stored access token is invalid")
645
        })?;
646
        http_request
647
            .headers_mut()
648
            .insert(http::header::AUTHORIZATION, value);
649
    }
650
    Ok(http_request)
651
}
652
653
pub(crate) async fn queue_upload(
654
    room: Room,
655
    upload: AttachmentUpload,
656
    max_bytes: usize,
657
) -> Result<(), Error> {
658
    let (upload, mime, mut config) = prepare_upload(upload, max_bytes)?;
659
    if let TimelineId::Thread { root_event_id, .. } = &upload.timeline_id {
660
        config.reply = Some(SdkReply {
661
            event_id: root_event_id.clone(),
662
            enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
663
            add_mentions: AddMentions::No,
664
        });
665
    }
666
    room.send_queue()
667
        .send_attachment(upload.filename, mime, upload.bytes, config)
668
        .await
669
        .map(drop)
670
        .map_err(|source| {
671
            use matrix_sdk::send_queue::RoomSendQueueError;
672
            let kind = match &source {
673
                RoomSendQueueError::RoomNotJoined | RoomSendQueueError::RoomDisappeared => {
674
                    ErrorKind::RoomUnknown
675
                }
676
                RoomSendQueueError::StorageError(_) => ErrorKind::Store,
677
                _ => ErrorKind::MediaUnavailable,
678
            };
679
            Error::with_source(kind, "media upload could not be queued", source)
680
        })
681
}
682
683
/// least recently seen goes first. stale order slots are skipped by stamp rather than hunted down
684
struct MediaRegistry {
685
    entries: HashMap<MediaId, (MediaRegistration, u64)>,
686
    order: VecDeque<(MediaId, u64)>,
687
    next_stamp: u64,
688
    limit: usize,
689
}
690
691
impl MediaRegistry {
692
    fn new(limit: usize) -> Self {
693
        Self {
694
            entries: HashMap::new(),
695
            order: VecDeque::new(),
696
            next_stamp: 0,
697
            limit,
698
        }
699
    }
700
701
    fn get(&self, id: &MediaId) -> Option<&MediaRegistration> {
702
        self.entries.get(id).map(|(media, _)| media)
703
    }
704
705
    fn insert_all(&mut self, media: Vec<MediaRegistration>) {
706
        for media in media {
707
            let stamp = self.next_stamp;
708
            self.next_stamp += 1;
709
            self.order.push_back((media.id.clone(), stamp));
710
            self.entries.insert(media.id.clone(), (media, stamp));
711
        }
712
        while self.entries.len() > self.limit {
713
            let Some((id, stamp)) = self.order.pop_front() else {
714
                break;
715
            };
716
            if self.is_current(&id, stamp) {
717
                self.entries.remove(&id);
718
            }
719
        }
720
        if self.order.len() > self.limit.saturating_mul(4) {
721
            let entries = &self.entries;
722
            self.order
723
                .retain(|(id, stamp)| entries.get(id).is_some_and(|(_, current)| current == stamp));
724
        }
725
    }
726
727
    fn is_current(&self, id: &MediaId, stamp: u64) -> bool {
728
        self.entries
729
            .get(id)
730
            .is_some_and(|(_, current)| *current == stamp)
731
    }
732
733
    fn release(&mut self, scope: &MediaScope) {
734
        self.entries.retain(|id, _| !scope.owns(id));
735
        let entries = &self.entries;
736
        self.order.retain(|(id, _)| entries.contains_key(id));
737
    }
738
739
    fn clear(&mut self) {
740
        self.entries.clear();
741
        self.order.clear();
742
    }
743
}