atagen

macltok

public
Star0
Watch0
31ce2453c82f470aa8b5d3861ab0eec4273afc5d
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,
31ce2453c82fatagen2026-09-24 13:07 UTC7
};
31ce2453c82fatagen2026-09-24 13:07 UTC8
31ce2453c82fatagen2026-09-24 13:07 UTC9
use futures_util::{
31ce2453c82fatagen2026-09-24 13:07 UTC10
    FutureExt, StreamExt,
31ce2453c82fatagen2026-09-24 13:07 UTC11
    future::{BoxFuture, Shared, WeakShared},
31ce2453c82fatagen2026-09-24 13:07 UTC12
};
31ce2453c82fatagen2026-09-24 13:07 UTC13
use matrix_sdk::{
31ce2453c82fatagen2026-09-24 13:07 UTC14
    Client, Room,
31ce2453c82fatagen2026-09-24 13:07 UTC15
    media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
31ce2453c82fatagen2026-09-24 13:07 UTC16
    room::reply::{EnforceThread, Reply as SdkReply},
31ce2453c82fatagen2026-09-24 13:07 UTC17
    ruma::{
31ce2453c82fatagen2026-09-24 13:07 UTC18
        api::{
31ce2453c82fatagen2026-09-24 13:07 UTC19
            Metadata, OutgoingRequest,
31ce2453c82fatagen2026-09-24 13:07 UTC20
            auth_scheme::SendAccessToken,
31ce2453c82fatagen2026-09-24 13:07 UTC21
            client::{authenticated_media, media},
31ce2453c82fatagen2026-09-24 13:07 UTC22
        },
31ce2453c82fatagen2026-09-24 13:07 UTC23
        events::room::{
31ce2453c82fatagen2026-09-24 13:07 UTC24
            MediaSource,
31ce2453c82fatagen2026-09-24 13:07 UTC25
            message::{AddMentions, ReplyWithinThread},
31ce2453c82fatagen2026-09-24 13:07 UTC26
        },
31ce2453c82fatagen2026-09-24 13:07 UTC27
    },
31ce2453c82fatagen2026-09-24 13:07 UTC28
};
31ce2453c82fatagen2026-09-24 13:07 UTC29
use matrix_sdk_crypto::{AttachmentDecryptor, MediaEncryptionInfo};
31ce2453c82fatagen2026-09-24 13:07 UTC30
use ruma::MxcUri;
31ce2453c82fatagen2026-09-24 13:07 UTC31
use tokio::sync::Mutex;
31ce2453c82fatagen2026-09-24 13:07 UTC32
31ce2453c82fatagen2026-09-24 13:07 UTC33
use crate::{
31ce2453c82fatagen2026-09-24 13:07 UTC34
    AttachmentUpload, Error, ErrorKind, Media, MediaId, MediaKind, MediaSize, TimelineId,
31ce2453c82fatagen2026-09-24 13:07 UTC35
    normalise::{MediaRegistration, MediaScope},
31ce2453c82fatagen2026-09-24 13:07 UTC36
    upload::prepare_upload,
31ce2453c82fatagen2026-09-24 13:07 UTC37
};
31ce2453c82fatagen2026-09-24 13:07 UTC38
31ce2453c82fatagen2026-09-24 13:07 UTC39
/// the uploader's thumbnail is used when there is one, this is only what we ask the server for
31ce2453c82fatagen2026-09-24 13:07 UTC40
const TIMELINE_THUMBNAIL: u32 = 800;
31ce2453c82fatagen2026-09-24 13:07 UTC41
31ce2453c82fatagen2026-09-24 13:07 UTC42
type FlightFuture = BoxFuture<'static, Result<Media, Error>>;
31ce2453c82fatagen2026-09-24 13:07 UTC43
type Flight = Shared<FlightFuture>;
31ce2453c82fatagen2026-09-24 13:07 UTC44
31ce2453c82fatagen2026-09-24 13:07 UTC45
/// owns everything needed to turn a media id or an mxc uri into bytes, so the
31ce2453c82fatagen2026-09-24 13:07 UTC46
/// handle can answer media queries without a trip through the engine
31ce2453c82fatagen2026-09-24 13:07 UTC47
pub(crate) struct MediaService {
31ce2453c82fatagen2026-09-24 13:07 UTC48
    client: Client,
31ce2453c82fatagen2026-09-24 13:07 UTC49
    registry: Mutex<MediaRegistry>,
31ce2453c82fatagen2026-09-24 13:07 UTC50
    flights: Mutex<HashMap<String, WeakShared<FlightFuture>>>,
31ce2453c82fatagen2026-09-24 13:07 UTC51
    max_bytes: usize,
31ce2453c82fatagen2026-09-24 13:07 UTC52
}
31ce2453c82fatagen2026-09-24 13:07 UTC53
31ce2453c82fatagen2026-09-24 13:07 UTC54
impl MediaService {
31ce2453c82fatagen2026-09-24 13:07 UTC55
    pub(crate) fn new(client: Client, max_bytes: usize, max_sources: usize) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC56
        Self {
31ce2453c82fatagen2026-09-24 13:07 UTC57
            client,
31ce2453c82fatagen2026-09-24 13:07 UTC58
            registry: Mutex::new(MediaRegistry::new(max_sources)),
31ce2453c82fatagen2026-09-24 13:07 UTC59
            flights: Mutex::new(HashMap::new()),
31ce2453c82fatagen2026-09-24 13:07 UTC60
            max_bytes,
31ce2453c82fatagen2026-09-24 13:07 UTC61
        }
31ce2453c82fatagen2026-09-24 13:07 UTC62
    }
31ce2453c82fatagen2026-09-24 13:07 UTC63
31ce2453c82fatagen2026-09-24 13:07 UTC64
    pub(crate) async fn register(&self, media: Vec<MediaRegistration>) {
31ce2453c82fatagen2026-09-24 13:07 UTC65
        if !media.is_empty() {
31ce2453c82fatagen2026-09-24 13:07 UTC66
            self.registry.lock().await.insert_all(media);
31ce2453c82fatagen2026-09-24 13:07 UTC67
        }
31ce2453c82fatagen2026-09-24 13:07 UTC68
    }
31ce2453c82fatagen2026-09-24 13:07 UTC69
31ce2453c82fatagen2026-09-24 13:07 UTC70
    pub(crate) async fn release(&self, scope: &MediaScope) {
31ce2453c82fatagen2026-09-24 13:07 UTC71
        self.registry.lock().await.release(scope);
31ce2453c82fatagen2026-09-24 13:07 UTC72
    }
31ce2453c82fatagen2026-09-24 13:07 UTC73
31ce2453c82fatagen2026-09-24 13:07 UTC74
    pub(crate) async fn clear(&self) {
31ce2453c82fatagen2026-09-24 13:07 UTC75
        self.registry.lock().await.clear();
31ce2453c82fatagen2026-09-24 13:07 UTC76
    }
