atagen

macltok

public
Star0
Watch0
722b4c3d0a09ca2aa7b242170395338933a65d52
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/engine/media.rs

31ce2453c82fatagen2026-09-24 13:07 UTC1
use std::{
31ce2453c82fatagen2026-09-24 13:07 UTC2
    borrow::Cow,
31ce2453c82fatagen2026-09-24 13:07 UTC3
    collections::{HashMap, VecDeque},
31ce2453c82fatagen2026-09-24 13:07 UTC4
    io::Read as _,
31ce2453c82fatagen2026-09-24 13:07 UTC5
    panic::AssertUnwindSafe,
31ce2453c82fatagen2026-09-24 13:07 UTC6
    sync::Arc,
5a52012c9b1datagen2026-09-24 23:45 UTC7
    time::Duration,
31ce2453c82fatagen2026-09-24 13:07 UTC8
};
31ce2453c82fatagen2026-09-24 13:07 UTC9
31ce2453c82fatagen2026-09-24 13:07 UTC10
use futures_util::{
31ce2453c82fatagen2026-09-24 13:07 UTC11
    FutureExt, StreamExt,
1fe480c16a3catagen2026-09-26 08:44 UTC12
    future::{Shared, WeakShared},
31ce2453c82fatagen2026-09-24 13:07 UTC13
};
31ce2453c82fatagen2026-09-24 13:07 UTC14
use matrix_sdk::{
1fe480c16a3catagen2026-09-26 08:44 UTC15
    Client, Room, SendOutsideWasm, SyncOutsideWasm,
31ce2453c82fatagen2026-09-24 13:07 UTC16
    media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
31ce2453c82fatagen2026-09-24 13:07 UTC17
    room::reply::{EnforceThread, Reply as SdkReply},
31ce2453c82fatagen2026-09-24 13:07 UTC18
    ruma::{
31ce2453c82fatagen2026-09-24 13:07 UTC19
        api::{
31ce2453c82fatagen2026-09-24 13:07 UTC20
            Metadata, OutgoingRequest,
31ce2453c82fatagen2026-09-24 13:07 UTC21
            auth_scheme::SendAccessToken,
31ce2453c82fatagen2026-09-24 13:07 UTC22
            client::{authenticated_media, media},
31ce2453c82fatagen2026-09-24 13:07 UTC23
        },
31ce2453c82fatagen2026-09-24 13:07 UTC24
        events::room::{
5b21b919d1e6atagen2026-09-27 11:47 UTC25
            EncryptedFileHash, EncryptedFileHashAlgorithm, EncryptedFileInfo, MediaSource,
0827ad2d24e9atagen2026-10-23 06:49 UTC26
            message::{
0827ad2d24e9atagen2026-10-23 06:49 UTC27
                AddMentions, Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation,
0827ad2d24e9atagen2026-10-23 06:49 UTC28
            },
31ce2453c82fatagen2026-09-24 13:07 UTC29
        },
31ce2453c82fatagen2026-09-24 13:07 UTC30
    },
31ce2453c82fatagen2026-09-24 13:07 UTC31
};
31ce2453c82fatagen2026-09-24 13:07 UTC32
use matrix_sdk_crypto::{AttachmentDecryptor, MediaEncryptionInfo};
0827ad2d24e9atagen2026-10-23 06:49 UTC33
use ruma::{MxcUri, OwnedEventId};
31ce2453c82fatagen2026-09-24 13:07 UTC34
use tokio::sync::Mutex;
31ce2453c82fatagen2026-09-24 13:07 UTC35
0291516d40b5atagen2026-10-22 08:31 UTC36
use super::{executor::EngineBoxFuture, url_preview::UrlPreviews};
31ce2453c82fatagen2026-09-24 13:07 UTC37
use crate::{
0291516d40b5atagen2026-10-22 08:31 UTC38
    AttachmentEncryption, AttachmentUpload, Error, ErrorKind, LinkPreview, Media, MediaFile,
0827ad2d24e9atagen2026-10-23 06:49 UTC39
    MediaId, MediaKind, MediaLocation, MediaSize,
5d2974de5826atagen2026-10-08 11:29 UTC40
    normalise::{MediaRegistration, MediaScope, MediaSourceInfo},
31ce2453c82fatagen2026-09-24 13:07 UTC41
    upload::prepare_upload,
31ce2453c82fatagen2026-09-24 13:07 UTC42
};
31ce2453c82fatagen2026-09-24 13:07 UTC43
5a52012c9b1datagen2026-09-24 23:45 UTC44
/// a stalled download gives up after this long without a byte
5a52012c9b1datagen2026-09-24 23:45 UTC45
const DOWNLOAD_IDLE: Duration = Duration::from_secs(60);
5a52012c9b1datagen2026-09-24 23:45 UTC46
/// the sdk client gives up on any request after thirty seconds, far too soon for a video
5a52012c9b1datagen2026-09-24 23:45 UTC47
const DOWNLOAD_TOTAL: Duration = Duration::from_secs(60 * 60);
31ce2453c82fatagen2026-09-24 13:07 UTC48
1fe480c16a3catagen2026-09-26 08:44 UTC49
type FlightFuture = EngineBoxFuture<'static, Result<Media, Error>>;
31ce2453c82fatagen2026-09-24 13:07 UTC50
type Flight = Shared<FlightFuture>;
31ce2453c82fatagen2026-09-24 13:07 UTC51
31ce2453c82fatagen2026-09-24 13:07 UTC52
/// owns everything needed to turn a media id or an mxc uri into bytes, so the
31ce2453c82fatagen2026-09-24 13:07 UTC53
/// handle can answer media queries without a trip through the engine
31ce2453c82fatagen2026-09-24 13:07 UTC54
pub(crate) struct MediaService {
31ce2453c82fatagen2026-09-24 13:07 UTC55
    client: Client,
31ce2453c82fatagen2026-09-24 13:07 UTC56
    registry: Mutex<MediaRegistry>,
31ce2453c82fatagen2026-09-24 13:07 UTC57
    flights: Mutex<HashMap<String, WeakShared<FlightFuture>>>,
31ce2453c82fatagen2026-09-24 13:07 UTC58
    max_bytes: usize,
da8896938cf0atagen2026-10-25 10:04 UTC59
    context_limit: usize,
0291516d40b5atagen2026-10-22 08:31 UTC60
    url_previews: UrlPreviews,
31ce2453c82fatagen2026-09-24 13:07 UTC61
}
31ce2453c82fatagen2026-09-24 13:07 UTC62
31ce2453c82fatagen2026-09-24 13:07 UTC63
impl MediaService {
da8896938cf0atagen2026-10-25 10:04 UTC64
    pub(crate) fn new(client: Client, max_bytes: usize, context_limit: usize) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC65
        Self {
0291516d40b5atagen2026-10-22 08:31 UTC66
            url_previews: UrlPreviews::new(client.clone()),
31ce2453c82fatagen2026-09-24 13:07 UTC67
            client,
da8896938cf0atagen2026-10-25 10:04 UTC68
            registry: Mutex::new(MediaRegistry::default()),
31ce2453c82fatagen2026-09-24 13:07 UTC69
            flights: Mutex::new(HashMap::new()),
31ce2453c82fatagen2026-09-24 13:07 UTC70
            max_bytes,
da8896938cf0atagen2026-10-25 10:04 UTC71
            context_limit,
31ce2453c82fatagen2026-09-24 13:07 UTC72
        }
31ce2453c82fatagen2026-09-24 13:07 UTC73
    }
31ce2453c82fatagen2026-09-24 13:07 UTC74
da8896938cf0atagen2026-10-25 10:04 UTC75
    /// one event context session's media, bounded by `EngineConfig::max_context_media`
da8896938cf0atagen2026-10-25 10:04 UTC76
    pub(crate) fn context_scope(&self) -> MediaScope {
da8896938cf0atagen2026-10-25 10:04 UTC77
        MediaScope::bounded(self.context_limit)
da8896938cf0atagen2026-10-25 10:04 UTC78
    }
da8896938cf0atagen2026-10-25 10:04 UTC79
0291516d40b5atagen2026-10-22 08:31 UTC80
    pub(crate) async fn url_preview(&self, url: &str) -> Result<Option<LinkPreview>, Error> {
0291516d40b5atagen2026-10-22 08:31 UTC81
        self.url_previews.preview(url).await
0291516d40b5atagen2026-10-22 08:31 UTC82
    }
0291516d40b5atagen2026-10-22 08:31 UTC83
da8896938cf0atagen2026-10-25 10:04 UTC84
    pub(crate) async fn register(&self, scope: &MediaScope, media: Vec<MediaRegistration>) {
31ce2453c82fatagen2026-09-24 13:07 UTC85
        if !media.is_empty() {
da8896938cf0atagen2026-10-25 10:04 UTC86
            self.registry.lock().await.insert_all(scope, media);
31ce2453c82fatagen2026-09-24 13:07 UTC87
        }
31ce2453c82fatagen2026-09-24 13:07 UTC88
    }
31ce2453c82fatagen2026-09-24 13:07 UTC89
31ce2453c82fatagen2026-09-24 13:07 UTC90
    pub(crate) async fn release(&self, scope: &MediaScope) {
31ce2453c82fatagen2026-09-24 13:07 UTC91
        self.registry.lock().await.release(scope);
31ce2453c82fatagen2026-09-24 13:07 UTC92
    }
31ce2453c82fatagen2026-09-24 13:07 UTC93
31ce2453c82fatagen2026-09-24 13:07 UTC94
    pub(crate) async fn clear(&self) {
31ce2453c82fatagen2026-09-24 13:07 UTC95
        self.registry.lock().await.clear();
31ce2453c82fatagen2026-09-24 13:07 UTC96
    }
31ce2453c82fatagen2026-09-24 13:07 UTC97
31ce2453c82fatagen2026-09-24 13:07 UTC98
    pub(crate) async fn timeline_media(
31ce2453c82fatagen2026-09-24 13:07 UTC99
        &self,
31ce2453c82fatagen2026-09-24 13:07 UTC100
        media_id: &MediaId,
31ce2453c82fatagen2026-09-24 13:07 UTC101
        size: MediaSize,
31ce2453c82fatagen2026-09-24 13:07 UTC102
    ) -> Result<Media, Error> {
5b21b919d1e6atagen2026-09-27 11:47 UTC103
        let chosen = choose_source(self.registered(media_id).await?, size)?;
5b21b919d1e6atagen2026-09-27 11:47 UTC104
        if chosen
5b21b919d1e6atagen2026-09-27 11:47 UTC105
            .declared_size
5b21b919d1e6atagen2026-09-27 11:47 UTC106
            .is_some_and(|size| size > self.max_bytes as u64)
5b21b919d1e6atagen2026-09-27 11:47 UTC107
        {
5b21b919d1e6atagen2026-09-27 11:47 UTC108
            return Err(too_large("media exceeds the configured byte limit"));
5b21b919d1e6atagen2026-09-27 11:47 UTC109
        }
5b21b919d1e6atagen2026-09-27 11:47 UTC110
        self.fetch_or_file(chosen.request, chosen.mimetype).await
5b21b919d1e6atagen2026-09-27 11:47 UTC111
    }
