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