31ce2453c82fatagen2026-09-24 13:07 UTC77
31ce2453c82fatagen2026-09-24 13:07 UTC78
    pub(crate) async fn timeline_media(
31ce2453c82fatagen2026-09-24 13:07 UTC79
        &self,
31ce2453c82fatagen2026-09-24 13:07 UTC80
        media_id: &MediaId,
31ce2453c82fatagen2026-09-24 13:07 UTC81
        size: MediaSize,
31ce2453c82fatagen2026-09-24 13:07 UTC82
    ) -> Result<Media, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC83
        let media = self
31ce2453c82fatagen2026-09-24 13:07 UTC84
            .registry
31ce2453c82fatagen2026-09-24 13:07 UTC85
            .lock()
31ce2453c82fatagen2026-09-24 13:07 UTC86
            .await
31ce2453c82fatagen2026-09-24 13:07 UTC87
            .get(media_id)
31ce2453c82fatagen2026-09-24 13:07 UTC88
            .cloned()
31ce2453c82fatagen2026-09-24 13:07 UTC89
            .ok_or_else(|| {
31ce2453c82fatagen2026-09-24 13:07 UTC90
                Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC91
                    ErrorKind::MediaUnknown,
31ce2453c82fatagen2026-09-24 13:07 UTC92
                    "media is not registered in a retained timeline",
31ce2453c82fatagen2026-09-24 13:07 UTC93
                )
31ce2453c82fatagen2026-09-24 13:07 UTC94
            })?;
31ce2453c82fatagen2026-09-24 13:07 UTC95
        let (source, format, mimetype, declared_size) = match size {
31ce2453c82fatagen2026-09-24 13:07 UTC96
            MediaSize::Full => (
31ce2453c82fatagen2026-09-24 13:07 UTC97
                media.full.source,
31ce2453c82fatagen2026-09-24 13:07 UTC98
                MediaFormat::File,
31ce2453c82fatagen2026-09-24 13:07 UTC99
                media.full.mimetype,
31ce2453c82fatagen2026-09-24 13:07 UTC100
                media.full.declared_size,
31ce2453c82fatagen2026-09-24 13:07 UTC101
            ),
31ce2453c82fatagen2026-09-24 13:07 UTC102
            MediaSize::Thumbnail { .. } => match media.thumbnail {
31ce2453c82fatagen2026-09-24 13:07 UTC103
                Some(thumbnail) => (
31ce2453c82fatagen2026-09-24 13:07 UTC104
                    thumbnail.source,
31ce2453c82fatagen2026-09-24 13:07 UTC105
                    MediaFormat::File,
31ce2453c82fatagen2026-09-24 13:07 UTC106
                    thumbnail.mimetype,
31ce2453c82fatagen2026-09-24 13:07 UTC107
                    thumbnail.declared_size,
31ce2453c82fatagen2026-09-24 13:07 UTC108
                ),
31ce2453c82fatagen2026-09-24 13:07 UTC109
                // the server can only resize what it can read
31ce2453c82fatagen2026-09-24 13:07 UTC110
                None if matches!(media.kind, MediaKind::Image | MediaKind::Video)
31ce2453c82fatagen2026-09-24 13:07 UTC111
                    && matches!(media.full.source, MediaSource::Plain(_)) =>
31ce2453c82fatagen2026-09-24 13:07 UTC112
                {
31ce2453c82fatagen2026-09-24 13:07 UTC113
                    (
31ce2453c82fatagen2026-09-24 13:07 UTC114
                        media.full.source,
31ce2453c82fatagen2026-09-24 13:07 UTC115
                        thumbnail_format(TIMELINE_THUMBNAIL, TIMELINE_THUMBNAIL),
31ce2453c82fatagen2026-09-24 13:07 UTC116
                        media.full.mimetype,
31ce2453c82fatagen2026-09-24 13:07 UTC117
                        None,
31ce2453c82fatagen2026-09-24 13:07 UTC118
                    )
31ce2453c82fatagen2026-09-24 13:07 UTC119
                }
31ce2453c82fatagen2026-09-24 13:07 UTC120
                None => {
31ce2453c82fatagen2026-09-24 13:07 UTC121
                    return Err(Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC122
                        ErrorKind::MediaUnavailable,
31ce2453c82fatagen2026-09-24 13:07 UTC123
                        "the requested media size is unavailable",
31ce2453c82fatagen2026-09-24 13:07 UTC124
                    ));
31ce2453c82fatagen2026-09-24 13:07 UTC125
                }
31ce2453c82fatagen2026-09-24 13:07 UTC126
            },
31ce2453c82fatagen2026-09-24 13:07 UTC127
        };
31ce2453c82fatagen2026-09-24 13:07 UTC128
        if declared_size.is_some_and(|size| size > self.max_bytes as u64) {
31ce2453c82fatagen2026-09-24 13:07 UTC129
            return Err(too_large("media exceeds the configured byte limit"));
31ce2453c82fatagen2026-09-24 13:07 UTC130
        }
31ce2453c82fatagen2026-09-24 13:07 UTC131
        self.fetch(MediaRequestParameters { source, format }, mimetype)
31ce2453c82fatagen2026-09-24 13:07 UTC132
            .await
31ce2453c82fatagen2026-09-24 13:07 UTC133
    }
31ce2453c82fatagen2026-09-24 13:07 UTC134
31ce2453c82fatagen2026-09-24 13:07 UTC135
    pub(crate) async fn mxc_media(&self, uri: &MxcUri, size: MediaSize) -> Result<Media, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC136
        if !uri.is_valid() {
31ce2453c82fatagen2026-09-24 13:07 UTC137
            return Err(Error::new(ErrorKind::Invalid, "mxc uri is malformed"));
31ce2453c82fatagen2026-09-24 13:07 UTC138
        }
31ce2453c82fatagen2026-09-24 13:07 UTC139
        let format = match size {
31ce2453c82fatagen2026-09-24 13:07 UTC140
            MediaSize::Full => MediaFormat::File,
31ce2453c82fatagen2026-09-24 13:07 UTC141
            MediaSize::Thumbnail { width, height } => thumbnail_format(width, height),
31ce2453c82fatagen2026-09-24 13:07 UTC142
        };
31ce2453c82fatagen2026-09-24 13:07 UTC143
        self.fetch(
31ce2453c82fatagen2026-09-24 13:07 UTC144
            MediaRequestParameters {
31ce2453c82fatagen2026-09-24 13:07 UTC145
                source: MediaSource::Plain(uri.to_owned()),
31ce2453c82fatagen2026-09-24 13:07 UTC146
                format,
31ce2453c82fatagen2026-09-24 13:07 UTC147
            },
31ce2453c82fatagen2026-09-24 13:07 UTC148
            None,
31ce2453c82fatagen2026-09-24 13:07 UTC149
        )