5b21b919d1e6atagen2026-09-27 11:47 UTC112
5b21b919d1e6atagen2026-09-27 11:47 UTC113
    pub(crate) async fn media_location(
5b21b919d1e6atagen2026-09-27 11:47 UTC114
        &self,
5b21b919d1e6atagen2026-09-27 11:47 UTC115
        media_id: &MediaId,
5b21b919d1e6atagen2026-09-27 11:47 UTC116
        size: MediaSize,
5b21b919d1e6atagen2026-09-27 11:47 UTC117
    ) -> Result<MediaLocation, Error> {
5b21b919d1e6atagen2026-09-27 11:47 UTC118
        let media = self.registered(media_id).await?;
5b21b919d1e6atagen2026-09-27 11:47 UTC119
        let chosen = choose_source(media.clone(), size)?;
5b21b919d1e6atagen2026-09-27 11:47 UTC120
        if is_local(&chosen.request.source) {
5b21b919d1e6atagen2026-09-27 11:47 UTC121
            return Ok(MediaLocation::Local);
5b21b919d1e6atagen2026-09-27 11:47 UTC122
        }
5b21b919d1e6atagen2026-09-27 11:47 UTC123
        if !matches!(chosen.request.format, MediaFormat::Thumbnail(_)) {
5b21b919d1e6atagen2026-09-27 11:47 UTC124
            return Ok(MediaLocation::File(
5b21b919d1e6atagen2026-09-27 11:47 UTC125
                media_file(
5b21b919d1e6atagen2026-09-27 11:47 UTC126
                    &self.client,
5b21b919d1e6atagen2026-09-27 11:47 UTC127
                    chosen.request,
5b21b919d1e6atagen2026-09-27 11:47 UTC128
                    chosen.mimetype,
5b21b919d1e6atagen2026-09-27 11:47 UTC129
                    chosen.declared_size,
5b21b919d1e6atagen2026-09-27 11:47 UTC130
                )
5b21b919d1e6atagen2026-09-27 11:47 UTC131
                .await?,
5b21b919d1e6atagen2026-09-27 11:47 UTC132
            ));
5b21b919d1e6atagen2026-09-27 11:47 UTC133
        }
5b21b919d1e6atagen2026-09-27 11:47 UTC134
        let url = media_http_request(&self.client, &chosen.request)
5b21b919d1e6atagen2026-09-27 11:47 UTC135
            .await?
5b21b919d1e6atagen2026-09-27 11:47 UTC136
            .uri()
5b21b919d1e6atagen2026-09-27 11:47 UTC137
            .to_string();
5b21b919d1e6atagen2026-09-27 11:47 UTC138
        let fallback = MediaRequestParameters {
5b21b919d1e6atagen2026-09-27 11:47 UTC139
            source: media.full.source,
5b21b919d1e6atagen2026-09-27 11:47 UTC140
            format: MediaFormat::File,
5b21b919d1e6atagen2026-09-27 11:47 UTC141
        };
5b21b919d1e6atagen2026-09-27 11:47 UTC142
        Ok(MediaLocation::ServerThumbnail {
5b21b919d1e6atagen2026-09-27 11:47 UTC143
            url,
5b21b919d1e6atagen2026-09-27 11:47 UTC144
            fallback: media_file(
5b21b919d1e6atagen2026-09-27 11:47 UTC145
                &self.client,
5b21b919d1e6atagen2026-09-27 11:47 UTC146
                fallback,
5b21b919d1e6atagen2026-09-27 11:47 UTC147
                media.full.mimetype,
5b21b919d1e6atagen2026-09-27 11:47 UTC148
                media.full.declared_size,
5b21b919d1e6atagen2026-09-27 11:47 UTC149
            )
5b21b919d1e6atagen2026-09-27 11:47 UTC150
            .await?,
5b21b919d1e6atagen2026-09-27 11:47 UTC151
        })
5b21b919d1e6atagen2026-09-27 11:47 UTC152
    }
5b21b919d1e6atagen2026-09-27 11:47 UTC153
5b21b919d1e6atagen2026-09-27 11:47 UTC154
    async fn registered(&self, media_id: &MediaId) -> Result<MediaRegistration, Error> {
5d2974de5826atagen2026-10-08 11:29 UTC155
        if let Some(uri) = media_id.as_plain() {
5d2974de5826atagen2026-10-08 11:29 UTC156
            return plain_registration(media_id, uri);
5d2974de5826atagen2026-10-08 11:29 UTC157
        }
5b21b919d1e6atagen2026-09-27 11:47 UTC158
        self.registry
31ce2453c82fatagen2026-09-24 13:07 UTC159
            .lock()
31ce2453c82fatagen2026-09-24 13:07 UTC160
            .await
31ce2453c82fatagen2026-09-24 13:07 UTC161
            .get(media_id)
31ce2453c82fatagen2026-09-24 13:07 UTC162
            .cloned()
31ce2453c82fatagen2026-09-24 13:07 UTC163
            .ok_or_else(|| {
31ce2453c82fatagen2026-09-24 13:07 UTC164
                Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC165
                    ErrorKind::MediaUnknown,
31ce2453c82fatagen2026-09-24 13:07 UTC166
                    "media is not registered in a retained timeline",
31ce2453c82fatagen2026-09-24 13:07 UTC167
                )
5b21b919d1e6atagen2026-09-27 11:47 UTC168
            })
31ce2453c82fatagen2026-09-24 13:07 UTC169
    }
31ce2453c82fatagen2026-09-24 13:07 UTC170
5a52012c9b1datagen2026-09-24 23:45 UTC171
    /// plenty of servers won't thumbnail everything, and the file itself is the next best
5a52012c9b1datagen2026-09-24 23:45 UTC172
    /// thing. the byte cap still holds either way
5a52012c9b1datagen2026-09-24 23:45 UTC173
    async fn fetch_or_file(
5a52012c9b1datagen2026-09-24 23:45 UTC174
        &self,
5a52012c9b1datagen2026-09-24 23:45 UTC175
        request: MediaRequestParameters,
5a52012c9b1datagen2026-09-24 23:45 UTC176
        mimetype: Option<String>,
5a52012c9b1datagen2026-09-24 23:45 UTC177
    ) -> Result<Media, Error> {
5a52012c9b1datagen2026-09-24 23:45 UTC178
        if !matches!(request.format, MediaFormat::Thumbnail(_)) {
5a52012c9b1datagen2026-09-24 23:45 UTC179
            return self.fetch(request, mimetype).await;
5a52012c9b1datagen2026-09-24 23:45 UTC180
        }
5a52012c9b1datagen2026-09-24 23:45 UTC181
        match self.fetch(request.clone(), mimetype.clone()).await {
5a52012c9b1datagen2026-09-24 23:45 UTC182
            Err(error) if error.kind == ErrorKind::MediaUnavailable => {
5a52012c9b1datagen2026-09-24 23:45 UTC183
                let file = MediaRequestParameters {
5a52012c9b1datagen2026-09-24 23:45 UTC184
                    source: request.source,
5a52012c9b1datagen2026-09-24 23:45 UTC185
                    format: MediaFormat::File,
5a52012c9b1datagen2026-09-24 23:45 UTC186
                };
5a52012c9b1datagen2026-09-24 23:45 UTC187
                self.fetch(file, mimetype).await
5a52012c9b1datagen2026-09-24 23:45 UTC188
            }
5a52012c9b1datagen2026-09-24 23:45 UTC189
            result => result,
5a52012c9b1datagen2026-09-24 23:45 UTC190
        }
5a52012c9b1datagen2026-09-24 23:45 UTC191
    }
5a52012c9b1datagen2026-09-24 23:45 UTC192
31ce2453c82fatagen2026-09-24 13:07 UTC193
    async fn fetch(
31ce2453c82fatagen2026-09-24 13:07 UTC194
        &self,
31ce2453c82fatagen2026-09-24 13:07 UTC195
        request: MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC196
        mimetype: Option<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC197
    ) -> Result<Media, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC198
        let key = flight_key(&request);
31ce2453c82fatagen2026-09-24 13:07 UTC199
        let flight = {
31ce2453c82fatagen2026-09-24 13:07 UTC200
            let mut flights = self.flights.lock().await;
31ce2453c82fatagen2026-09-24 13:07 UTC201
            flights.retain(|_, flight| flight.upgrade().is_some());
31ce2453c82fatagen2026-09-24 13:07 UTC202
            match flights.get(&key).and_then(WeakShared::upgrade) {
31ce2453c82fatagen2026-09-24 13:07 UTC203
                Some(flight) => flight,
31ce2453c82fatagen2026-09-24 13:07 UTC204
                None => {
1fe480c16a3catagen2026-09-26 08:44 UTC205
                    let prepared = AssertUnwindSafe(fetch(
31ce2453c82fatagen2026-09-24 13:07 UTC206
                        self.client.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC207
                        request,
31ce2453c82fatagen2026-09-24 13:07 UTC208
                        mimetype,
31ce2453c82fatagen2026-09-24 13:07 UTC209
                        self.max_bytes,
31ce2453c82fatagen2026-09-24 13:07 UTC210
                    ))
31ce2453c82fatagen2026-09-24 13:07 UTC211
                    .catch_unwind()
31ce2453c82fatagen2026-09-24 13:07 UTC212
                    .map(|result| {
31ce2453c82fatagen2026-09-24 13:07 UTC213
                        result.unwrap_or_else(|_| {
31ce2453c82fatagen2026-09-24 13:07 UTC214
                            Err(Error::new(ErrorKind::Other, "media fetch panicked"))
31ce2453c82fatagen2026-09-24 13:07 UTC215
                        })
1fe480c16a3catagen2026-09-26 08:44 UTC216
                    });
1fe480c16a3catagen2026-09-26 08:44 UTC217
                    let boxed: FlightFuture = Box::pin(prepared);
1fe480c16a3catagen2026-09-26 08:44 UTC218
                    let flight: Flight = boxed.shared();
31ce2453c82fatagen2026-09-24 13:07 UTC219
                    if let Some(weak) = flight.downgrade() {
31ce2453c82fatagen2026-09-24 13:07 UTC220
                        flights.insert(key, weak);
31ce2453c82fatagen2026-09-24 13:07 UTC221
                    }
31ce2453c82fatagen2026-09-24 13:07 UTC222
                    flight
31ce2453c82fatagen2026-09-24 13:07 UTC223
                }
31ce2453c82fatagen2026-09-24 13:07 UTC224
            }
31ce2453c82fatagen2026-09-24 13:07 UTC225
        };
31ce2453c82fatagen2026-09-24 13:07 UTC226
        flight.await
31ce2453c82fatagen2026-09-24 13:07 UTC227
    }
