atagen

macltok

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