31ce2453c82fatagen2026-09-24 13:07 UTC150
        .await
31ce2453c82fatagen2026-09-24 13:07 UTC151
    }
31ce2453c82fatagen2026-09-24 13:07 UTC152
31ce2453c82fatagen2026-09-24 13:07 UTC153
    async fn fetch(
31ce2453c82fatagen2026-09-24 13:07 UTC154
        &self,
31ce2453c82fatagen2026-09-24 13:07 UTC155
        request: MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC156
        mimetype: Option<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC157
    ) -> Result<Media, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC158
        let key = flight_key(&request);
31ce2453c82fatagen2026-09-24 13:07 UTC159
        let flight = {
31ce2453c82fatagen2026-09-24 13:07 UTC160
            let mut flights = self.flights.lock().await;
31ce2453c82fatagen2026-09-24 13:07 UTC161
            flights.retain(|_, flight| flight.upgrade().is_some());
31ce2453c82fatagen2026-09-24 13:07 UTC162
            match flights.get(&key).and_then(WeakShared::upgrade) {
31ce2453c82fatagen2026-09-24 13:07 UTC163
                Some(flight) => flight,
31ce2453c82fatagen2026-09-24 13:07 UTC164
                None => {
31ce2453c82fatagen2026-09-24 13:07 UTC165
                    let flight: Flight = AssertUnwindSafe(fetch(
31ce2453c82fatagen2026-09-24 13:07 UTC166
                        self.client.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC167
                        request,
31ce2453c82fatagen2026-09-24 13:07 UTC168
                        mimetype,
31ce2453c82fatagen2026-09-24 13:07 UTC169
                        self.max_bytes,
31ce2453c82fatagen2026-09-24 13:07 UTC170
                    ))
31ce2453c82fatagen2026-09-24 13:07 UTC171
                    .catch_unwind()
31ce2453c82fatagen2026-09-24 13:07 UTC172
                    .map(|result| {
31ce2453c82fatagen2026-09-24 13:07 UTC173
                        result.unwrap_or_else(|_| {
31ce2453c82fatagen2026-09-24 13:07 UTC174
                            Err(Error::new(ErrorKind::Other, "media fetch panicked"))
31ce2453c82fatagen2026-09-24 13:07 UTC175
                        })
31ce2453c82fatagen2026-09-24 13:07 UTC176
                    })
31ce2453c82fatagen2026-09-24 13:07 UTC177
                    .boxed()
31ce2453c82fatagen2026-09-24 13:07 UTC178
                    .shared();
31ce2453c82fatagen2026-09-24 13:07 UTC179
                    if let Some(weak) = flight.downgrade() {
31ce2453c82fatagen2026-09-24 13:07 UTC180
                        flights.insert(key, weak);
31ce2453c82fatagen2026-09-24 13:07 UTC181
                    }
31ce2453c82fatagen2026-09-24 13:07 UTC182
                    flight
31ce2453c82fatagen2026-09-24 13:07 UTC183
                }
31ce2453c82fatagen2026-09-24 13:07 UTC184
            }
31ce2453c82fatagen2026-09-24 13:07 UTC185
        };
31ce2453c82fatagen2026-09-24 13:07 UTC186
        flight.await
31ce2453c82fatagen2026-09-24 13:07 UTC187
    }
31ce2453c82fatagen2026-09-24 13:07 UTC188
}
31ce2453c82fatagen2026-09-24 13:07 UTC189
31ce2453c82fatagen2026-09-24 13:07 UTC190
fn thumbnail_format(width: u32, height: u32) -> MediaFormat {
31ce2453c82fatagen2026-09-24 13:07 UTC191
    MediaFormat::Thumbnail(MediaThumbnailSettings::new(width.into(), height.into()))
31ce2453c82fatagen2026-09-24 13:07 UTC192
}
31ce2453c82fatagen2026-09-24 13:07 UTC193
31ce2453c82fatagen2026-09-24 13:07 UTC194
fn flight_key(request: &MediaRequestParameters) -> String {
31ce2453c82fatagen2026-09-24 13:07 UTC195
    let uri = match &request.source {
31ce2453c82fatagen2026-09-24 13:07 UTC196
        MediaSource::Plain(uri) => uri.as_str(),
31ce2453c82fatagen2026-09-24 13:07 UTC197
        MediaSource::Encrypted(file) => file.url.as_str(),
31ce2453c82fatagen2026-09-24 13:07 UTC198
    };
31ce2453c82fatagen2026-09-24 13:07 UTC199
    match &request.format {
31ce2453c82fatagen2026-09-24 13:07 UTC200
        MediaFormat::File => uri.to_owned(),
31ce2453c82fatagen2026-09-24 13:07 UTC201
        MediaFormat::Thumbnail(settings) => format!(
31ce2453c82fatagen2026-09-24 13:07 UTC202
            "{uri}|{}|{}x{}|{}",
31ce2453c82fatagen2026-09-24 13:07 UTC203
            settings.method, settings.width, settings.height, settings.animated
31ce2453c82fatagen2026-09-24 13:07 UTC204
        ),
31ce2453c82fatagen2026-09-24 13:07 UTC205
    }
31ce2453c82fatagen2026-09-24 13:07 UTC206
}
31ce2453c82fatagen2026-09-24 13:07 UTC207
31ce2453c82fatagen2026-09-24 13:07 UTC208
fn too_large(message: &'static str) -> Error {
31ce2453c82fatagen2026-09-24 13:07 UTC209
    Error::new(ErrorKind::MediaTooLarge, message)
31ce2453c82fatagen2026-09-24 13:07 UTC210
}
31ce2453c82fatagen2026-09-24 13:07 UTC211
31ce2453c82fatagen2026-09-24 13:07 UTC212
async fn fetch(
31ce2453c82fatagen2026-09-24 13:07 UTC213
    client: Client,
31ce2453c82fatagen2026-09-24 13:07 UTC214
    request: MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC215
    mimetype: Option<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC216
    max_bytes: usize,
31ce2453c82fatagen2026-09-24 13:07 UTC217
) -> Result<Media, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC218
    if let Some(cached) = cached_media(&client, &request).await? {
31ce2453c82fatagen2026-09-24 13:07 UTC219
        if cached.len() > max_bytes {
31ce2453c82fatagen2026-09-24 13:07 UTC220
            if let Ok(store) = client.media_store().lock().await {
31ce2453c82fatagen2026-09-24 13:07 UTC221
                let _ = store.remove_media_content(&request).await;
31ce2453c82fatagen2026-09-24 13:07 UTC222
            }
31ce2453c82fatagen2026-09-24 13:07 UTC223
            return Err(too_large("cached media exceeds the configured byte limit"));
31ce2453c82fatagen2026-09-24 13:07 UTC224
        }
31ce2453c82fatagen2026-09-24 13:07 UTC225
        return Ok(media(cached, mimetype));
31ce2453c82fatagen2026-09-24 13:07 UTC226
    }