31ce2453c82fatagen2026-09-24 13:07 UTC228
}
31ce2453c82fatagen2026-09-24 13:07 UTC229
5b21b919d1e6atagen2026-09-27 11:47 UTC230
struct ChosenSource {
5b21b919d1e6atagen2026-09-27 11:47 UTC231
    request: MediaRequestParameters,
5b21b919d1e6atagen2026-09-27 11:47 UTC232
    mimetype: Option<String>,
5b21b919d1e6atagen2026-09-27 11:47 UTC233
    declared_size: Option<u64>,
5b21b919d1e6atagen2026-09-27 11:47 UTC234
}
5b21b919d1e6atagen2026-09-27 11:47 UTC235
5d2974de5826atagen2026-10-08 11:29 UTC236
fn plain_registration(media_id: &MediaId, uri: &MxcUri) -> Result<MediaRegistration, Error> {
5d2974de5826atagen2026-10-08 11:29 UTC237
    if !uri.is_valid() {
5d2974de5826atagen2026-10-08 11:29 UTC238
        return Err(Error::new(ErrorKind::Invalid, "mxc uri is malformed"));
5d2974de5826atagen2026-10-08 11:29 UTC239
    }
5d2974de5826atagen2026-10-08 11:29 UTC240
    Ok(MediaRegistration {
5d2974de5826atagen2026-10-08 11:29 UTC241
        id: media_id.clone(),
5d2974de5826atagen2026-10-08 11:29 UTC242
        kind: MediaKind::Image,
5d2974de5826atagen2026-10-08 11:29 UTC243
        full: MediaSourceInfo {
5d2974de5826atagen2026-10-08 11:29 UTC244
            source: MediaSource::Plain(uri.to_owned()),
5d2974de5826atagen2026-10-08 11:29 UTC245
            mimetype: None,
5d2974de5826atagen2026-10-08 11:29 UTC246
            declared_size: None,
5d2974de5826atagen2026-10-08 11:29 UTC247
        },
5d2974de5826atagen2026-10-08 11:29 UTC248
        thumbnail: None,
5d2974de5826atagen2026-10-08 11:29 UTC249
    })
5d2974de5826atagen2026-10-08 11:29 UTC250
}
5d2974de5826atagen2026-10-08 11:29 UTC251
5b21b919d1e6atagen2026-09-27 11:47 UTC252
fn choose_source(media: MediaRegistration, size: MediaSize) -> Result<ChosenSource, Error> {
5b21b919d1e6atagen2026-09-27 11:47 UTC253
    let (source, format, mimetype, declared_size) = match size {
5b21b919d1e6atagen2026-09-27 11:47 UTC254
        MediaSize::Full => (
5b21b919d1e6atagen2026-09-27 11:47 UTC255
            media.full.source,
5b21b919d1e6atagen2026-09-27 11:47 UTC256
            MediaFormat::File,
5b21b919d1e6atagen2026-09-27 11:47 UTC257
            media.full.mimetype,
5b21b919d1e6atagen2026-09-27 11:47 UTC258
            media.full.declared_size,
5b21b919d1e6atagen2026-09-27 11:47 UTC259
        ),
5b21b919d1e6atagen2026-09-27 11:47 UTC260
        MediaSize::Thumbnail { width, height } => match media.thumbnail {
5b21b919d1e6atagen2026-09-27 11:47 UTC261
            Some(thumbnail) => (
5b21b919d1e6atagen2026-09-27 11:47 UTC262
                thumbnail.source,
5b21b919d1e6atagen2026-09-27 11:47 UTC263
                MediaFormat::File,
5b21b919d1e6atagen2026-09-27 11:47 UTC264
                thumbnail.mimetype,
5b21b919d1e6atagen2026-09-27 11:47 UTC265
                thumbnail.declared_size,
5b21b919d1e6atagen2026-09-27 11:47 UTC266
            ),
5b21b919d1e6atagen2026-09-27 11:47 UTC267
            // the server can only resize what it can read
5b21b919d1e6atagen2026-09-27 11:47 UTC268
            None if matches!(media.kind, MediaKind::Image | MediaKind::Video)
5b21b919d1e6atagen2026-09-27 11:47 UTC269
                && matches!(media.full.source, MediaSource::Plain(_)) =>
5b21b919d1e6atagen2026-09-27 11:47 UTC270
            {
5b21b919d1e6atagen2026-09-27 11:47 UTC271
                // the server picks the thumbnail's type, so the original's would be a lie
5b21b919d1e6atagen2026-09-27 11:47 UTC272
                let format = if is_local(&media.full.source) {
5b21b919d1e6atagen2026-09-27 11:47 UTC273
                    MediaFormat::File
5b21b919d1e6atagen2026-09-27 11:47 UTC274
                } else {
5b21b919d1e6atagen2026-09-27 11:47 UTC275
                    thumbnail_format(width, height)
5b21b919d1e6atagen2026-09-27 11:47 UTC276
                };
5b21b919d1e6atagen2026-09-27 11:47 UTC277
                (media.full.source, format, None, None)
5b21b919d1e6atagen2026-09-27 11:47 UTC278
            }
5b21b919d1e6atagen2026-09-27 11:47 UTC279
            None => {
5b21b919d1e6atagen2026-09-27 11:47 UTC280
                return Err(Error::new(
5b21b919d1e6atagen2026-09-27 11:47 UTC281
                    ErrorKind::MediaUnavailable,
5b21b919d1e6atagen2026-09-27 11:47 UTC282
                    "the requested media size is unavailable",
5b21b919d1e6atagen2026-09-27 11:47 UTC283
                ));
5b21b919d1e6atagen2026-09-27 11:47 UTC284
            }
5b21b919d1e6atagen2026-09-27 11:47 UTC285
        },
5b21b919d1e6atagen2026-09-27 11:47 UTC286
    };
5b21b919d1e6atagen2026-09-27 11:47 UTC287
    Ok(ChosenSource {
5b21b919d1e6atagen2026-09-27 11:47 UTC288
        request: MediaRequestParameters { source, format },
5b21b919d1e6atagen2026-09-27 11:47 UTC289
        mimetype,
5b21b919d1e6atagen2026-09-27 11:47 UTC290
        declared_size,
5b21b919d1e6atagen2026-09-27 11:47 UTC291
    })
5b21b919d1e6atagen2026-09-27 11:47 UTC292
}
5b21b919d1e6atagen2026-09-27 11:47 UTC293
5b21b919d1e6atagen2026-09-27 11:47 UTC294
async fn media_file(
5b21b919d1e6atagen2026-09-27 11:47 UTC295
    client: &Client,
5b21b919d1e6atagen2026-09-27 11:47 UTC296
    request: MediaRequestParameters,
5b21b919d1e6atagen2026-09-27 11:47 UTC297
    mimetype: Option<String>,
5b21b919d1e6atagen2026-09-27 11:47 UTC298
    declared_size: Option<u64>,
5b21b919d1e6atagen2026-09-27 11:47 UTC299
) -> Result<MediaFile, Error> {
5b21b919d1e6atagen2026-09-27 11:47 UTC300
    let url = media_http_request(client, &request)
5b21b919d1e6atagen2026-09-27 11:47 UTC301
        .await?
5b21b919d1e6atagen2026-09-27 11:47 UTC302
        .uri()
5b21b919d1e6atagen2026-09-27 11:47 UTC303
        .to_string();
5b21b919d1e6atagen2026-09-27 11:47 UTC304
    let encryption = match &request.source {
5b21b919d1e6atagen2026-09-27 11:47 UTC305
        MediaSource::Plain(_) => None,
5b21b919d1e6atagen2026-09-27 11:47 UTC306
        MediaSource::Encrypted(file) => {
5b21b919d1e6atagen2026-09-27 11:47 UTC307
            let (EncryptedFileInfo::V2(info), Some(EncryptedFileHash::Sha256(sha256))) = (
5b21b919d1e6atagen2026-09-27 11:47 UTC308
                &file.info,
5b21b919d1e6atagen2026-09-27 11:47 UTC309
                file.hashes.get(&EncryptedFileHashAlgorithm::Sha256),
5b21b919d1e6atagen2026-09-27 11:47 UTC310
            ) else {
5b21b919d1e6atagen2026-09-27 11:47 UTC311
                return Err(Error::new(
5b21b919d1e6atagen2026-09-27 11:47 UTC312
                    ErrorKind::MediaUnavailable,
5b21b919d1e6atagen2026-09-27 11:47 UTC313
                    "encrypted media is not v2 with a sha256 hash",
5b21b919d1e6atagen2026-09-27 11:47 UTC314
                ));
5b21b919d1e6atagen2026-09-27 11:47 UTC315
            };
5b21b919d1e6atagen2026-09-27 11:47 UTC316
            Some(AttachmentEncryption {
5b21b919d1e6atagen2026-09-27 11:47 UTC317
                key: info.k.encode(),
5b21b919d1e6atagen2026-09-27 11:47 UTC318
                iv: info.iv.encode(),
5b21b919d1e6atagen2026-09-27 11:47 UTC319
                sha256: sha256.encode(),
5b21b919d1e6atagen2026-09-27 11:47 UTC320
            })
5b21b919d1e6atagen2026-09-27 11:47 UTC321
        }
5b21b919d1e6atagen2026-09-27 11:47 UTC322
    };
5b21b919d1e6atagen2026-09-27 11:47 UTC323
    Ok(MediaFile {
5b21b919d1e6atagen2026-09-27 11:47 UTC324
        url,
5b21b919d1e6atagen2026-09-27 11:47 UTC325
        mimetype,
5b21b919d1e6atagen2026-09-27 11:47 UTC326
        declared_size,
5b21b919d1e6atagen2026-09-27 11:47 UTC327
        encryption,
5b21b919d1e6atagen2026-09-27 11:47 UTC328
    })
5b21b919d1e6atagen2026-09-27 11:47 UTC329
}
5b21b919d1e6atagen2026-09-27 11:47 UTC330
f137a3c74d11atagen2026-10-24 09:44 UTC331
/// servers without spec v1.11 animated thumbnails ignore the flag and send a still frame
31ce2453c82fatagen2026-09-24 13:07 UTC332
fn thumbnail_format(width: u32, height: u32) -> MediaFormat {
f137a3c74d11atagen2026-10-24 09:44 UTC333
    let mut settings = MediaThumbnailSettings::new(width.into(), height.into());
f137a3c74d11atagen2026-10-24 09:44 UTC334
    settings.animated = true;
f137a3c74d11atagen2026-10-24 09:44 UTC335
    MediaFormat::Thumbnail(settings)
31ce2453c82fatagen2026-09-24 13:07 UTC336
}
31ce2453c82fatagen2026-09-24 13:07 UTC337
5a52012c9b1datagen2026-09-24 23:45 UTC338
fn is_local(source: &MediaSource) -> bool {
5a52012c9b1datagen2026-09-24 23:45 UTC339
    let uri = match source {
5a52012c9b1datagen2026-09-24 23:45 UTC340
        MediaSource::Plain(uri) => uri,
5a52012c9b1datagen2026-09-24 23:45 UTC341
        MediaSource::Encrypted(file) => &file.url,
5a52012c9b1datagen2026-09-24 23:45 UTC342
    };
5a52012c9b1datagen2026-09-24 23:45 UTC343
    uri.server_name()
5a52012c9b1datagen2026-09-24 23:45 UTC344
        .is_ok_and(|server| server.as_str() == "send-queue.localhost")
5a52012c9b1datagen2026-09-24 23:45 UTC345
}
5a52012c9b1datagen2026-09-24 23:45 UTC346
5a52012c9b1datagen2026-09-24 23:45 UTC347
/// plain and encrypted sources share a cache key in the sdk, but must never share a flight
31ce2453c82fatagen2026-09-24 13:07 UTC348
fn flight_key(request: &MediaRequestParameters) -> String {
31ce2453c82fatagen2026-09-24 13:07 UTC349
    let uri = match &request.source {
5a52012c9b1datagen2026-09-24 23:45 UTC350
        MediaSource::Plain(uri) => format!("plain|{uri}"),
5a52012c9b1datagen2026-09-24 23:45 UTC351
        MediaSource::Encrypted(file) => format!("encrypted|{}", file.url),
31ce2453c82fatagen2026-09-24 13:07 UTC352
    };
31ce2453c82fatagen2026-09-24 13:07 UTC353
    match &request.format {
31ce2453c82fatagen2026-09-24 13:07 UTC354
        MediaFormat::File => uri.to_owned(),
31ce2453c82fatagen2026-09-24 13:07 UTC355
        MediaFormat::Thumbnail(settings) => format!(
31ce2453c82fatagen2026-09-24 13:07 UTC356
            "{uri}|{}|{}x{}|{}",
31ce2453c82fatagen2026-09-24 13:07 UTC357
            settings.method, settings.width, settings.height, settings.animated
31ce2453c82fatagen2026-09-24 13:07 UTC358
        ),
31ce2453c82fatagen2026-09-24 13:07 UTC359
    }
31ce2453c82fatagen2026-09-24 13:07 UTC360
}
31ce2453c82fatagen2026-09-24 13:07 UTC361
31ce2453c82fatagen2026-09-24 13:07 UTC362
fn too_large(message: &'static str) -> Error {
31ce2453c82fatagen2026-09-24 13:07 UTC363
    Error::new(ErrorKind::MediaTooLarge, message)
31ce2453c82fatagen2026-09-24 13:07 UTC364
}
31ce2453c82fatagen2026-09-24 13:07 UTC365
fa51490b6ae9atagen2026-10-24 08:41 UTC366
pub(crate) async fn download_file(
fa51490b6ae9atagen2026-10-24 08:41 UTC367
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC368
    source: MediaSource,
fa51490b6ae9atagen2026-10-24 08:41 UTC369
    mimetype: Option<String>,
fa51490b6ae9atagen2026-10-24 08:41 UTC370
    max_bytes: usize,
fa51490b6ae9atagen2026-10-24 08:41 UTC371
) -> Result<Media, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC372
    let request = MediaRequestParameters {
fa51490b6ae9atagen2026-10-24 08:41 UTC373
        source,
fa51490b6ae9atagen2026-10-24 08:41 UTC374
        format: MediaFormat::File,
fa51490b6ae9atagen2026-10-24 08:41 UTC375
    };
