matrix_sdk_base/media/
mod.rs

1// Copyright 2025 Kévin Commaille
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Media store and common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
16
17pub mod store;
18
19use ruma::{
20    MxcUri, UInt,
21    api::client::media::get_content_thumbnail::v3::Method,
22    events::{
23        room::{
24            MediaSource,
25            message::{
26                AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent,
27                LocationMessageEventContent, VideoMessageEventContent,
28            },
29        },
30        sticker::StickerEventContent,
31    },
32};
33use serde::{Deserialize, Serialize};
34
35const UNIQUE_SEPARATOR: &str = "_";
36
37/// A trait to uniquely identify values of the same type.
38pub trait UniqueKey {
39    /// A string that uniquely identifies `Self` compared to other values of
40    /// the same type.
41    fn unique_key(&self) -> String;
42}
43
44/// The requested format of a media file.
45#[derive(Clone, Debug, Serialize, Deserialize)]
46pub enum MediaFormat {
47    /// The file that was uploaded.
48    File,
49
50    /// A thumbnail of the file that was uploaded.
51    Thumbnail(MediaThumbnailSettings),
52}
53
54impl UniqueKey for MediaFormat {
55    fn unique_key(&self) -> String {
56        match self {
57            Self::File => "file".into(),
58            Self::Thumbnail(settings) => settings.unique_key(),
59        }
60    }
61}
62
63/// The desired settings of a media thumbnail.
64#[derive(Clone, Debug, Serialize, Deserialize)]
65pub struct MediaThumbnailSettings {
66    /// The desired resizing method.
67    pub method: Method,
68
69    /// The desired width of the thumbnail. The actual thumbnail may not match
70    /// the size specified.
71    pub width: UInt,
72
73    /// The desired height of the thumbnail. The actual thumbnail may not match
74    /// the size specified.
75    pub height: UInt,
76
77    /// If we want to request an animated thumbnail from the homeserver.
78    ///
79    /// If it is `true`, the server should return an animated thumbnail if
80    /// the media supports it.
81    ///
82    /// Defaults to `false`.
83    pub animated: bool,
84}
85
86impl MediaThumbnailSettings {
87    /// Constructs a new `MediaThumbnailSettings` with the given method, width
88    /// and height.
89    ///
90    /// Requests a non-animated thumbnail by default.
91    pub fn with_method(method: Method, width: UInt, height: UInt) -> Self {
92        Self { method, width, height, animated: false }
93    }
94
95    /// Constructs a new `MediaThumbnailSettings` with the given width and
96    /// height.
97    ///
98    /// Requests scaling, and a non-animated thumbnail.
99    pub fn new(width: UInt, height: UInt) -> Self {
100        Self { method: Method::Scale, width, height, animated: false }
101    }
102}
103
104impl UniqueKey for MediaThumbnailSettings {
105    fn unique_key(&self) -> String {
106        let mut key = format!("{}{UNIQUE_SEPARATOR}{}x{}", self.method, self.width, self.height);
107
108        if self.animated {
109            key.push_str(UNIQUE_SEPARATOR);
110            key.push_str("animated");
111        }
112
113        key
114    }
115}
116
117impl UniqueKey for MediaSource {
118    fn unique_key(&self) -> String {
119        match self {
120            Self::Plain(uri) => uri.to_string(),
121            Self::Encrypted(file) => file.url.to_string(),
122        }
123    }
124}
125
126/// Parameters for a request for retrieve media data.
127///
128/// This is used as a key in the media cache too.
129#[derive(Clone, Debug, Serialize, Deserialize)]
130pub struct MediaRequestParameters {
131    /// The source of the media file.
132    pub source: MediaSource,
133
134    /// The requested format of the media data.
135    pub format: MediaFormat,
136}
137
138impl MediaRequestParameters {
139    /// Get the [`MxcUri`] from `Self`.
140    pub fn uri(&self) -> &MxcUri {
141        match &self.source {
142            MediaSource::Plain(url) => url.as_ref(),
143            MediaSource::Encrypted(file) => file.url.as_ref(),
144        }
145    }
146}
147
148impl UniqueKey for MediaRequestParameters {
149    fn unique_key(&self) -> String {
150        format!("{}{UNIQUE_SEPARATOR}{}", self.source.unique_key(), self.format.unique_key())
151    }
152}
153
154/// Trait for media event content.
155pub trait MediaEventContent {
156    /// Get the source of the file for `Self`.
157    ///
158    /// Returns `None` if `Self` has no file.
159    fn source(&self) -> Option<MediaSource>;
160
161    /// Get the source of the thumbnail for `Self`.
162    ///
163    /// Returns `None` if `Self` has no thumbnail.
164    fn thumbnail_source(&self) -> Option<MediaSource>;
165}
166
167impl MediaEventContent for StickerEventContent {
168    fn source(&self) -> Option<MediaSource> {
169        Some(MediaSource::from(self.source.clone()))
170    }
171
172    fn thumbnail_source(&self) -> Option<MediaSource> {
173        None
174    }
175}
176
177impl MediaEventContent for AudioMessageEventContent {
178    fn source(&self) -> Option<MediaSource> {
179        Some(self.source.clone())
180    }
181
182    fn thumbnail_source(&self) -> Option<MediaSource> {
183        None
184    }
185}
186
187impl MediaEventContent for FileMessageEventContent {
188    fn source(&self) -> Option<MediaSource> {
189        Some(self.source.clone())
190    }
191
192    fn thumbnail_source(&self) -> Option<MediaSource> {
193        self.info.as_ref()?.thumbnail_source.clone()
194    }
195}
196
197impl MediaEventContent for ImageMessageEventContent {
198    fn source(&self) -> Option<MediaSource> {
199        Some(self.source.clone())
200    }
201
202    fn thumbnail_source(&self) -> Option<MediaSource> {
203        self.info
204            .as_ref()
205            .and_then(|info| info.thumbnail_source.clone())
206            .or_else(|| Some(self.source.clone()))
207    }
208}
209
210impl MediaEventContent for VideoMessageEventContent {
211    fn source(&self) -> Option<MediaSource> {
212        Some(self.source.clone())
213    }
214
215    fn thumbnail_source(&self) -> Option<MediaSource> {
216        self.info
217            .as_ref()
218            .and_then(|info| info.thumbnail_source.clone())
219            .or_else(|| Some(self.source.clone()))
220    }
221}
222
223impl MediaEventContent for LocationMessageEventContent {
224    fn source(&self) -> Option<MediaSource> {
225        None
226    }
227
228    fn thumbnail_source(&self) -> Option<MediaSource> {
229        self.info.as_ref()?.thumbnail_source.clone()
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use ruma::mxc_uri;
236    use serde_json::json;
237
238    use super::*;
239
240    #[test]
241    fn test_media_request_url() {
242        let mxc_uri = mxc_uri!("mxc://homeserver/media");
243
244        let plain = MediaRequestParameters {
245            source: MediaSource::Plain(mxc_uri.to_owned()),
246            format: MediaFormat::File,
247        };
248
249        assert_eq!(plain.uri(), mxc_uri);
250
251        let file = MediaRequestParameters {
252            source: MediaSource::Encrypted(Box::new(
253                serde_json::from_value(json!({
254                    "url": mxc_uri,
255                    "key": {
256                        "kty": "oct",
257                        "key_ops": ["encrypt", "decrypt"],
258                        "alg": "A256CTR",
259                        "k": "b50ACIv6LMn9AfMCFD1POJI_UAFWIclxAN1kWrEO2X8",
260                        "ext": true,
261                    },
262                    "iv": "AK1wyzigZtQAAAABAAAAKK",
263                    "hashes": {
264                        "sha256": "foobar",
265                    },
266                    "v": "v2",
267                }))
268                .unwrap(),
269            )),
270            format: MediaFormat::File,
271        };
272
273        assert_eq!(file.uri(), mxc_uri);
274    }
275}