31ce2453c82fatagen2026-09-24 13:07 UTC227
    let (bytes, served_mimetype) = download_bounded(&client, &request, max_bytes).await?;
31ce2453c82fatagen2026-09-24 13:07 UTC228
    // the sdk caches plaintext under the encrypted key, so we do too or nobody can read anything
31ce2453c82fatagen2026-09-24 13:07 UTC229
    let bytes = maybe_decrypt(bytes, &request, max_bytes)?;
31ce2453c82fatagen2026-09-24 13:07 UTC230
    if let Ok(store) = client.media_store().lock().await {
31ce2453c82fatagen2026-09-24 13:07 UTC231
        let _ = store
31ce2453c82fatagen2026-09-24 13:07 UTC232
            .add_media_content(
31ce2453c82fatagen2026-09-24 13:07 UTC233
                &request,
31ce2453c82fatagen2026-09-24 13:07 UTC234
                bytes.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC235
                matrix_sdk::media::store::IgnoreMediaRetentionPolicy::No,
31ce2453c82fatagen2026-09-24 13:07 UTC236
            )
31ce2453c82fatagen2026-09-24 13:07 UTC237
            .await;
31ce2453c82fatagen2026-09-24 13:07 UTC238
    }
31ce2453c82fatagen2026-09-24 13:07 UTC239
    Ok(media(bytes, mimetype.or(served_mimetype)))
31ce2453c82fatagen2026-09-24 13:07 UTC240
}
31ce2453c82fatagen2026-09-24 13:07 UTC241
31ce2453c82fatagen2026-09-24 13:07 UTC242
fn media(bytes: Vec<u8>, mimetype: Option<String>) -> Media {
31ce2453c82fatagen2026-09-24 13:07 UTC243
    let mimetype = mimetype
31ce2453c82fatagen2026-09-24 13:07 UTC244
        .or_else(|| sniff_image(&bytes).map(str::to_owned))
31ce2453c82fatagen2026-09-24 13:07 UTC245
        .unwrap_or_else(|| "application/octet-stream".to_owned());
31ce2453c82fatagen2026-09-24 13:07 UTC246
    Media {
31ce2453c82fatagen2026-09-24 13:07 UTC247
        bytes: Arc::from(bytes),
31ce2453c82fatagen2026-09-24 13:07 UTC248
        mimetype,
31ce2453c82fatagen2026-09-24 13:07 UTC249
    }
31ce2453c82fatagen2026-09-24 13:07 UTC250
}
31ce2453c82fatagen2026-09-24 13:07 UTC251
31ce2453c82fatagen2026-09-24 13:07 UTC252
/// the cache forgets content types, and avatars never declare one. images are the common case
31ce2453c82fatagen2026-09-24 13:07 UTC253
fn sniff_image(bytes: &[u8]) -> Option<&'static str> {
31ce2453c82fatagen2026-09-24 13:07 UTC254
    match bytes {
31ce2453c82fatagen2026-09-24 13:07 UTC255
        [0x89, b'P', b'N', b'G', ..] => Some("image/png"),
31ce2453c82fatagen2026-09-24 13:07 UTC256
        [0xff, 0xd8, 0xff, ..] => Some("image/jpeg"),
31ce2453c82fatagen2026-09-24 13:07 UTC257
        [b'G', b'I', b'F', b'8', ..] => Some("image/gif"),
31ce2453c82fatagen2026-09-24 13:07 UTC258
        [
31ce2453c82fatagen2026-09-24 13:07 UTC259
            b'R',
31ce2453c82fatagen2026-09-24 13:07 UTC260
            b'I',
31ce2453c82fatagen2026-09-24 13:07 UTC261
            b'F',
31ce2453c82fatagen2026-09-24 13:07 UTC262
            b'F',
31ce2453c82fatagen2026-09-24 13:07 UTC263
            _,
31ce2453c82fatagen2026-09-24 13:07 UTC264
            _,
31ce2453c82fatagen2026-09-24 13:07 UTC265
            _,
31ce2453c82fatagen2026-09-24 13:07 UTC266
            _,
31ce2453c82fatagen2026-09-24 13:07 UTC267
            b'W',
31ce2453c82fatagen2026-09-24 13:07 UTC268
            b'E',
31ce2453c82fatagen2026-09-24 13:07 UTC269
            b'B',
31ce2453c82fatagen2026-09-24 13:07 UTC270
            b'P',
31ce2453c82fatagen2026-09-24 13:07 UTC271
            ..,
31ce2453c82fatagen2026-09-24 13:07 UTC272
        ] => Some("image/webp"),
31ce2453c82fatagen2026-09-24 13:07 UTC273
        _ => None,
31ce2453c82fatagen2026-09-24 13:07 UTC274
    }
31ce2453c82fatagen2026-09-24 13:07 UTC275
}
31ce2453c82fatagen2026-09-24 13:07 UTC276
31ce2453c82fatagen2026-09-24 13:07 UTC277
async fn cached_media(
31ce2453c82fatagen2026-09-24 13:07 UTC278
    client: &Client,
31ce2453c82fatagen2026-09-24 13:07 UTC279
    request: &MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC280
) -> Result<Option<Vec<u8>>, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC281
    fn cache_failed(source: impl std::error::Error + Send + Sync + 'static) -> Error {
31ce2453c82fatagen2026-09-24 13:07 UTC282
        Error::with_source(ErrorKind::Store, "reading media cache failed", source)
31ce2453c82fatagen2026-09-24 13:07 UTC283
    }
31ce2453c82fatagen2026-09-24 13:07 UTC284
    client
31ce2453c82fatagen2026-09-24 13:07 UTC285
        .media_store()
31ce2453c82fatagen2026-09-24 13:07 UTC286
        .lock()
31ce2453c82fatagen2026-09-24 13:07 UTC287
        .await
31ce2453c82fatagen2026-09-24 13:07 UTC288
        .map_err(cache_failed)?
31ce2453c82fatagen2026-09-24 13:07 UTC289
        .get_media_content(request)
31ce2453c82fatagen2026-09-24 13:07 UTC290
        .await
31ce2453c82fatagen2026-09-24 13:07 UTC291
        .map_err(cache_failed)