fa51490b6ae9atagen2026-10-24 08:41 UTC376
    fetch(client.clone(), request, mimetype, max_bytes).await
fa51490b6ae9atagen2026-10-24 08:41 UTC377
}
fa51490b6ae9atagen2026-10-24 08:41 UTC378
31ce2453c82fatagen2026-09-24 13:07 UTC379
async fn fetch(
31ce2453c82fatagen2026-09-24 13:07 UTC380
    client: Client,
31ce2453c82fatagen2026-09-24 13:07 UTC381
    request: MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC382
    mimetype: Option<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC383
    max_bytes: usize,
31ce2453c82fatagen2026-09-24 13:07 UTC384
) -> Result<Media, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC385
    if let Some(cached) = cached_media(&client, &request).await? {
31ce2453c82fatagen2026-09-24 13:07 UTC386
        if cached.len() > max_bytes {
31ce2453c82fatagen2026-09-24 13:07 UTC387
            if let Ok(store) = client.media_store().lock().await {
31ce2453c82fatagen2026-09-24 13:07 UTC388
                let _ = store.remove_media_content(&request).await;
31ce2453c82fatagen2026-09-24 13:07 UTC389
            }
31ce2453c82fatagen2026-09-24 13:07 UTC390
            return Err(too_large("cached media exceeds the configured byte limit"));
31ce2453c82fatagen2026-09-24 13:07 UTC391
        }
5a52012c9b1datagen2026-09-24 23:45 UTC392
        // plain and encrypted share a cache key, so a plain fetch of an encrypted file's uri
5a52012c9b1datagen2026-09-24 23:45 UTC393
        // leaves ciphertext here. its hash gives it away
5a52012c9b1datagen2026-09-24 23:45 UTC394
        if is_ciphertext(&cached, &request.source) {
5a52012c9b1datagen2026-09-24 23:45 UTC395
            let bytes = maybe_decrypt(cached, &request, max_bytes)?;
5a52012c9b1datagen2026-09-24 23:45 UTC396
            cache(&client, &request, &bytes).await;
5a52012c9b1datagen2026-09-24 23:45 UTC397
            return Ok(media(bytes, mimetype));
5a52012c9b1datagen2026-09-24 23:45 UTC398
        }
31ce2453c82fatagen2026-09-24 13:07 UTC399
        return Ok(media(cached, mimetype));
31ce2453c82fatagen2026-09-24 13:07 UTC400
    }
31ce2453c82fatagen2026-09-24 13:07 UTC401
    let (bytes, served_mimetype) = download_bounded(&client, &request, max_bytes).await?;
31ce2453c82fatagen2026-09-24 13:07 UTC402
    // the sdk caches plaintext under the encrypted key, so we do too or nobody can read anything
31ce2453c82fatagen2026-09-24 13:07 UTC403
    let bytes = maybe_decrypt(bytes, &request, max_bytes)?;
5a52012c9b1datagen2026-09-24 23:45 UTC404
    cache(&client, &request, &bytes).await;
5a52012c9b1datagen2026-09-24 23:45 UTC405
    Ok(media(bytes, mimetype.or(served_mimetype)))
5a52012c9b1datagen2026-09-24 23:45 UTC406
}
5a52012c9b1datagen2026-09-24 23:45 UTC407
5a52012c9b1datagen2026-09-24 23:45 UTC408
async fn cache(client: &Client, request: &MediaRequestParameters, bytes: &[u8]) {
31ce2453c82fatagen2026-09-24 13:07 UTC409
    if let Ok(store) = client.media_store().lock().await {
31ce2453c82fatagen2026-09-24 13:07 UTC410
        let _ = store
31ce2453c82fatagen2026-09-24 13:07 UTC411
            .add_media_content(
5a52012c9b1datagen2026-09-24 23:45 UTC412
                request,
5a52012c9b1datagen2026-09-24 23:45 UTC413
                bytes.to_vec(),
31ce2453c82fatagen2026-09-24 13:07 UTC414
                matrix_sdk::media::store::IgnoreMediaRetentionPolicy::No,
31ce2453c82fatagen2026-09-24 13:07 UTC415
            )
31ce2453c82fatagen2026-09-24 13:07 UTC416
            .await;
31ce2453c82fatagen2026-09-24 13:07 UTC417
    }
5a52012c9b1datagen2026-09-24 23:45 UTC418
}
5a52012c9b1datagen2026-09-24 23:45 UTC419
5a52012c9b1datagen2026-09-24 23:45 UTC420
fn is_ciphertext(bytes: &[u8], source: &MediaSource) -> bool {
5a52012c9b1datagen2026-09-24 23:45 UTC421
    use sha2::{Digest, Sha256};
5a52012c9b1datagen2026-09-24 23:45 UTC422
    let MediaSource::Encrypted(file) = source else {
5a52012c9b1datagen2026-09-24 23:45 UTC423
        return false;
5a52012c9b1datagen2026-09-24 23:45 UTC424
    };
5a52012c9b1datagen2026-09-24 23:45 UTC425
    file.hashes
5a52012c9b1datagen2026-09-24 23:45 UTC426
        .get(&ruma::events::room::EncryptedFileHashAlgorithm::Sha256)
5a52012c9b1datagen2026-09-24 23:45 UTC427
        .is_some_and(|hash| hash.as_bytes() == Sha256::digest(bytes).as_slice())
31ce2453c82fatagen2026-09-24 13:07 UTC428
}
31ce2453c82fatagen2026-09-24 13:07 UTC429
31ce2453c82fatagen2026-09-24 13:07 UTC430
fn media(bytes: Vec<u8>, mimetype: Option<String>) -> Media {
31ce2453c82fatagen2026-09-24 13:07 UTC431
    let mimetype = mimetype
31ce2453c82fatagen2026-09-24 13:07 UTC432
        .or_else(|| sniff_image(&bytes).map(str::to_owned))
31ce2453c82fatagen2026-09-24 13:07 UTC433
        .unwrap_or_else(|| "application/octet-stream".to_owned());
31ce2453c82fatagen2026-09-24 13:07 UTC434
    Media {
31ce2453c82fatagen2026-09-24 13:07 UTC435
        bytes: Arc::from(bytes),
31ce2453c82fatagen2026-09-24 13:07 UTC436
        mimetype,
31ce2453c82fatagen2026-09-24 13:07 UTC437
    }
31ce2453c82fatagen2026-09-24 13:07 UTC438
}
31ce2453c82fatagen2026-09-24 13:07 UTC439
31ce2453c82fatagen2026-09-24 13:07 UTC440
/// the cache forgets content types, and avatars never declare one. images are the common case
31ce2453c82fatagen2026-09-24 13:07 UTC441
fn sniff_image(bytes: &[u8]) -> Option<&'static str> {
31ce2453c82fatagen2026-09-24 13:07 UTC442
    match bytes {
31ce2453c82fatagen2026-09-24 13:07 UTC443
        [0x89, b'P', b'N', b'G', ..] => Some("image/png"),
31ce2453c82fatagen2026-09-24 13:07 UTC444
        [0xff, 0xd8, 0xff, ..] => Some("image/jpeg"),
31ce2453c82fatagen2026-09-24 13:07 UTC445
        [b'G', b'I', b'F', b'8', ..] => Some("image/gif"),
31ce2453c82fatagen2026-09-24 13:07 UTC446
        [
31ce2453c82fatagen2026-09-24 13:07 UTC447
            b'R',
31ce2453c82fatagen2026-09-24 13:07 UTC448
            b'I',
31ce2453c82fatagen2026-09-24 13:07 UTC449
            b'F',
31ce2453c82fatagen2026-09-24 13:07 UTC450
            b'F',
31ce2453c82fatagen2026-09-24 13:07 UTC451
            _,
31ce2453c82fatagen2026-09-24 13:07 UTC452
            _,
31ce2453c82fatagen2026-09-24 13:07 UTC453
            _,
31ce2453c82fatagen2026-09-24 13:07 UTC454
            _,
31ce2453c82fatagen2026-09-24 13:07 UTC455
            b'W',
31ce2453c82fatagen2026-09-24 13:07 UTC456
            b'E',
31ce2453c82fatagen2026-09-24 13:07 UTC457
            b'B',
31ce2453c82fatagen2026-09-24 13:07 UTC458
            b'P',
31ce2453c82fatagen2026-09-24 13:07 UTC459
            ..,
31ce2453c82fatagen2026-09-24 13:07 UTC460
        ] => Some("image/webp"),
31ce2453c82fatagen2026-09-24 13:07 UTC461
        _ => None,
31ce2453c82fatagen2026-09-24 13:07 UTC462
    }
