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, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 7 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 8 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 9 | use futures_util::{ |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 10 | FutureExt, StreamExt, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 11 | future::{BoxFuture, Shared, WeakShared}, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 12 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 13 | use matrix_sdk::{ |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 14 | Client, Room, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 15 | media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 16 | room::reply::{EnforceThread, Reply as SdkReply}, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 17 | ruma::{ |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 18 | api::{ |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 19 | Metadata, OutgoingRequest, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 20 | auth_scheme::SendAccessToken, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 21 | client::{authenticated_media, media}, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 22 | }, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 23 | events::room::{ |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 24 | MediaSource, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 25 | message::{AddMentions, ReplyWithinThread}, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 26 | }, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 27 | }, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 28 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 29 | use matrix_sdk_crypto::{AttachmentDecryptor, MediaEncryptionInfo}; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 30 | use ruma::MxcUri; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 31 | use tokio::sync::Mutex; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 32 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 33 | use crate::{ |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 34 | AttachmentUpload, Error, ErrorKind, Media, MediaId, MediaKind, MediaSize, TimelineId, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 35 | normalise::{MediaRegistration, MediaScope}, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 36 | upload::prepare_upload, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 37 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 38 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 39 | /// the uploader's thumbnail is used when there is one, this is only what we ask the server for |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 40 | const TIMELINE_THUMBNAIL: u32 = 800; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 41 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 42 | type FlightFuture = BoxFuture<'static, Result<Media, Error>>; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 43 | type Flight = Shared<FlightFuture>; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 44 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 45 | /// owns everything needed to turn a media id or an mxc uri into bytes, so the |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 46 | /// handle can answer media queries without a trip through the engine |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 47 | pub(crate) struct MediaService { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 48 | client: Client, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 49 | registry: Mutex<MediaRegistry>, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 50 | flights: Mutex<HashMap<String, WeakShared<FlightFuture>>>, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 51 | max_bytes: usize, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 52 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 53 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 54 | impl MediaService { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 55 | pub(crate) fn new(client: Client, max_bytes: usize, max_sources: usize) -> Self { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 56 | Self { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 57 | client, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 58 | registry: Mutex::new(MediaRegistry::new(max_sources)), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 59 | flights: Mutex::new(HashMap::new()), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 60 | max_bytes, |
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 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 64 | pub(crate) async fn register(&self, media: Vec<MediaRegistration>) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 65 | if !media.is_empty() { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 66 | self.registry.lock().await.insert_all(media); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 67 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 68 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 69 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 70 | pub(crate) async fn release(&self, scope: &MediaScope) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 71 | self.registry.lock().await.release(scope); |
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 | pub(crate) async fn clear(&self) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 75 | self.registry.lock().await.clear(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 76 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 77 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 78 | pub(crate) async fn timeline_media( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 79 | &self, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 80 | media_id: &MediaId, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 81 | size: MediaSize, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 82 | ) -> Result<Media, Error> { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 83 | let media = self |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 84 | .registry |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 85 | .lock() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 86 | .await |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 87 | .get(media_id) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 88 | .cloned() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 89 | .ok_or_else(|| { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 90 | Error::new( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 91 | ErrorKind::MediaUnknown, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 92 | "media is not registered in a retained timeline", |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 93 | ) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 94 | })?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 95 | let (source, format, mimetype, declared_size) = match size { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 96 | MediaSize::Full => ( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 97 | media.full.source, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 98 | MediaFormat::File, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 99 | media.full.mimetype, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 100 | media.full.declared_size, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 101 | ), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 102 | MediaSize::Thumbnail { .. } => match media.thumbnail { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 103 | Some(thumbnail) => ( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 104 | thumbnail.source, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 105 | MediaFormat::File, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 106 | thumbnail.mimetype, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 107 | thumbnail.declared_size, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 108 | ), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 109 | // the server can only resize what it can read |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 110 | None if matches!(media.kind, MediaKind::Image | MediaKind::Video) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 111 | && matches!(media.full.source, MediaSource::Plain(_)) => |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 112 | { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 113 | ( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 114 | media.full.source, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 115 | thumbnail_format(TIMELINE_THUMBNAIL, TIMELINE_THUMBNAIL), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 116 | media.full.mimetype, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 117 | None, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 118 | ) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 119 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 120 | None => { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 121 | return Err(Error::new( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 122 | ErrorKind::MediaUnavailable, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 123 | "the requested media size is unavailable", |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 124 | )); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 125 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 126 | }, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 127 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 128 | if declared_size.is_some_and(|size| size > self.max_bytes as u64) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 129 | return Err(too_large("media exceeds the configured byte limit")); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 130 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 131 | self.fetch(MediaRequestParameters { source, format }, mimetype) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 132 | .await |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 133 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 134 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 135 | pub(crate) async fn mxc_media(&self, uri: &MxcUri, size: MediaSize) -> Result<Media, Error> { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 136 | if !uri.is_valid() { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 137 | return Err(Error::new(ErrorKind::Invalid, "mxc uri is malformed")); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 138 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 139 | let format = match size { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 140 | MediaSize::Full => MediaFormat::File, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 141 | MediaSize::Thumbnail { width, height } => thumbnail_format(width, height), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 142 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 143 | self.fetch( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 144 | MediaRequestParameters { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 145 | source: MediaSource::Plain(uri.to_owned()), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 146 | format, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 147 | }, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 148 | None, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 149 | ) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 150 | .await |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 151 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 152 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 153 | async fn fetch( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 154 | &self, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 155 | request: MediaRequestParameters, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 156 | mimetype: Option<String>, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 157 | ) -> Result<Media, Error> { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 158 | let key = flight_key(&request); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 159 | let flight = { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 160 | let mut flights = self.flights.lock().await; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 161 | flights.retain(|_, flight| flight.upgrade().is_some()); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 162 | match flights.get(&key).and_then(WeakShared::upgrade) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 163 | Some(flight) => flight, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 164 | None => { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 165 | let flight: Flight = AssertUnwindSafe(fetch( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 166 | self.client.clone(), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 167 | request, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 168 | mimetype, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 169 | self.max_bytes, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 170 | )) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 171 | .catch_unwind() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 172 | .map(|result| { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 173 | result.unwrap_or_else(|_| { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 174 | Err(Error::new(ErrorKind::Other, "media fetch panicked")) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 175 | }) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 176 | }) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 177 | .boxed() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 178 | .shared(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 179 | if let Some(weak) = flight.downgrade() { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 180 | flights.insert(key, weak); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 181 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 182 | flight |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 183 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 184 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 185 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 186 | flight.await |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 187 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 188 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 189 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 190 | fn thumbnail_format(width: u32, height: u32) -> MediaFormat { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 191 | MediaFormat::Thumbnail(MediaThumbnailSettings::new(width.into(), height.into())) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 192 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 193 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 194 | fn flight_key(request: &MediaRequestParameters) -> String { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 195 | let uri = match &request.source { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 196 | MediaSource::Plain(uri) => uri.as_str(), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 197 | MediaSource::Encrypted(file) => file.url.as_str(), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 198 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 199 | match &request.format { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 200 | MediaFormat::File => uri.to_owned(), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 201 | MediaFormat::Thumbnail(settings) => format!( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 202 | "{uri}|{}|{}x{}|{}", |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 203 | settings.method, settings.width, settings.height, settings.animated |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 204 | ), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 205 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 206 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 207 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 208 | fn too_large(message: &'static str) -> Error { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 209 | Error::new(ErrorKind::MediaTooLarge, message) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 210 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 211 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 212 | async fn fetch( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 213 | client: Client, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 214 | request: MediaRequestParameters, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 215 | mimetype: Option<String>, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 216 | max_bytes: usize, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 217 | ) -> Result<Media, Error> { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 218 | if let Some(cached) = cached_media(&client, &request).await? { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 219 | if cached.len() > max_bytes { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 220 | if let Ok(store) = client.media_store().lock().await { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 221 | let _ = store.remove_media_content(&request).await; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 222 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 223 | return Err(too_large("cached media exceeds the configured byte limit")); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 224 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 225 | return Ok(media(cached, mimetype)); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 226 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 227 | let (bytes, served_mimetype) = download_bounded(&client, &request, max_bytes).await?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 228 | // 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 | 229 | let bytes = maybe_decrypt(bytes, &request, max_bytes)?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 230 | if let Ok(store) = client.media_store().lock().await { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 231 | let _ = store |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 232 | .add_media_content( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 233 | &request, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 234 | bytes.clone(), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 235 | matrix_sdk::media::store::IgnoreMediaRetentionPolicy::No, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 236 | ) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 237 | .await; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 238 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 239 | Ok(media(bytes, mimetype.or(served_mimetype))) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 240 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 241 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 242 | fn media(bytes: Vec<u8>, mimetype: Option<String>) -> Media { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 243 | let mimetype = mimetype |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 244 | .or_else(|| sniff_image(&bytes).map(str::to_owned)) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 245 | .unwrap_or_else(|| "application/octet-stream".to_owned()); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 246 | Media { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 247 | bytes: Arc::from(bytes), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 248 | mimetype, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 249 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 250 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 251 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 252 | /// the cache forgets content types, and avatars never declare one. images are the common case |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 253 | fn sniff_image(bytes: &[u8]) -> Option<&'static str> { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 254 | match bytes { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 255 | [0x89, b'P', b'N', b'G', ..] => Some("image/png"), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 256 | [0xff, 0xd8, 0xff, ..] => Some("image/jpeg"), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 257 | [b'G', b'I', b'F', b'8', ..] => Some("image/gif"), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 258 | [ |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 259 | b'R', |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 260 | b'I', |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 261 | b'F', |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 262 | b'F', |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 263 | _, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 264 | _, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 265 | _, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 266 | _, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 267 | b'W', |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 268 | b'E', |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 269 | b'B', |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 270 | b'P', |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 271 | .., |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 272 | ] => Some("image/webp"), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 273 | _ => None, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 274 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 275 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 276 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 277 | async fn cached_media( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 278 | client: &Client, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 279 | request: &MediaRequestParameters, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 280 | ) -> Result<Option<Vec<u8>>, Error> { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 281 | fn cache_failed(source: impl std::error::Error + Send + Sync + 'static) -> Error { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 282 | Error::with_source(ErrorKind::Store, "reading media cache failed", source) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 283 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 284 | client |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 285 | .media_store() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 286 | .lock() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 287 | .await |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 288 | .map_err(cache_failed)? |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 289 | .get_media_content(request) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 290 | .await |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 291 | .map_err(cache_failed) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 292 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 293 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 294 | fn maybe_decrypt( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 295 | bytes: Vec<u8>, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 296 | request: &MediaRequestParameters, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 297 | max_bytes: usize, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 298 | ) -> Result<Vec<u8>, Error> { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 299 | let MediaSource::Encrypted(file) = &request.source else { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 300 | return Ok(bytes); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 301 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 302 | fn decrypt_failed(source: impl std::error::Error + Send + Sync + 'static) -> Error { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 303 | Error::with_source( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 304 | ErrorKind::MediaUnavailable, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 305 | "media decryption failed", |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 306 | source, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 307 | ) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 308 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 309 | let info: MediaEncryptionInfo = (**file).clone().into(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 310 | let mut cursor = std::io::Cursor::new(bytes); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 311 | let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).map_err(decrypt_failed)?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 312 | let mut decrypted = Vec::new(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 313 | let mut chunk = [0_u8; 8192]; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 314 | loop { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 315 | let read = decryptor.read(&mut chunk).map_err(decrypt_failed)?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 316 | if read == 0 { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 317 | return Ok(decrypted); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 318 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 319 | if read > max_bytes.saturating_sub(decrypted.len()) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 320 | return Err(too_large( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 321 | "decrypted media exceeds the configured byte limit", |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 322 | )); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 323 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 324 | decrypted.extend_from_slice(&chunk[..read]); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 325 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 326 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 327 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 328 | /// the sdk's own download has no size cap and no timeout, so we do it by hand |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 329 | async fn download_bounded( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 330 | client: &Client, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 331 | request: &MediaRequestParameters, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 332 | max_bytes: usize, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 333 | ) -> Result<(Vec<u8>, Option<String>), Error> { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 334 | let uri = match &request.source { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 335 | MediaSource::Plain(uri) => uri, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 336 | MediaSource::Encrypted(file) => &file.url, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 337 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 338 | if uri |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 339 | .server_name() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 340 | .is_ok_and(|server| server.as_str() == "send-queue.localhost") |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 341 | { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 342 | return Err(Error::new( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 343 | ErrorKind::MediaUnavailable, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 344 | "local media is not in the media cache", |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 345 | )); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 346 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 347 | let network_failed = |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 348 | |source| Error::with_source(ErrorKind::Network, "media download failed", source); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 349 | let mut refreshed = false; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 350 | let response = loop { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 351 | let http_request = media_http_request(client, request).await?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 352 | let (parts, _) = http_request.into_parts(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 353 | let mut builder = client.http_client().get(parts.uri.to_string()); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 354 | for (name, value) in &parts.headers { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 355 | builder = builder.header(name.as_str(), value.as_bytes()); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 356 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 357 | let response = builder.send().await.map_err(network_failed)?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 358 | if response.status() != matrix_sdk::reqwest::StatusCode::UNAUTHORIZED { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 359 | break response; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 360 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 361 | if refreshed |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 362 | || client |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 363 | .session_tokens() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 364 | .is_none_or(|tokens| tokens.refresh_token.is_none()) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 365 | { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 366 | return Err(Error::new( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 367 | ErrorKind::Unauthenticated, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 368 | "media download was not authorized", |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 369 | )); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 370 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 371 | client.refresh_access_token().await.map_err(|source| { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 372 | Error::from_refresh_token("refreshing media access token failed", source) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 373 | })?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 374 | refreshed = true; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 375 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 376 | let status = response.status(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 377 | if !status.is_success() { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 378 | return Err(Error::new( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 379 | ErrorKind::MediaUnavailable, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 380 | format!("media download failed with status {status}"), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 381 | )); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 382 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 383 | if response |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 384 | .content_length() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 385 | .is_some_and(|length| length > max_bytes as u64) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 386 | { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 387 | return Err(too_large("media exceeds the configured byte limit")); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 388 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 389 | let mimetype = response |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 390 | .headers() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 391 | .get(http::header::CONTENT_TYPE) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 392 | .and_then(|value| value.to_str().ok()) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 393 | .map(str::to_owned); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 394 | let mut stream = response.bytes_stream(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 395 | let mut bytes = Vec::new(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 396 | while let Some(chunk) = stream.next().await { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 397 | let chunk = chunk.map_err(network_failed)?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 398 | if chunk.len() > max_bytes.saturating_sub(bytes.len()) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 399 | return Err(too_large( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 400 | "downloaded media exceeds the configured byte limit", |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 401 | )); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 402 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 403 | bytes.extend_from_slice(&chunk); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 404 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 405 | Ok((bytes, mimetype)) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 406 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 407 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 408 | async fn media_http_request( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 409 | client: &Client, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 410 | request: &MediaRequestParameters, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 411 | ) -> Result<http::Request<Vec<u8>>, Error> { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 412 | let versions = client.supported_versions().await.map_err(|source| { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 413 | Error::with_source( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 414 | ErrorKind::Network, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 415 | "reading supported server versions failed", |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 416 | source, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 417 | ) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 418 | })?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 419 | let authenticated = |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 420 | authenticated_media::get_content::v1::Request::PATH_BUILDER.is_supported(&versions); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 421 | let base = client.homeserver().to_string(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 422 | let token = client.access_token(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 423 | let invalid_uri = |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 424 | |source| Error::with_source(ErrorKind::Invalid, "media URI is invalid", source); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 425 | let (uri, thumbnail) = match (&request.source, &request.format) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 426 | (MediaSource::Encrypted(file), _) => (&file.url, None), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 427 | (MediaSource::Plain(uri), MediaFormat::File) => (uri, None), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 428 | (MediaSource::Plain(uri), MediaFormat::Thumbnail(settings)) => (uri, Some(settings)), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 429 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 430 | // each endpoint has its own auth input type, so no generic helper can take them all |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 431 | macro_rules! into_http { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 432 | ($endpoint:expr, $token:expr) => { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 433 | $endpoint |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 434 | .try_into_http_request::<Vec<u8>>(&base, $token, Cow::Borrowed(&versions)) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 435 | .map_err(|source| { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 436 | Error::with_source(ErrorKind::Other, "media request is invalid", source) |
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 | let authed = SendAccessToken::IfRequired(token.as_deref().unwrap_or("")); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 441 | #[allow(deprecated)] |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 442 | let mut http_request = match (thumbnail, authenticated) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 443 | (None, true) => into_http!( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 444 | authenticated_media::get_content::v1::Request::from_uri(uri).map_err(invalid_uri)?, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 445 | authed |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 446 | ), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 447 | (None, false) => into_http!( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 448 | media::get_content::v3::Request::from_url(uri).map_err(invalid_uri)?, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 449 | SendAccessToken::None |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 450 | ), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 451 | (Some(settings), true) => { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 452 | let mut endpoint = authenticated_media::get_content_thumbnail::v1::Request::from_uri( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 453 | uri, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 454 | settings.width, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 455 | settings.height, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 456 | ) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 457 | .map_err(invalid_uri)?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 458 | endpoint.method = Some(settings.method.clone()); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 459 | endpoint.animated = Some(settings.animated); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 460 | into_http!(endpoint, authed) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 461 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 462 | (Some(settings), false) => { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 463 | let mut endpoint = media::get_content_thumbnail::v3::Request::from_url( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 464 | uri, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 465 | settings.width, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 466 | settings.height, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 467 | ) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 468 | .map_err(invalid_uri)?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 469 | endpoint.method = Some(settings.method.clone()); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 470 | endpoint.animated = Some(settings.animated); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 471 | into_http!(endpoint, SendAccessToken::None) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 472 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 473 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 474 | // legacy endpoints don't ask for a token but plenty of servers want one anyway |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 475 | if let Some(token) = token.as_deref() { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 476 | let value = format!("Bearer {token}").parse().map_err(|_| { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 477 | Error::new(ErrorKind::Unauthenticated, "stored access token is invalid") |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 478 | })?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 479 | http_request |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 480 | .headers_mut() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 481 | .insert(http::header::AUTHORIZATION, value); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 482 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 483 | Ok(http_request) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 484 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 485 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 486 | pub(crate) async fn queue_upload( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 487 | room: Room, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 488 | upload: AttachmentUpload, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 489 | max_bytes: usize, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 490 | ) -> Result<(), Error> { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 491 | let (upload, mime, mut config) = prepare_upload(upload, max_bytes)?; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 492 | if let TimelineId::Thread { root_event_id, .. } = &upload.timeline_id { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 493 | config.reply = Some(SdkReply { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 494 | event_id: root_event_id.clone(), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 495 | enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 496 | add_mentions: AddMentions::No, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 497 | }); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 498 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 499 | room.send_queue() |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 500 | .send_attachment(upload.filename, mime, upload.bytes, config) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 501 | .await |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 502 | .map(drop) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 503 | .map_err(|source| { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 504 | Error::with_source( |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 505 | ErrorKind::MediaUnavailable, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 506 | "media upload could not be queued", |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 507 | source, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 508 | ) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 509 | }) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 510 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 511 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 512 | /// least recently seen goes first. stale order slots are skipped by stamp rather than hunted down |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 513 | struct MediaRegistry { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 514 | entries: HashMap<MediaId, (MediaRegistration, u64)>, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 515 | order: VecDeque<(MediaId, u64)>, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 516 | next_stamp: u64, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 517 | limit: usize, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 518 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 519 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 520 | impl MediaRegistry { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 521 | fn new(limit: usize) -> Self { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 522 | Self { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 523 | entries: HashMap::new(), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 524 | order: VecDeque::new(), |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 525 | next_stamp: 0, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 526 | limit, |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 527 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 528 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 529 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 530 | fn get(&self, id: &MediaId) -> Option<&MediaRegistration> { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 531 | self.entries.get(id).map(|(media, _)| media) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 532 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 533 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 534 | fn insert_all(&mut self, media: Vec<MediaRegistration>) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 535 | for media in media { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 536 | let stamp = self.next_stamp; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 537 | self.next_stamp += 1; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 538 | self.order.push_back((media.id.clone(), stamp)); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 539 | self.entries.insert(media.id.clone(), (media, stamp)); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 540 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 541 | while self.entries.len() > self.limit { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 542 | let Some((id, stamp)) = self.order.pop_front() else { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 543 | break; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 544 | }; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 545 | if self.is_current(&id, stamp) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 546 | self.entries.remove(&id); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 547 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 548 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 549 | if self.order.len() > self.limit.saturating_mul(4) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 550 | let entries = &self.entries; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 551 | self.order |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 552 | .retain(|(id, stamp)| entries.get(id).is_some_and(|(_, current)| current == stamp)); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 553 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 554 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 555 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 556 | fn is_current(&self, id: &MediaId, stamp: u64) -> bool { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 557 | self.entries |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 558 | .get(id) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 559 | .is_some_and(|(_, current)| *current == stamp) |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 560 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 561 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 562 | fn release(&mut self, scope: &MediaScope) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 563 | self.entries.retain(|id, _| !scope.owns(id)); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 564 | let entries = &self.entries; |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 565 | self.order.retain(|(id, _)| entries.contains_key(id)); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 566 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 567 | |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 568 | fn clear(&mut self) { |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 569 | self.entries.clear(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 570 | self.order.clear(); |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 571 | } |
31ce2453c82f | atagen | 2026-09-24 13:07 UTC | 572 | } |