31ce2453c82fatagen2026-09-24 13:07 UTC292
}
31ce2453c82fatagen2026-09-24 13:07 UTC293
31ce2453c82fatagen2026-09-24 13:07 UTC294
fn maybe_decrypt(
31ce2453c82fatagen2026-09-24 13:07 UTC295
    bytes: Vec<u8>,
31ce2453c82fatagen2026-09-24 13:07 UTC296
    request: &MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC297
    max_bytes: usize,
31ce2453c82fatagen2026-09-24 13:07 UTC298
) -> Result<Vec<u8>, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC299
    let MediaSource::Encrypted(file) = &request.source else {
31ce2453c82fatagen2026-09-24 13:07 UTC300
        return Ok(bytes);
31ce2453c82fatagen2026-09-24 13:07 UTC301
    };
31ce2453c82fatagen2026-09-24 13:07 UTC302
    fn decrypt_failed(source: impl std::error::Error + Send + Sync + 'static) -> Error {
31ce2453c82fatagen2026-09-24 13:07 UTC303
        Error::with_source(
31ce2453c82fatagen2026-09-24 13:07 UTC304
            ErrorKind::MediaUnavailable,
31ce2453c82fatagen2026-09-24 13:07 UTC305
            "media decryption failed",
31ce2453c82fatagen2026-09-24 13:07 UTC306
            source,
31ce2453c82fatagen2026-09-24 13:07 UTC307
        )
31ce2453c82fatagen2026-09-24 13:07 UTC308
    }
31ce2453c82fatagen2026-09-24 13:07 UTC309
    let info: MediaEncryptionInfo = (**file).clone().into();
31ce2453c82fatagen2026-09-24 13:07 UTC310
    let mut cursor = std::io::Cursor::new(bytes);
31ce2453c82fatagen2026-09-24 13:07 UTC311
    let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).map_err(decrypt_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC312
    let mut decrypted = Vec::new();
31ce2453c82fatagen2026-09-24 13:07 UTC313
    let mut chunk = [0_u8; 8192];
31ce2453c82fatagen2026-09-24 13:07 UTC314
    loop {
31ce2453c82fatagen2026-09-24 13:07 UTC315
        let read = decryptor.read(&mut chunk).map_err(decrypt_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC316
        if read == 0 {
31ce2453c82fatagen2026-09-24 13:07 UTC317
            return Ok(decrypted);
31ce2453c82fatagen2026-09-24 13:07 UTC318
        }
31ce2453c82fatagen2026-09-24 13:07 UTC319
        if read > max_bytes.saturating_sub(decrypted.len()) {
31ce2453c82fatagen2026-09-24 13:07 UTC320
            return Err(too_large(
31ce2453c82fatagen2026-09-24 13:07 UTC321
                "decrypted media exceeds the configured byte limit",
31ce2453c82fatagen2026-09-24 13:07 UTC322
            ));
31ce2453c82fatagen2026-09-24 13:07 UTC323
        }
31ce2453c82fatagen2026-09-24 13:07 UTC324
        decrypted.extend_from_slice(&chunk[..read]);
31ce2453c82fatagen2026-09-24 13:07 UTC325
    }
31ce2453c82fatagen2026-09-24 13:07 UTC326
}
31ce2453c82fatagen2026-09-24 13:07 UTC327
31ce2453c82fatagen2026-09-24 13:07 UTC328
/// the sdk's own download has no size cap and no timeout, so we do it by hand
31ce2453c82fatagen2026-09-24 13:07 UTC329
async fn download_bounded(
31ce2453c82fatagen2026-09-24 13:07 UTC330
    client: &Client,
31ce2453c82fatagen2026-09-24 13:07 UTC331
    request: &MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC332
    max_bytes: usize,
31ce2453c82fatagen2026-09-24 13:07 UTC333
) -> Result<(Vec<u8>, Option<String>), Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC334
    let uri = match &request.source {
31ce2453c82fatagen2026-09-24 13:07 UTC335
        MediaSource::Plain(uri) => uri,
31ce2453c82fatagen2026-09-24 13:07 UTC336
        MediaSource::Encrypted(file) => &file.url,
31ce2453c82fatagen2026-09-24 13:07 UTC337
    };
31ce2453c82fatagen2026-09-24 13:07 UTC338
    if uri
31ce2453c82fatagen2026-09-24 13:07 UTC339
        .server_name()
31ce2453c82fatagen2026-09-24 13:07 UTC340
        .is_ok_and(|server| server.as_str() == "send-queue.localhost")
31ce2453c82fatagen2026-09-24 13:07 UTC341
    {
31ce2453c82fatagen2026-09-24 13:07 UTC342
        return Err(Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC343
            ErrorKind::MediaUnavailable,
31ce2453c82fatagen2026-09-24 13:07 UTC344
            "local media is not in the media cache",
31ce2453c82fatagen2026-09-24 13:07 UTC345
        ));
31ce2453c82fatagen2026-09-24 13:07 UTC346
    }
31ce2453c82fatagen2026-09-24 13:07 UTC347
    let network_failed =
31ce2453c82fatagen2026-09-24 13:07 UTC348
        |source| Error::with_source(ErrorKind::Network, "media download failed", source);
31ce2453c82fatagen2026-09-24 13:07 UTC349
    let mut refreshed = false;
31ce2453c82fatagen2026-09-24 13:07 UTC350
    let response = loop {
31ce2453c82fatagen2026-09-24 13:07 UTC351
        let http_request = media_http_request(client, request).await?;
31ce2453c82fatagen2026-09-24 13:07 UTC352
        let (parts, _) = http_request.into_parts();
31ce2453c82fatagen2026-09-24 13:07 UTC353
        let mut builder = client.http_client().get(parts.uri.to_string());
31ce2453c82fatagen2026-09-24 13:07 UTC354
        for (name, value) in &parts.headers {
31ce2453c82fatagen2026-09-24 13:07 UTC355
            builder = builder.header(name.as_str(), value.as_bytes());
31ce2453c82fatagen2026-09-24 13:07 UTC356
        }
31ce2453c82fatagen2026-09-24 13:07 UTC357
        let response = builder.send().await.map_err(network_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC358
        if response.status() != matrix_sdk::reqwest::StatusCode::UNAUTHORIZED {
31ce2453c82fatagen2026-09-24 13:07 UTC359
            break response;
31ce2453c82fatagen2026-09-24 13:07 UTC360
        }
31ce2453c82fatagen2026-09-24 13:07 UTC361
        if refreshed
31ce2453c82fatagen2026-09-24 13:07 UTC362
            || client
31ce2453c82fatagen2026-09-24 13:07 UTC363
                .session_tokens()
31ce2453c82fatagen2026-09-24 13:07 UTC364
                .is_none_or(|tokens| tokens.refresh_token.is_none())
31ce2453c82fatagen2026-09-24 13:07 UTC365
        {
31ce2453c82fatagen2026-09-24 13:07 UTC366
            return Err(Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC367
                ErrorKind::Unauthenticated,
31ce2453c82fatagen2026-09-24 13:07 UTC368
                "media download was not authorized",
31ce2453c82fatagen2026-09-24 13:07 UTC369
            ));
31ce2453c82fatagen2026-09-24 13:07 UTC370
        }
31ce2453c82fatagen2026-09-24 13:07 UTC371
        client.refresh_access_token().await.map_err(|source| {
31ce2453c82fatagen2026-09-24 13:07 UTC372
            Error::from_refresh_token("refreshing media access token failed", source)
31ce2453c82fatagen2026-09-24 13:07 UTC373
        })?;
31ce2453c82fatagen2026-09-24 13:07 UTC374
        refreshed = true;
31ce2453c82fatagen2026-09-24 13:07 UTC375
    };