31ce2453c82fatagen2026-09-24 13:07 UTC463
}
31ce2453c82fatagen2026-09-24 13:07 UTC464
31ce2453c82fatagen2026-09-24 13:07 UTC465
async fn cached_media(
31ce2453c82fatagen2026-09-24 13:07 UTC466
    client: &Client,
31ce2453c82fatagen2026-09-24 13:07 UTC467
    request: &MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC468
) -> Result<Option<Vec<u8>>, Error> {
1fe480c16a3catagen2026-09-26 08:44 UTC469
    fn cache_failed(
1fe480c16a3catagen2026-09-26 08:44 UTC470
        source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
1fe480c16a3catagen2026-09-26 08:44 UTC471
    ) -> Error {
31ce2453c82fatagen2026-09-24 13:07 UTC472
        Error::with_source(ErrorKind::Store, "reading media cache failed", source)
31ce2453c82fatagen2026-09-24 13:07 UTC473
    }
31ce2453c82fatagen2026-09-24 13:07 UTC474
    client
31ce2453c82fatagen2026-09-24 13:07 UTC475
        .media_store()
31ce2453c82fatagen2026-09-24 13:07 UTC476
        .lock()
31ce2453c82fatagen2026-09-24 13:07 UTC477
        .await
31ce2453c82fatagen2026-09-24 13:07 UTC478
        .map_err(cache_failed)?
31ce2453c82fatagen2026-09-24 13:07 UTC479
        .get_media_content(request)
31ce2453c82fatagen2026-09-24 13:07 UTC480
        .await
31ce2453c82fatagen2026-09-24 13:07 UTC481
        .map_err(cache_failed)
31ce2453c82fatagen2026-09-24 13:07 UTC482
}
31ce2453c82fatagen2026-09-24 13:07 UTC483
31ce2453c82fatagen2026-09-24 13:07 UTC484
fn maybe_decrypt(
31ce2453c82fatagen2026-09-24 13:07 UTC485
    bytes: Vec<u8>,
31ce2453c82fatagen2026-09-24 13:07 UTC486
    request: &MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC487
    max_bytes: usize,
31ce2453c82fatagen2026-09-24 13:07 UTC488
) -> Result<Vec<u8>, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC489
    let MediaSource::Encrypted(file) = &request.source else {
31ce2453c82fatagen2026-09-24 13:07 UTC490
        return Ok(bytes);
31ce2453c82fatagen2026-09-24 13:07 UTC491
    };
1fe480c16a3catagen2026-09-26 08:44 UTC492
    fn decrypt_failed(
1fe480c16a3catagen2026-09-26 08:44 UTC493
        source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
1fe480c16a3catagen2026-09-26 08:44 UTC494
    ) -> Error {
31ce2453c82fatagen2026-09-24 13:07 UTC495
        Error::with_source(
31ce2453c82fatagen2026-09-24 13:07 UTC496
            ErrorKind::MediaUnavailable,
31ce2453c82fatagen2026-09-24 13:07 UTC497
            "media decryption failed",
31ce2453c82fatagen2026-09-24 13:07 UTC498
            source,
31ce2453c82fatagen2026-09-24 13:07 UTC499
        )
31ce2453c82fatagen2026-09-24 13:07 UTC500
    }
31ce2453c82fatagen2026-09-24 13:07 UTC501
    let info: MediaEncryptionInfo = (**file).clone().into();
31ce2453c82fatagen2026-09-24 13:07 UTC502
    let mut cursor = std::io::Cursor::new(bytes);
31ce2453c82fatagen2026-09-24 13:07 UTC503
    let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).map_err(decrypt_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC504
    let mut decrypted = Vec::new();
31ce2453c82fatagen2026-09-24 13:07 UTC505
    let mut chunk = [0_u8; 8192];
31ce2453c82fatagen2026-09-24 13:07 UTC506
    loop {
31ce2453c82fatagen2026-09-24 13:07 UTC507
        let read = decryptor.read(&mut chunk).map_err(decrypt_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC508
        if read == 0 {
31ce2453c82fatagen2026-09-24 13:07 UTC509
            return Ok(decrypted);
31ce2453c82fatagen2026-09-24 13:07 UTC510
        }
31ce2453c82fatagen2026-09-24 13:07 UTC511
        if read > max_bytes.saturating_sub(decrypted.len()) {
31ce2453c82fatagen2026-09-24 13:07 UTC512
            return Err(too_large(
31ce2453c82fatagen2026-09-24 13:07 UTC513
                "decrypted media exceeds the configured byte limit",
31ce2453c82fatagen2026-09-24 13:07 UTC514
            ));
31ce2453c82fatagen2026-09-24 13:07 UTC515
        }
31ce2453c82fatagen2026-09-24 13:07 UTC516
        decrypted.extend_from_slice(&chunk[..read]);
31ce2453c82fatagen2026-09-24 13:07 UTC517
    }
31ce2453c82fatagen2026-09-24 13:07 UTC518
}
31ce2453c82fatagen2026-09-24 13:07 UTC519
5a52012c9b1datagen2026-09-24 23:45 UTC520
/// the sdk's own download has no size cap, so we do it by hand
31ce2453c82fatagen2026-09-24 13:07 UTC521
async fn download_bounded(
31ce2453c82fatagen2026-09-24 13:07 UTC522
    client: &Client,
31ce2453c82fatagen2026-09-24 13:07 UTC523
    request: &MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC524
    max_bytes: usize,
31ce2453c82fatagen2026-09-24 13:07 UTC525
) -> Result<(Vec<u8>, Option<String>), Error> {
5a52012c9b1datagen2026-09-24 23:45 UTC526
    if is_local(&request.source) {
31ce2453c82fatagen2026-09-24 13:07 UTC527
        return Err(Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC528
            ErrorKind::MediaUnavailable,
31ce2453c82fatagen2026-09-24 13:07 UTC529
            "local media is not in the media cache",
31ce2453c82fatagen2026-09-24 13:07 UTC530
        ));
31ce2453c82fatagen2026-09-24 13:07 UTC531
    }
31ce2453c82fatagen2026-09-24 13:07 UTC532
    let network_failed =
31ce2453c82fatagen2026-09-24 13:07 UTC533
        |source| Error::with_source(ErrorKind::Network, "media download failed", source);
31ce2453c82fatagen2026-09-24 13:07 UTC534
    let mut refreshed = false;
31ce2453c82fatagen2026-09-24 13:07 UTC535
    let response = loop {
31ce2453c82fatagen2026-09-24 13:07 UTC536
        let http_request = media_http_request(client, request).await?;
31ce2453c82fatagen2026-09-24 13:07 UTC537
        let (parts, _) = http_request.into_parts();
5a52012c9b1datagen2026-09-24 23:45 UTC538
        let mut builder = client
5a52012c9b1datagen2026-09-24 23:45 UTC539
            .http_client()
5a52012c9b1datagen2026-09-24 23:45 UTC540
            .get(parts.uri.to_string())
5a52012c9b1datagen2026-09-24 23:45 UTC541
            .timeout(DOWNLOAD_TOTAL);
31ce2453c82fatagen2026-09-24 13:07 UTC542
        for (name, value) in &parts.headers {
31ce2453c82fatagen2026-09-24 13:07 UTC543
            builder = builder.header(name.as_str(), value.as_bytes());
31ce2453c82fatagen2026-09-24 13:07 UTC544
        }
31ce2453c82fatagen2026-09-24 13:07 UTC545
        let response = builder.send().await.map_err(network_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC546
        if response.status() != matrix_sdk::reqwest::StatusCode::UNAUTHORIZED {
31ce2453c82fatagen2026-09-24 13:07 UTC547
            break response;
31ce2453c82fatagen2026-09-24 13:07 UTC548
        }
31ce2453c82fatagen2026-09-24 13:07 UTC549
        if refreshed
31ce2453c82fatagen2026-09-24 13:07 UTC550
            || client
31ce2453c82fatagen2026-09-24 13:07 UTC551
                .session_tokens()
31ce2453c82fatagen2026-09-24 13:07 UTC552
                .is_none_or(|tokens| tokens.refresh_token.is_none())
31ce2453c82fatagen2026-09-24 13:07 UTC553
        {
31ce2453c82fatagen2026-09-24 13:07 UTC554
            return Err(Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC555
                ErrorKind::Unauthenticated,
31ce2453c82fatagen2026-09-24 13:07 UTC556
                "media download was not authorized",
31ce2453c82fatagen2026-09-24 13:07 UTC557
            ));
31ce2453c82fatagen2026-09-24 13:07 UTC558
        }
31ce2453c82fatagen2026-09-24 13:07 UTC559
        client.refresh_access_token().await.map_err(|source| {
31ce2453c82fatagen2026-09-24 13:07 UTC560
            Error::from_refresh_token("refreshing media access token failed", source)
31ce2453c82fatagen2026-09-24 13:07 UTC561
        })?;
31ce2453c82fatagen2026-09-24 13:07 UTC562
        refreshed = true;
31ce2453c82fatagen2026-09-24 13:07 UTC563
    };
31ce2453c82fatagen2026-09-24 13:07 UTC564
    let status = response.status();
31ce2453c82fatagen2026-09-24 13:07 UTC565
    if !status.is_success() {
31ce2453c82fatagen2026-09-24 13:07 UTC566
        return Err(Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC567
            ErrorKind::MediaUnavailable,
31ce2453c82fatagen2026-09-24 13:07 UTC568
            format!("media download failed with status {status}"),
31ce2453c82fatagen2026-09-24 13:07 UTC569
        ));
31ce2453c82fatagen2026-09-24 13:07 UTC570
    }
31ce2453c82fatagen2026-09-24 13:07 UTC571
    if response
31ce2453c82fatagen2026-09-24 13:07 UTC572
        .content_length()
31ce2453c82fatagen2026-09-24 13:07 UTC573
        .is_some_and(|length| length > max_bytes as u64)
31ce2453c82fatagen2026-09-24 13:07 UTC574
    {
31ce2453c82fatagen2026-09-24 13:07 UTC575
        return Err(too_large("media exceeds the configured byte limit"));
31ce2453c82fatagen2026-09-24 13:07 UTC576
    }
31ce2453c82fatagen2026-09-24 13:07 UTC577
    let mimetype = response
31ce2453c82fatagen2026-09-24 13:07 UTC578
        .headers()
31ce2453c82fatagen2026-09-24 13:07 UTC579
        .get(http::header::CONTENT_TYPE)
31ce2453c82fatagen2026-09-24 13:07 UTC580
        .and_then(|value| value.to_str().ok())
31ce2453c82fatagen2026-09-24 13:07 UTC581
        .map(str::to_owned);
31ce2453c82fatagen2026-09-24 13:07 UTC582
    let mut stream = response.bytes_stream();
31ce2453c82fatagen2026-09-24 13:07 UTC583
    let mut bytes = Vec::new();
5a52012c9b1datagen2026-09-24 23:45 UTC584
    loop {
1fe480c16a3catagen2026-09-26 08:44 UTC585
        let chunk = matrix_sdk::timeout::timeout(stream.next(), DOWNLOAD_IDLE)
5a52012c9b1datagen2026-09-24 23:45 UTC586
            .await
5a52012c9b1datagen2026-09-24 23:45 UTC587
            .map_err(|_| Error::new(ErrorKind::Network, "media download stalled"))?;
5a52012c9b1datagen2026-09-24 23:45 UTC588
        let Some(chunk) = chunk else { break };
31ce2453c82fatagen2026-09-24 13:07 UTC589
        let chunk = chunk.map_err(network_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC590
        if chunk.len() > max_bytes.saturating_sub(bytes.len()) {
31ce2453c82fatagen2026-09-24 13:07 UTC591
            return Err(too_large(
31ce2453c82fatagen2026-09-24 13:07 UTC592
                "downloaded media exceeds the configured byte limit",
31ce2453c82fatagen2026-09-24 13:07 UTC593
            ));
31ce2453c82fatagen2026-09-24 13:07 UTC594
        }
31ce2453c82fatagen2026-09-24 13:07 UTC595
        bytes.extend_from_slice(&chunk);
31ce2453c82fatagen2026-09-24 13:07 UTC596
    }
31ce2453c82fatagen2026-09-24 13:07 UTC597
    Ok((bytes, mimetype))
31ce2453c82fatagen2026-09-24 13:07 UTC598
}
31ce2453c82fatagen2026-09-24 13:07 UTC599
31ce2453c82fatagen2026-09-24 13:07 UTC600
async fn media_http_request(
31ce2453c82fatagen2026-09-24 13:07 UTC601
    client: &Client,
31ce2453c82fatagen2026-09-24 13:07 UTC602
    request: &MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC603
) -> Result<http::Request<Vec<u8>>, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC604
    let versions = client.supported_versions().await.map_err(|source| {
31ce2453c82fatagen2026-09-24 13:07 UTC605
        Error::with_source(
31ce2453c82fatagen2026-09-24 13:07 UTC606
            ErrorKind::Network,
31ce2453c82fatagen2026-09-24 13:07 UTC607
            "reading supported server versions failed",
31ce2453c82fatagen2026-09-24 13:07 UTC608
            source,
31ce2453c82fatagen2026-09-24 13:07 UTC609
        )
31ce2453c82fatagen2026-09-24 13:07 UTC610
    })?;
31ce2453c82fatagen2026-09-24 13:07 UTC611
    let authenticated =
31ce2453c82fatagen2026-09-24 13:07 UTC612
        authenticated_media::get_content::v1::Request::PATH_BUILDER.is_supported(&versions);
31ce2453c82fatagen2026-09-24 13:07 UTC613
    let base = client.homeserver().to_string();
31ce2453c82fatagen2026-09-24 13:07 UTC614
    let token = client.access_token();
31ce2453c82fatagen2026-09-24 13:07 UTC615
    let invalid_uri =
31ce2453c82fatagen2026-09-24 13:07 UTC616
        |source| Error::with_source(ErrorKind::Invalid, "media URI is invalid", source);
31ce2453c82fatagen2026-09-24 13:07 UTC617
    let (uri, thumbnail) = match (&request.source, &request.format) {
31ce2453c82fatagen2026-09-24 13:07 UTC618
        (MediaSource::Encrypted(file), _) => (&file.url, None),
31ce2453c82fatagen2026-09-24 13:07 UTC619
        (MediaSource::Plain(uri), MediaFormat::File) => (uri, None),
31ce2453c82fatagen2026-09-24 13:07 UTC620
        (MediaSource::Plain(uri), MediaFormat::Thumbnail(settings)) => (uri, Some(settings)),
31ce2453c82fatagen2026-09-24 13:07 UTC621
    };
31ce2453c82fatagen2026-09-24 13:07 UTC622
    // each endpoint has its own auth input type, so no generic helper can take them all
31ce2453c82fatagen2026-09-24 13:07 UTC623
    macro_rules! into_http {
31ce2453c82fatagen2026-09-24 13:07 UTC624
        ($endpoint:expr, $token:expr) => {
31ce2453c82fatagen2026-09-24 13:07 UTC625
            $endpoint
31ce2453c82fatagen2026-09-24 13:07 UTC626
                .try_into_http_request::<Vec<u8>>(&base, $token, Cow::Borrowed(&versions))
31ce2453c82fatagen2026-09-24 13:07 UTC627
                .map_err(|source| {
31ce2453c82fatagen2026-09-24 13:07 UTC628
                    Error::with_source(ErrorKind::Other, "media request is invalid", source)
31ce2453c82fatagen2026-09-24 13:07 UTC629
                })?
31ce2453c82fatagen2026-09-24 13:07 UTC630
        };
31ce2453c82fatagen2026-09-24 13:07 UTC631
    }
31ce2453c82fatagen2026-09-24 13:07 UTC632
    let authed = SendAccessToken::IfRequired(token.as_deref().unwrap_or(""));
31ce2453c82fatagen2026-09-24 13:07 UTC633
    #[allow(deprecated)]
31ce2453c82fatagen2026-09-24 13:07 UTC634
    let mut http_request = match (thumbnail, authenticated) {
31ce2453c82fatagen2026-09-24 13:07 UTC635
        (None, true) => into_http!(
31ce2453c82fatagen2026-09-24 13:07 UTC636
            authenticated_media::get_content::v1::Request::from_uri(uri).map_err(invalid_uri)?,
31ce2453c82fatagen2026-09-24 13:07 UTC637
            authed
31ce2453c82fatagen2026-09-24 13:07 UTC638
        ),
31ce2453c82fatagen2026-09-24 13:07 UTC639
        (None, false) => into_http!(
31ce2453c82fatagen2026-09-24 13:07 UTC640
            media::get_content::v3::Request::from_url(uri).map_err(invalid_uri)?,
31ce2453c82fatagen2026-09-24 13:07 UTC641
            SendAccessToken::None
31ce2453c82fatagen2026-09-24 13:07 UTC642
        ),
31ce2453c82fatagen2026-09-24 13:07 UTC643
        (Some(settings), true) => {
31ce2453c82fatagen2026-09-24 13:07 UTC644
            let mut endpoint = authenticated_media::get_content_thumbnail::v1::Request::from_uri(
31ce2453c82fatagen2026-09-24 13:07 UTC645
                uri,
31ce2453c82fatagen2026-09-24 13:07 UTC646
                settings.width,
31ce2453c82fatagen2026-09-24 13:07 UTC647
                settings.height,
31ce2453c82fatagen2026-09-24 13:07 UTC648
            )
31ce2453c82fatagen2026-09-24 13:07 UTC649
            .map_err(invalid_uri)?;
31ce2453c82fatagen2026-09-24 13:07 UTC650
            endpoint.method = Some(settings.method.clone());
31ce2453c82fatagen2026-09-24 13:07 UTC651
            endpoint.animated = Some(settings.animated);
31ce2453c82fatagen2026-09-24 13:07 UTC652
            into_http!(endpoint, authed)
31ce2453c82fatagen2026-09-24 13:07 UTC653
        }
31ce2453c82fatagen2026-09-24 13:07 UTC654
        (Some(settings), false) => {
31ce2453c82fatagen2026-09-24 13:07 UTC655
            let mut endpoint = media::get_content_thumbnail::v3::Request::from_url(
31ce2453c82fatagen2026-09-24 13:07 UTC656
                uri,
31ce2453c82fatagen2026-09-24 13:07 UTC657
                settings.width,
31ce2453c82fatagen2026-09-24 13:07 UTC658
                settings.height,
31ce2453c82fatagen2026-09-24 13:07 UTC659
            )
31ce2453c82fatagen2026-09-24 13:07 UTC660
            .map_err(invalid_uri)?;
31ce2453c82fatagen2026-09-24 13:07 UTC661
            endpoint.method = Some(settings.method.clone());
31ce2453c82fatagen2026-09-24 13:07 UTC662
            endpoint.animated = Some(settings.animated);
31ce2453c82fatagen2026-09-24 13:07 UTC663
            into_http!(endpoint, SendAccessToken::None)
31ce2453c82fatagen2026-09-24 13:07 UTC664
        }
31ce2453c82fatagen2026-09-24 13:07 UTC665
    };
31ce2453c82fatagen2026-09-24 13:07 UTC666
    // legacy endpoints don't ask for a token but plenty of servers want one anyway
31ce2453c82fatagen2026-09-24 13:07 UTC667
    if let Some(token) = token.as_deref() {
31ce2453c82fatagen2026-09-24 13:07 UTC668
        let value = format!("Bearer {token}").parse().map_err(|_| {
31ce2453c82fatagen2026-09-24 13:07 UTC669
            Error::new(ErrorKind::Unauthenticated, "stored access token is invalid")
31ce2453c82fatagen2026-09-24 13:07 UTC670
        })?;
31ce2453c82fatagen2026-09-24 13:07 UTC671
        http_request
31ce2453c82fatagen2026-09-24 13:07 UTC672
            .headers_mut()
31ce2453c82fatagen2026-09-24 13:07 UTC673
            .insert(http::header::AUTHORIZATION, value);
31ce2453c82fatagen2026-09-24 13:07 UTC674
    }
31ce2453c82fatagen2026-09-24 13:07 UTC675
    Ok(http_request)