31ce2453c82fatagen2026-09-24 13:07 UTC376
    let status = response.status();
31ce2453c82fatagen2026-09-24 13:07 UTC377
    if !status.is_success() {
31ce2453c82fatagen2026-09-24 13:07 UTC378
        return Err(Error::new(
31ce2453c82fatagen2026-09-24 13:07 UTC379
            ErrorKind::MediaUnavailable,
31ce2453c82fatagen2026-09-24 13:07 UTC380
            format!("media download failed with status {status}"),
31ce2453c82fatagen2026-09-24 13:07 UTC381
        ));
31ce2453c82fatagen2026-09-24 13:07 UTC382
    }
31ce2453c82fatagen2026-09-24 13:07 UTC383
    if response
31ce2453c82fatagen2026-09-24 13:07 UTC384
        .content_length()
31ce2453c82fatagen2026-09-24 13:07 UTC385
        .is_some_and(|length| length > max_bytes as u64)
31ce2453c82fatagen2026-09-24 13:07 UTC386
    {
31ce2453c82fatagen2026-09-24 13:07 UTC387
        return Err(too_large("media exceeds the configured byte limit"));
31ce2453c82fatagen2026-09-24 13:07 UTC388
    }
31ce2453c82fatagen2026-09-24 13:07 UTC389
    let mimetype = response
31ce2453c82fatagen2026-09-24 13:07 UTC390
        .headers()
31ce2453c82fatagen2026-09-24 13:07 UTC391
        .get(http::header::CONTENT_TYPE)
31ce2453c82fatagen2026-09-24 13:07 UTC392
        .and_then(|value| value.to_str().ok())
31ce2453c82fatagen2026-09-24 13:07 UTC393
        .map(str::to_owned);
31ce2453c82fatagen2026-09-24 13:07 UTC394
    let mut stream = response.bytes_stream();
31ce2453c82fatagen2026-09-24 13:07 UTC395
    let mut bytes = Vec::new();
31ce2453c82fatagen2026-09-24 13:07 UTC396
    while let Some(chunk) = stream.next().await {
31ce2453c82fatagen2026-09-24 13:07 UTC397
        let chunk = chunk.map_err(network_failed)?;
31ce2453c82fatagen2026-09-24 13:07 UTC398
        if chunk.len() > max_bytes.saturating_sub(bytes.len()) {
31ce2453c82fatagen2026-09-24 13:07 UTC399
            return Err(too_large(
31ce2453c82fatagen2026-09-24 13:07 UTC400
                "downloaded media exceeds the configured byte limit",
31ce2453c82fatagen2026-09-24 13:07 UTC401
            ));
31ce2453c82fatagen2026-09-24 13:07 UTC402
        }
31ce2453c82fatagen2026-09-24 13:07 UTC403
        bytes.extend_from_slice(&chunk);
31ce2453c82fatagen2026-09-24 13:07 UTC404
    }
31ce2453c82fatagen2026-09-24 13:07 UTC405
    Ok((bytes, mimetype))
31ce2453c82fatagen2026-09-24 13:07 UTC406
}
31ce2453c82fatagen2026-09-24 13:07 UTC407
31ce2453c82fatagen2026-09-24 13:07 UTC408
async fn media_http_request(
31ce2453c82fatagen2026-09-24 13:07 UTC409
    client: &Client,
31ce2453c82fatagen2026-09-24 13:07 UTC410
    request: &MediaRequestParameters,
31ce2453c82fatagen2026-09-24 13:07 UTC411
) -> Result<http::Request<Vec<u8>>, Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC412
    let versions = client.supported_versions().await.map_err(|source| {
31ce2453c82fatagen2026-09-24 13:07 UTC413
        Error::with_source(
31ce2453c82fatagen2026-09-24 13:07 UTC414
            ErrorKind::Network,
31ce2453c82fatagen2026-09-24 13:07 UTC415
            "reading supported server versions failed",
31ce2453c82fatagen2026-09-24 13:07 UTC416
            source,
31ce2453c82fatagen2026-09-24 13:07 UTC417
        )
31ce2453c82fatagen2026-09-24 13:07 UTC418
    })?;
31ce2453c82fatagen2026-09-24 13:07 UTC419
    let authenticated =
31ce2453c82fatagen2026-09-24 13:07 UTC420
        authenticated_media::get_content::v1::Request::PATH_BUILDER.is_supported(&versions);
31ce2453c82fatagen2026-09-24 13:07 UTC421
    let base = client.homeserver().to_string();
31ce2453c82fatagen2026-09-24 13:07 UTC422
    let token = client.access_token();
31ce2453c82fatagen2026-09-24 13:07 UTC423
    let invalid_uri =
31ce2453c82fatagen2026-09-24 13:07 UTC424
        |source| Error::with_source(ErrorKind::Invalid, "media URI is invalid", source);
31ce2453c82fatagen2026-09-24 13:07 UTC425
    let (uri, thumbnail) = match (&request.source, &request.format) {
31ce2453c82fatagen2026-09-24 13:07 UTC426
        (MediaSource::Encrypted(file), _) => (&file.url, None),
31ce2453c82fatagen2026-09-24 13:07 UTC427
        (MediaSource::Plain(uri), MediaFormat::File) => (uri, None),
31ce2453c82fatagen2026-09-24 13:07 UTC428
        (MediaSource::Plain(uri), MediaFormat::Thumbnail(settings)) => (uri, Some(settings)),
31ce2453c82fatagen2026-09-24 13:07 UTC429
    };
31ce2453c82fatagen2026-09-24 13:07 UTC430
    // each endpoint has its own auth input type, so no generic helper can take them all
31ce2453c82fatagen2026-09-24 13:07 UTC431
    macro_rules! into_http {
31ce2453c82fatagen2026-09-24 13:07 UTC432
        ($endpoint:expr, $token:expr) => {
31ce2453c82fatagen2026-09-24 13:07 UTC433
            $endpoint
31ce2453c82fatagen2026-09-24 13:07 UTC434
                .try_into_http_request::<Vec<u8>>(&base, $token, Cow::Borrowed(&versions))
31ce2453c82fatagen2026-09-24 13:07 UTC435
                .map_err(|source| {
31ce2453c82fatagen2026-09-24 13:07 UTC436
                    Error::with_source(ErrorKind::Other, "media request is invalid", source)
31ce2453c82fatagen2026-09-24 13:07 UTC437
                })?
31ce2453c82fatagen2026-09-24 13:07 UTC438
        };
31ce2453c82fatagen2026-09-24 13:07 UTC439
    }
31ce2453c82fatagen2026-09-24 13:07 UTC440
    let authed = SendAccessToken::IfRequired(token.as_deref().unwrap_or(""));
31ce2453c82fatagen2026-09-24 13:07 UTC441
    #[allow(deprecated)]
31ce2453c82fatagen2026-09-24 13:07 UTC442
    let mut http_request = match (thumbnail, authenticated) {
31ce2453c82fatagen2026-09-24 13:07 UTC443
        (None, true) => into_http!(
31ce2453c82fatagen2026-09-24 13:07 UTC444
            authenticated_media::get_content::v1::Request::from_uri(uri).map_err(invalid_uri)?,
31ce2453c82fatagen2026-09-24 13:07 UTC445
            authed
31ce2453c82fatagen2026-09-24 13:07 UTC446
        ),
31ce2453c82fatagen2026-09-24 13:07 UTC447
        (None, false) => into_http!(
31ce2453c82fatagen2026-09-24 13:07 UTC448
            media::get_content::v3::Request::from_url(uri).map_err(invalid_uri)?,
31ce2453c82fatagen2026-09-24 13:07 UTC449
            SendAccessToken::None
31ce2453c82fatagen2026-09-24 13:07 UTC450
        ),
31ce2453c82fatagen2026-09-24 13:07 UTC451
        (Some(settings), true) => {
31ce2453c82fatagen2026-09-24 13:07 UTC452
            let mut endpoint = authenticated_media::get_content_thumbnail::v1::Request::from_uri(
31ce2453c82fatagen2026-09-24 13:07 UTC453
                uri,
31ce2453c82fatagen2026-09-24 13:07 UTC454
                settings.width,
31ce2453c82fatagen2026-09-24 13:07 UTC455
                settings.height,
31ce2453c82fatagen2026-09-24 13:07 UTC456
            )
31ce2453c82fatagen2026-09-24 13:07 UTC457
            .map_err(invalid_uri)?;
31ce2453c82fatagen2026-09-24 13:07 UTC458
            endpoint.method = Some(settings.method.clone());
31ce2453c82fatagen2026-09-24 13:07 UTC459
            endpoint.animated = Some(settings.animated);
31ce2453c82fatagen2026-09-24 13:07 UTC460
            into_http!(endpoint, authed)
31ce2453c82fatagen2026-09-24 13:07 UTC461
        }
31ce2453c82fatagen2026-09-24 13:07 UTC462
        (Some(settings), false) => {
31ce2453c82fatagen2026-09-24 13:07 UTC463
            let mut endpoint = media::get_content_thumbnail::v3::Request::from_url(
31ce2453c82fatagen2026-09-24 13:07 UTC464
                uri,
31ce2453c82fatagen2026-09-24 13:07 UTC465
                settings.width,
31ce2453c82fatagen2026-09-24 13:07 UTC466
                settings.height,
31ce2453c82fatagen2026-09-24 13:07 UTC467
            )
31ce2453c82fatagen2026-09-24 13:07 UTC468
            .map_err(invalid_uri)?;
31ce2453c82fatagen2026-09-24 13:07 UTC469
            endpoint.method = Some(settings.method.clone());
31ce2453c82fatagen2026-09-24 13:07 UTC470
            endpoint.animated = Some(settings.animated);
31ce2453c82fatagen2026-09-24 13:07 UTC471
            into_http!(endpoint, SendAccessToken::None)
31ce2453c82fatagen2026-09-24 13:07 UTC472
        }
31ce2453c82fatagen2026-09-24 13:07 UTC473
    };
31ce2453c82fatagen2026-09-24 13:07 UTC474
    // legacy endpoints don't ask for a token but plenty of servers want one anyway
31ce2453c82fatagen2026-09-24 13:07 UTC475
    if let Some(token) = token.as_deref() {
31ce2453c82fatagen2026-09-24 13:07 UTC476
        let value = format!("Bearer {token}").parse().map_err(|_| {
31ce2453c82fatagen2026-09-24 13:07 UTC477
            Error::new(ErrorKind::Unauthenticated, "stored access token is invalid")
31ce2453c82fatagen2026-09-24 13:07 UTC478
        })?;
31ce2453c82fatagen2026-09-24 13:07 UTC479
        http_request
31ce2453c82fatagen2026-09-24 13:07 UTC480
            .headers_mut()
31ce2453c82fatagen2026-09-24 13:07 UTC481
            .insert(http::header::AUTHORIZATION, value);
31ce2453c82fatagen2026-09-24 13:07 UTC482
    }
31ce2453c82fatagen2026-09-24 13:07 UTC483
    Ok(http_request)