31ce2453c82fatagen2026-09-24 13:07 UTC676
}
31ce2453c82fatagen2026-09-24 13:07 UTC677
31ce2453c82fatagen2026-09-24 13:07 UTC678
pub(crate) async fn queue_upload(
31ce2453c82fatagen2026-09-24 13:07 UTC679
    room: Room,
31ce2453c82fatagen2026-09-24 13:07 UTC680
    upload: AttachmentUpload,
31ce2453c82fatagen2026-09-24 13:07 UTC681
    max_bytes: usize,
31ce2453c82fatagen2026-09-24 13:07 UTC682
) -> Result<(), Error> {
0827ad2d24e9atagen2026-10-23 06:49 UTC683
    let (mut upload, mime, mut config) = prepare_upload(upload, max_bytes)?;
0827ad2d24e9atagen2026-10-23 06:49 UTC684
    let thread_root = upload.timeline_id.thread_root().cloned();
0827ad2d24e9atagen2026-10-23 06:49 UTC685
    config.reply = match (upload.reply_to.take(), thread_root) {
0827ad2d24e9atagen2026-10-23 06:49 UTC686
        (Some(event_id), thread_root) => {
0827ad2d24e9atagen2026-10-23 06:49 UTC687
            let reply = || SdkReply {
0827ad2d24e9atagen2026-10-23 06:49 UTC688
                event_id: event_id.clone(),
0827ad2d24e9atagen2026-10-23 06:49 UTC689
                enforce_thread: if thread_root.is_some() {
0827ad2d24e9atagen2026-10-23 06:49 UTC690
                    EnforceThread::Threaded(ReplyWithinThread::Yes)
0827ad2d24e9atagen2026-10-23 06:49 UTC691
                } else {
0827ad2d24e9atagen2026-10-23 06:49 UTC692
                    EnforceThread::MaybeThreaded
0827ad2d24e9atagen2026-10-23 06:49 UTC693
                },
0827ad2d24e9atagen2026-10-23 06:49 UTC694
                add_mentions: if upload.notify_replied_to {
0827ad2d24e9atagen2026-10-23 06:49 UTC695
                    AddMentions::Yes
0827ad2d24e9atagen2026-10-23 06:49 UTC696
                } else {
0827ad2d24e9atagen2026-10-23 06:49 UTC697
                    AddMentions::No
0827ad2d24e9atagen2026-10-23 06:49 UTC698
                },
0827ad2d24e9atagen2026-10-23 06:49 UTC699
            };
0827ad2d24e9atagen2026-10-23 06:49 UTC700
            if let Some(thread_root) = &thread_root {
0827ad2d24e9atagen2026-10-23 06:49 UTC701
                check_reply_thread(&room, reply(), thread_root).await?;
0827ad2d24e9atagen2026-10-23 06:49 UTC702
            }
0827ad2d24e9atagen2026-10-23 06:49 UTC703
            Some(reply())
0827ad2d24e9atagen2026-10-23 06:49 UTC704
        }
0827ad2d24e9atagen2026-10-23 06:49 UTC705
        (None, Some(root_event_id)) => Some(SdkReply {
0827ad2d24e9atagen2026-10-23 06:49 UTC706
            event_id: root_event_id,
31ce2453c82fatagen2026-09-24 13:07 UTC707
            enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
31ce2453c82fatagen2026-09-24 13:07 UTC708
            add_mentions: AddMentions::No,
0827ad2d24e9atagen2026-10-23 06:49 UTC709
        }),
0827ad2d24e9atagen2026-10-23 06:49 UTC710
        (None, None) => None,
0827ad2d24e9atagen2026-10-23 06:49 UTC711
    };
31ce2453c82fatagen2026-09-24 13:07 UTC712
    room.send_queue()
31ce2453c82fatagen2026-09-24 13:07 UTC713
        .send_attachment(upload.filename, mime, upload.bytes, config)
31ce2453c82fatagen2026-09-24 13:07 UTC714
        .await
31ce2453c82fatagen2026-09-24 13:07 UTC715
        .map(drop)
31ce2453c82fatagen2026-09-24 13:07 UTC716
        .map_err(|source| {
5a52012c9b1datagen2026-09-24 23:45 UTC717
            use matrix_sdk::send_queue::RoomSendQueueError;
5a52012c9b1datagen2026-09-24 23:45 UTC718
            let kind = match &source {
5a52012c9b1datagen2026-09-24 23:45 UTC719
                RoomSendQueueError::RoomNotJoined | RoomSendQueueError::RoomDisappeared => {
5a52012c9b1datagen2026-09-24 23:45 UTC720
                    ErrorKind::RoomUnknown
5a52012c9b1datagen2026-09-24 23:45 UTC721
                }
5a52012c9b1datagen2026-09-24 23:45 UTC722
                RoomSendQueueError::StorageError(_) => ErrorKind::Store,
5a52012c9b1datagen2026-09-24 23:45 UTC723
                _ => ErrorKind::MediaUnavailable,
5a52012c9b1datagen2026-09-24 23:45 UTC724
            };
5a52012c9b1datagen2026-09-24 23:45 UTC725
            Error::with_source(kind, "media upload could not be queued", source)
31ce2453c82fatagen2026-09-24 13:07 UTC726
        })
31ce2453c82fatagen2026-09-24 13:07 UTC727
}
31ce2453c82fatagen2026-09-24 13:07 UTC728
0827ad2d24e9atagen2026-10-23 06:49 UTC729
/// the send queue builds the relation itself, so a reply that would land in another thread has to be caught before it is queued
0827ad2d24e9atagen2026-10-23 06:49 UTC730
async fn check_reply_thread(
0827ad2d24e9atagen2026-10-23 06:49 UTC731
    room: &Room,
0827ad2d24e9atagen2026-10-23 06:49 UTC732
    reply: SdkReply,
0827ad2d24e9atagen2026-10-23 06:49 UTC733
    thread_root: &OwnedEventId,
0827ad2d24e9atagen2026-10-23 06:49 UTC734
) -> Result<(), Error> {
0827ad2d24e9atagen2026-10-23 06:49 UTC735
    let content = room
9bd34e25bb86atagen2026-10-24 07:23 UTC736
        .make_reply_event(
9bd34e25bb86atagen2026-10-24 07:23 UTC737
            RoomMessageEventContentWithoutRelation::text_plain(""),
9bd34e25bb86atagen2026-10-24 07:23 UTC738
            reply,
9bd34e25bb86atagen2026-10-24 07:23 UTC739
        )
0827ad2d24e9atagen2026-10-23 06:49 UTC740
        .await
0827ad2d24e9atagen2026-10-23 06:49 UTC741
        .map_err(|source| {
0827ad2d24e9atagen2026-10-23 06:49 UTC742
            Error::with_source(ErrorKind::Timeline, "reply operation failed", source)
0827ad2d24e9atagen2026-10-23 06:49 UTC743
        })?;
0827ad2d24e9atagen2026-10-23 06:49 UTC744
    if matches!(
0827ad2d24e9atagen2026-10-23 06:49 UTC745
        content.relates_to.as_ref(),
0827ad2d24e9atagen2026-10-23 06:49 UTC746
        Some(Relation::Thread(thread)) if &thread.event_id == thread_root
0827ad2d24e9atagen2026-10-23 06:49 UTC747
    ) {
0827ad2d24e9atagen2026-10-23 06:49 UTC748
        Ok(())
0827ad2d24e9atagen2026-10-23 06:49 UTC749
    } else {
0827ad2d24e9atagen2026-10-23 06:49 UTC750
        Err(Error::new(
0827ad2d24e9atagen2026-10-23 06:49 UTC751
            ErrorKind::Invalid,
0827ad2d24e9atagen2026-10-23 06:49 UTC752
            "reply target does not belong to the selected thread",
0827ad2d24e9atagen2026-10-23 06:49 UTC753
        ))
0827ad2d24e9atagen2026-10-23 06:49 UTC754
    }
0827ad2d24e9atagen2026-10-23 06:49 UTC755
}
0827ad2d24e9atagen2026-10-23 06:49 UTC756
da8896938cf0atagen2026-10-25 10:04 UTC757
/// every entry belongs to the scopes that registered it and lives until the last of them
da8896938cf0atagen2026-10-25 10:04 UTC758
/// lets go. there is no global cap, a bounded scope only ever trims itself
da8896938cf0atagen2026-10-25 10:04 UTC759
#[derive(Default)]
31ce2453c82fatagen2026-09-24 13:07 UTC760
struct MediaRegistry {
da8896938cf0atagen2026-10-25 10:04 UTC761
    entries: HashMap<MediaId, Entry>,
da8896938cf0atagen2026-10-25 10:04 UTC762
    scopes: HashMap<u64, ScopeEntries>,
da8896938cf0atagen2026-10-25 10:04 UTC763
}
da8896938cf0atagen2026-10-25 10:04 UTC764
da8896938cf0atagen2026-10-25 10:04 UTC765
struct Entry {
da8896938cf0atagen2026-10-25 10:04 UTC766
    media: MediaRegistration,
da8896938cf0atagen2026-10-25 10:04 UTC767
    owners: Vec<u64>,
da8896938cf0atagen2026-10-25 10:04 UTC768
}
da8896938cf0atagen2026-10-25 10:04 UTC769
da8896938cf0atagen2026-10-25 10:04 UTC770
/// least recently used first. stale order slots are skipped by stamp rather than hunted down
da8896938cf0atagen2026-10-25 10:04 UTC771
#[derive(Default)]
da8896938cf0atagen2026-10-25 10:04 UTC772
struct ScopeEntries {
da8896938cf0atagen2026-10-25 10:04 UTC773
    stamps: HashMap<MediaId, u64>,
31ce2453c82fatagen2026-09-24 13:07 UTC774
    order: VecDeque<(MediaId, u64)>,
31ce2453c82fatagen2026-09-24 13:07 UTC775
    next_stamp: u64,
da8896938cf0atagen2026-10-25 10:04 UTC776
    limit: Option<usize>,
31ce2453c82fatagen2026-09-24 13:07 UTC777
}
31ce2453c82fatagen2026-09-24 13:07 UTC778
da8896938cf0atagen2026-10-25 10:04 UTC779
impl ScopeEntries {
da8896938cf0atagen2026-10-25 10:04 UTC780
    fn touch(&mut self, id: &MediaId) {
da8896938cf0atagen2026-10-25 10:04 UTC781
        if self.limit.is_none() {
da8896938cf0atagen2026-10-25 10:04 UTC782
            self.stamps.insert(id.clone(), 0);
da8896938cf0atagen2026-10-25 10:04 UTC783
            return;
31ce2453c82fatagen2026-09-24 13:07 UTC784
        }
da8896938cf0atagen2026-10-25 10:04 UTC785
        let stamp = self.next_stamp;
da8896938cf0atagen2026-10-25 10:04 UTC786
        self.next_stamp += 1;
da8896938cf0atagen2026-10-25 10:04 UTC787
        self.stamps.insert(id.clone(), stamp);
da8896938cf0atagen2026-10-25 10:04 UTC788
        self.order.push_back((id.clone(), stamp));
31ce2453c82fatagen2026-09-24 13:07 UTC789
    }
31ce2453c82fatagen2026-09-24 13:07 UTC790
da8896938cf0atagen2026-10-25 10:04 UTC791
    /// what fell out past the limit
da8896938cf0atagen2026-10-25 10:04 UTC792
    fn trim(&mut self) -> Vec<MediaId> {
da8896938cf0atagen2026-10-25 10:04 UTC793
        let Some(limit) = self.limit else {
da8896938cf0atagen2026-10-25 10:04 UTC794
            return Vec::new();
da8896938cf0atagen2026-10-25 10:04 UTC795
        };
da8896938cf0atagen2026-10-25 10:04 UTC796
        let mut dropped = Vec::new();
da8896938cf0atagen2026-10-25 10:04 UTC797
        while self.stamps.len() > limit {
31ce2453c82fatagen2026-09-24 13:07 UTC798
            let Some((id, stamp)) = self.order.pop_front() else {
31ce2453c82fatagen2026-09-24 13:07 UTC799
                break;
31ce2453c82fatagen2026-09-24 13:07 UTC800
            };
da8896938cf0atagen2026-10-25 10:04 UTC801
            if self.stamps.get(&id) == Some(&stamp) {
da8896938cf0atagen2026-10-25 10:04 UTC802
                self.stamps.remove(&id);
da8896938cf0atagen2026-10-25 10:04 UTC803
                dropped.push(id);
31ce2453c82fatagen2026-09-24 13:07 UTC804
            }
31ce2453c82fatagen2026-09-24 13:07 UTC805
        }
da8896938cf0atagen2026-10-25 10:04 UTC806
        if self.order.len() > limit.saturating_mul(4) {
da8896938cf0atagen2026-10-25 10:04 UTC807
            let stamps = &self.stamps;
31ce2453c82fatagen2026-09-24 13:07 UTC808
            self.order
da8896938cf0atagen2026-10-25 10:04 UTC809
                .retain(|(id, stamp)| stamps.get(id) == Some(stamp));
31ce2453c82fatagen2026-09-24 13:07 UTC810
        }
da8896938cf0atagen2026-10-25 10:04 UTC811
        dropped
31ce2453c82fatagen2026-09-24 13:07 UTC812
    }
da8896938cf0atagen2026-10-25 10:04 UTC813
}
31ce2453c82fatagen2026-09-24 13:07 UTC814
da8896938cf0atagen2026-10-25 10:04 UTC815
impl MediaRegistry {
da8896938cf0atagen2026-10-25 10:04 UTC816
    fn get(&mut self, id: &MediaId) -> Option<&MediaRegistration> {
da8896938cf0atagen2026-10-25 10:04 UTC817
        let entry = self.entries.get(id)?;
da8896938cf0atagen2026-10-25 10:04 UTC818
        for owner in &entry.owners {
da8896938cf0atagen2026-10-25 10:04 UTC819
            if let Some(scope) = self.scopes.get_mut(owner) {
da8896938cf0atagen2026-10-25 10:04 UTC820
                scope.touch(id);
da8896938cf0atagen2026-10-25 10:04 UTC821
            }
da8896938cf0atagen2026-10-25 10:04 UTC822
        }
da8896938cf0atagen2026-10-25 10:04 UTC823
        Some(&entry.media)
da8896938cf0atagen2026-10-25 10:04 UTC824
    }
da8896938cf0atagen2026-10-25 10:04 UTC825
da8896938cf0atagen2026-10-25 10:04 UTC826
    fn insert_all(&mut self, scope: &MediaScope, media: Vec<MediaRegistration>) {
da8896938cf0atagen2026-10-25 10:04 UTC827
        let owner = scope.nonce();
da8896938cf0atagen2026-10-25 10:04 UTC828
        let entries = self.scopes.entry(owner).or_insert_with(|| ScopeEntries {
da8896938cf0atagen2026-10-25 10:04 UTC829
            limit: scope.limit(),
da8896938cf0atagen2026-10-25 10:04 UTC830
            ..ScopeEntries::default()
da8896938cf0atagen2026-10-25 10:04 UTC831
        });
da8896938cf0atagen2026-10-25 10:04 UTC832
        for media in media {
da8896938cf0atagen2026-10-25 10:04 UTC833
            let id = media.id.clone();
da8896938cf0atagen2026-10-25 10:04 UTC834
            entries.touch(&id);
da8896938cf0atagen2026-10-25 10:04 UTC835
            let entry = self.entries.entry(id).or_insert_with(|| Entry {
da8896938cf0atagen2026-10-25 10:04 UTC836
                media: media.clone(),
da8896938cf0atagen2026-10-25 10:04 UTC837
                owners: Vec::new(),
da8896938cf0atagen2026-10-25 10:04 UTC838
            });
da8896938cf0atagen2026-10-25 10:04 UTC839
            entry.media = media;
da8896938cf0atagen2026-10-25 10:04 UTC840
            if !entry.owners.contains(&owner) {
da8896938cf0atagen2026-10-25 10:04 UTC841
                entry.owners.push(owner);
da8896938cf0atagen2026-10-25 10:04 UTC842
            }
da8896938cf0atagen2026-10-25 10:04 UTC843
        }
da8896938cf0atagen2026-10-25 10:04 UTC844
        for id in entries.trim() {
da8896938cf0atagen2026-10-25 10:04 UTC845
            Self::disown(&mut self.entries, &id, owner);
da8896938cf0atagen2026-10-25 10:04 UTC846
        }
da8896938cf0atagen2026-10-25 10:04 UTC847
    }
da8896938cf0atagen2026-10-25 10:04 UTC848
da8896938cf0atagen2026-10-25 10:04 UTC849
    fn disown(entries: &mut HashMap<MediaId, Entry>, id: &MediaId, owner: u64) {
da8896938cf0atagen2026-10-25 10:04 UTC850
        let Some(entry) = entries.get_mut(id) else {
da8896938cf0atagen2026-10-25 10:04 UTC851
            return;
da8896938cf0atagen2026-10-25 10:04 UTC852
        };
da8896938cf0atagen2026-10-25 10:04 UTC853
        entry.owners.retain(|current| *current != owner);
da8896938cf0atagen2026-10-25 10:04 UTC854
        if entry.owners.is_empty() {
da8896938cf0atagen2026-10-25 10:04 UTC855
            entries.remove(id);
da8896938cf0atagen2026-10-25 10:04 UTC856
        }
31ce2453c82fatagen2026-09-24 13:07 UTC857
    }
31ce2453c82fatagen2026-09-24 13:07 UTC858
31ce2453c82fatagen2026-09-24 13:07 UTC859
    fn release(&mut self, scope: &MediaScope) {
da8896938cf0atagen2026-10-25 10:04 UTC860
        let Some(released) = self.scopes.remove(&scope.nonce()) else {
da8896938cf0atagen2026-10-25 10:04 UTC861
            return;
da8896938cf0atagen2026-10-25 10:04 UTC862
        };
da8896938cf0atagen2026-10-25 10:04 UTC863
        for id in released.stamps.keys() {
da8896938cf0atagen2026-10-25 10:04 UTC864
            Self::disown(&mut self.entries, id, scope.nonce());
da8896938cf0atagen2026-10-25 10:04 UTC865
        }
31ce2453c82fatagen2026-09-24 13:07 UTC866
    }
31ce2453c82fatagen2026-09-24 13:07 UTC867
31ce2453c82fatagen2026-09-24 13:07 UTC868
    fn clear(&mut self) {
31ce2453c82fatagen2026-09-24 13:07 UTC869
        self.entries.clear();
da8896938cf0atagen2026-10-25 10:04 UTC870
        self.scopes.clear();
da8896938cf0atagen2026-10-25 10:04 UTC871
    }
da8896938cf0atagen2026-10-25 10:04 UTC872
}
da8896938cf0atagen2026-10-25 10:04 UTC873
da8896938cf0atagen2026-10-25 10:04 UTC874
#[cfg(test)]
da8896938cf0atagen2026-10-25 10:04 UTC875
mod tests {
da8896938cf0atagen2026-10-25 10:04 UTC876
    use super::*;
da8896938cf0atagen2026-10-25 10:04 UTC877
    use crate::MediaKind;
da8896938cf0atagen2026-10-25 10:04 UTC878
da8896938cf0atagen2026-10-25 10:04 UTC879
    fn media(name: &str) -> MediaRegistration {
da8896938cf0atagen2026-10-25 10:04 UTC880
        let uri = ruma::OwnedMxcUri::from(format!("mxc://example.org/{name}"));
da8896938cf0atagen2026-10-25 10:04 UTC881
        MediaRegistration {
da8896938cf0atagen2026-10-25 10:04 UTC882
            id: MediaId::new(format!("test#{name}")),
da8896938cf0atagen2026-10-25 10:04 UTC883
            kind: MediaKind::Image,
da8896938cf0atagen2026-10-25 10:04 UTC884
            full: MediaSourceInfo {
da8896938cf0atagen2026-10-25 10:04 UTC885
                source: MediaSource::Plain(uri),
da8896938cf0atagen2026-10-25 10:04 UTC886
                mimetype: None,
da8896938cf0atagen2026-10-25 10:04 UTC887
                declared_size: None,
da8896938cf0atagen2026-10-25 10:04 UTC888
            },
da8896938cf0atagen2026-10-25 10:04 UTC889
            thumbnail: None,
da8896938cf0atagen2026-10-25 10:04 UTC890
        }
da8896938cf0atagen2026-10-25 10:04 UTC891
    }
da8896938cf0atagen2026-10-25 10:04 UTC892
da8896938cf0atagen2026-10-25 10:04 UTC893
    fn has(registry: &mut MediaRegistry, name: &str) -> bool {
da8896938cf0atagen2026-10-25 10:04 UTC894
        registry.get(&media(name).id).is_some()
da8896938cf0atagen2026-10-25 10:04 UTC895
    }
da8896938cf0atagen2026-10-25 10:04 UTC896
da8896938cf0atagen2026-10-25 10:04 UTC897
    #[test]
da8896938cf0atagen2026-10-25 10:04 UTC898
    fn shared_media_lives_until_every_owner_lets_go() {
da8896938cf0atagen2026-10-25 10:04 UTC899
        let mut registry = MediaRegistry::default();
da8896938cf0atagen2026-10-25 10:04 UTC900
        let (timeline, context) = (MediaScope::new(), MediaScope::bounded(8));
da8896938cf0atagen2026-10-25 10:04 UTC901
        registry.insert_all(&timeline, vec![media("shared"), media("row")]);
da8896938cf0atagen2026-10-25 10:04 UTC902
        registry.insert_all(&context, vec![media("shared"), media("page")]);
da8896938cf0atagen2026-10-25 10:04 UTC903
        registry.release(&context);
da8896938cf0atagen2026-10-25 10:04 UTC904
        assert!(has(&mut registry, "shared"));
da8896938cf0atagen2026-10-25 10:04 UTC905
        assert!(has(&mut registry, "row"));
da8896938cf0atagen2026-10-25 10:04 UTC906
        assert!(!has(&mut registry, "page"));
da8896938cf0atagen2026-10-25 10:04 UTC907
        registry.release(&timeline);
da8896938cf0atagen2026-10-25 10:04 UTC908
        assert!(!has(&mut registry, "shared"));
da8896938cf0atagen2026-10-25 10:04 UTC909
        assert!(registry.entries.is_empty());
da8896938cf0atagen2026-10-25 10:04 UTC910
    }
da8896938cf0atagen2026-10-25 10:04 UTC911
da8896938cf0atagen2026-10-25 10:04 UTC912
    #[test]
da8896938cf0atagen2026-10-25 10:04 UTC913
    fn heavy_context_paging_trims_only_its_own_least_recent_entries() {
da8896938cf0atagen2026-10-25 10:04 UTC914
        let mut registry = MediaRegistry::default();
da8896938cf0atagen2026-10-25 10:04 UTC915
        let (timeline, context) = (MediaScope::new(), MediaScope::bounded(4));
da8896938cf0atagen2026-10-25 10:04 UTC916
        registry.insert_all(&timeline, vec![media("on-screen"), media("both")]);
da8896938cf0atagen2026-10-25 10:04 UTC917
        registry.insert_all(&context, vec![media("both"), media("kept")]);
da8896938cf0atagen2026-10-25 10:04 UTC918
        for page in 0..50 {
da8896938cf0atagen2026-10-25 10:04 UTC919
            assert!(has(&mut registry, "kept"));
da8896938cf0atagen2026-10-25 10:04 UTC920
            registry.insert_all(&context, vec![media(&format!("page-{page}"))]);
da8896938cf0atagen2026-10-25 10:04 UTC921
        }
da8896938cf0atagen2026-10-25 10:04 UTC922
        assert!(has(&mut registry, "on-screen"));
da8896938cf0atagen2026-10-25 10:04 UTC923
        assert!(has(&mut registry, "both"));
da8896938cf0atagen2026-10-25 10:04 UTC924
        assert!(has(&mut registry, "kept"));
da8896938cf0atagen2026-10-25 10:04 UTC925
        assert!(!has(&mut registry, "page-0"));
da8896938cf0atagen2026-10-25 10:04 UTC926
        assert!(registry.scopes[&context.nonce()].stamps.len() <= 4);
da8896938cf0atagen2026-10-25 10:04 UTC927
        registry.release(&context);
da8896938cf0atagen2026-10-25 10:04 UTC928
        assert!(has(&mut registry, "both"));
da8896938cf0atagen2026-10-25 10:04 UTC929
        assert!(!has(&mut registry, "kept"));
31ce2453c82fatagen2026-09-24 13:07 UTC930
    }
31ce2453c82fatagen2026-09-24 13:07 UTC931
}