31ce2453c82fatagen2026-09-24 13:07 UTC484
}
31ce2453c82fatagen2026-09-24 13:07 UTC485
31ce2453c82fatagen2026-09-24 13:07 UTC486
pub(crate) async fn queue_upload(
31ce2453c82fatagen2026-09-24 13:07 UTC487
    room: Room,
31ce2453c82fatagen2026-09-24 13:07 UTC488
    upload: AttachmentUpload,
31ce2453c82fatagen2026-09-24 13:07 UTC489
    max_bytes: usize,
31ce2453c82fatagen2026-09-24 13:07 UTC490
) -> Result<(), Error> {
31ce2453c82fatagen2026-09-24 13:07 UTC491
    let (upload, mime, mut config) = prepare_upload(upload, max_bytes)?;
31ce2453c82fatagen2026-09-24 13:07 UTC492
    if let TimelineId::Thread { root_event_id, .. } = &upload.timeline_id {
31ce2453c82fatagen2026-09-24 13:07 UTC493
        config.reply = Some(SdkReply {
31ce2453c82fatagen2026-09-24 13:07 UTC494
            event_id: root_event_id.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC495
            enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
31ce2453c82fatagen2026-09-24 13:07 UTC496
            add_mentions: AddMentions::No,
31ce2453c82fatagen2026-09-24 13:07 UTC497
        });
31ce2453c82fatagen2026-09-24 13:07 UTC498
    }
31ce2453c82fatagen2026-09-24 13:07 UTC499
    room.send_queue()
31ce2453c82fatagen2026-09-24 13:07 UTC500
        .send_attachment(upload.filename, mime, upload.bytes, config)
31ce2453c82fatagen2026-09-24 13:07 UTC501
        .await
31ce2453c82fatagen2026-09-24 13:07 UTC502
        .map(drop)
31ce2453c82fatagen2026-09-24 13:07 UTC503
        .map_err(|source| {
31ce2453c82fatagen2026-09-24 13:07 UTC504
            Error::with_source(
31ce2453c82fatagen2026-09-24 13:07 UTC505
                ErrorKind::MediaUnavailable,
31ce2453c82fatagen2026-09-24 13:07 UTC506
                "media upload could not be queued",
31ce2453c82fatagen2026-09-24 13:07 UTC507
                source,
31ce2453c82fatagen2026-09-24 13:07 UTC508
            )
31ce2453c82fatagen2026-09-24 13:07 UTC509
        })
31ce2453c82fatagen2026-09-24 13:07 UTC510
}
31ce2453c82fatagen2026-09-24 13:07 UTC511
31ce2453c82fatagen2026-09-24 13:07 UTC512
/// least recently seen goes first. stale order slots are skipped by stamp rather than hunted down
31ce2453c82fatagen2026-09-24 13:07 UTC513
struct MediaRegistry {
31ce2453c82fatagen2026-09-24 13:07 UTC514
    entries: HashMap<MediaId, (MediaRegistration, u64)>,
31ce2453c82fatagen2026-09-24 13:07 UTC515
    order: VecDeque<(MediaId, u64)>,
31ce2453c82fatagen2026-09-24 13:07 UTC516
    next_stamp: u64,
31ce2453c82fatagen2026-09-24 13:07 UTC517
    limit: usize,
31ce2453c82fatagen2026-09-24 13:07 UTC518
}
31ce2453c82fatagen2026-09-24 13:07 UTC519
31ce2453c82fatagen2026-09-24 13:07 UTC520
impl MediaRegistry {
31ce2453c82fatagen2026-09-24 13:07 UTC521
    fn new(limit: usize) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC522
        Self {
31ce2453c82fatagen2026-09-24 13:07 UTC523
            entries: HashMap::new(),
31ce2453c82fatagen2026-09-24 13:07 UTC524
            order: VecDeque::new(),
31ce2453c82fatagen2026-09-24 13:07 UTC525
            next_stamp: 0,
31ce2453c82fatagen2026-09-24 13:07 UTC526
            limit,
31ce2453c82fatagen2026-09-24 13:07 UTC527
        }
31ce2453c82fatagen2026-09-24 13:07 UTC528
    }
31ce2453c82fatagen2026-09-24 13:07 UTC529
31ce2453c82fatagen2026-09-24 13:07 UTC530
    fn get(&self, id: &MediaId) -> Option<&MediaRegistration> {
31ce2453c82fatagen2026-09-24 13:07 UTC531
        self.entries.get(id).map(|(media, _)| media)
31ce2453c82fatagen2026-09-24 13:07 UTC532
    }
31ce2453c82fatagen2026-09-24 13:07 UTC533
31ce2453c82fatagen2026-09-24 13:07 UTC534
    fn insert_all(&mut self, media: Vec<MediaRegistration>) {
31ce2453c82fatagen2026-09-24 13:07 UTC535
        for media in media {
31ce2453c82fatagen2026-09-24 13:07 UTC536
            let stamp = self.next_stamp;
31ce2453c82fatagen2026-09-24 13:07 UTC537
            self.next_stamp += 1;
31ce2453c82fatagen2026-09-24 13:07 UTC538
            self.order.push_back((media.id.clone(), stamp));
31ce2453c82fatagen2026-09-24 13:07 UTC539
            self.entries.insert(media.id.clone(), (media, stamp));
31ce2453c82fatagen2026-09-24 13:07 UTC540
        }
31ce2453c82fatagen2026-09-24 13:07 UTC541
        while self.entries.len() > self.limit {
31ce2453c82fatagen2026-09-24 13:07 UTC542
            let Some((id, stamp)) = self.order.pop_front() else {
31ce2453c82fatagen2026-09-24 13:07 UTC543
                break;
31ce2453c82fatagen2026-09-24 13:07 UTC544
            };
31ce2453c82fatagen2026-09-24 13:07 UTC545
            if self.is_current(&id, stamp) {
31ce2453c82fatagen2026-09-24 13:07 UTC546
                self.entries.remove(&id);
31ce2453c82fatagen2026-09-24 13:07 UTC547
            }
31ce2453c82fatagen2026-09-24 13:07 UTC548
        }
31ce2453c82fatagen2026-09-24 13:07 UTC549
        if self.order.len() > self.limit.saturating_mul(4) {
31ce2453c82fatagen2026-09-24 13:07 UTC550
            let entries = &self.entries;
31ce2453c82fatagen2026-09-24 13:07 UTC551
            self.order
31ce2453c82fatagen2026-09-24 13:07 UTC552
                .retain(|(id, stamp)| entries.get(id).is_some_and(|(_, current)| current == stamp));
31ce2453c82fatagen2026-09-24 13:07 UTC553
        }
31ce2453c82fatagen2026-09-24 13:07 UTC554
    }
31ce2453c82fatagen2026-09-24 13:07 UTC555
31ce2453c82fatagen2026-09-24 13:07 UTC556
    fn is_current(&self, id: &MediaId, stamp: u64) -> bool {
31ce2453c82fatagen2026-09-24 13:07 UTC557
        self.entries
31ce2453c82fatagen2026-09-24 13:07 UTC558
            .get(id)
31ce2453c82fatagen2026-09-24 13:07 UTC559
            .is_some_and(|(_, current)| *current == stamp)
31ce2453c82fatagen2026-09-24 13:07 UTC560
    }
31ce2453c82fatagen2026-09-24 13:07 UTC561
31ce2453c82fatagen2026-09-24 13:07 UTC562
    fn release(&mut self, scope: &MediaScope) {
31ce2453c82fatagen2026-09-24 13:07 UTC563
        self.entries.retain(|id, _| !scope.owns(id));
31ce2453c82fatagen2026-09-24 13:07 UTC564
        let entries = &self.entries;
31ce2453c82fatagen2026-09-24 13:07 UTC565
        self.order.retain(|(id, _)| entries.contains_key(id));
31ce2453c82fatagen2026-09-24 13:07 UTC566
    }
31ce2453c82fatagen2026-09-24 13:07 UTC567
31ce2453c82fatagen2026-09-24 13:07 UTC568
    fn clear(&mut self) {
31ce2453c82fatagen2026-09-24 13:07 UTC569
        self.entries.clear();
31ce2453c82fatagen2026-09-24 13:07 UTC570
        self.order.clear();
31ce2453c82fatagen2026-09-24 13:07 UTC571
    }
31ce2453c82fatagen2026-09-24 13:07 UTC572
}