atagen

macltok

public
Star0
Watch0
beea75a1f14613acd309e69836163d5a23851aea
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/normalise/media.rsRust396 lines12.8 KB
1
use std::sync::atomic::{AtomicU64, Ordering};
2
3
use matrix_sdk::{media::MediaEventContent, ruma::events::room::MediaSource};
4
use matrix_sdk_ui::timeline::{MsgLikeKind, TimelineItem as SdkTimelineItem, TimelineItemContent};
5
use ruma::{
6
    OwnedMxcUri,
7
    events::{
8
        room::{
9
            ImageInfo, ThumbnailInfo,
10
            message::{FileInfo, MessageType, VideoInfo},
11
        },
12
        sticker::{StickerEventContent, StickerMediaSource},
13
    },
14
};
15
16
use super::link_preview::sender_previews;
17
use crate::{MediaDescriptor, MediaId, MediaKind};
18
19
static NEXT_MEDIA_SCOPE: AtomicU64 = AtomicU64::new(1);
20
21
/// Per-timeline-session nonce namespacing recycled SDK unique IDs.
22
///
23
/// SDK 0.18 recycles [`SdkTimelineItem::unique_id`] across the local-to-remote
24
/// echo replacement, while transaction IDs disappear. Scoping the recycled ID
25
/// by a process-wide nonce keeps media IDs stable across replacement without
26
/// colliding across rooms, threads, or reopened sessions.
27
///
28
/// It is also the owner its registrations belong to. Media stays registered while any
29
/// scope that registered it is open, and a bounded scope drops its least recently used
30
/// entries past its limit without touching anyone else's.
31
#[derive(Clone, Debug)]
32
pub(crate) struct MediaScope {
33
    nonce: u64,
34
    limit: Option<usize>,
35
}
36
37
impl MediaScope {
38
    /// for a timeline, which holds what is on screen and so is never trimmed
39
    pub(crate) fn new() -> Self {
40
        Self {
41
            nonce: NEXT_MEDIA_SCOPE.fetch_add(1, Ordering::Relaxed),
42
            limit: None,
43
        }
44
    }
45
46
    pub(crate) fn bounded(limit: usize) -> Self {
47
        Self {
48
            limit: Some(limit.max(1)),
49
            ..Self::new()
50
        }
51
    }
52
53
    pub(crate) const fn nonce(&self) -> u64 {
54
        self.nonce
55
    }
56
57
    pub(crate) const fn limit(&self) -> Option<usize> {
58
        self.limit
59
    }
60
}
61
62
#[derive(Clone)]
63
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64
pub(crate) struct MediaRegistration {
65
    pub(crate) id: MediaId,
66
    pub(crate) kind: MediaKind,
67
    pub(crate) full: MediaSourceInfo,
68
    pub(crate) thumbnail: Option<MediaSourceInfo>,
69
}
70
71
#[derive(Clone)]
72
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73
pub(crate) struct MediaSourceInfo {
74
    pub(crate) source: MediaSource,
75
    pub(crate) mimetype: Option<String>,
76
    pub(crate) declared_size: Option<u64>,
77
}
78
79
pub(super) fn media_registrations_for_item(
80
    item: &SdkTimelineItem,
81
    scope: &MediaScope,
82
) -> Vec<MediaRegistration> {
83
    let Some(event) = item.as_event() else {
84
        return Vec::new();
85
    };
86
    let identity = media_identity(item, scope);
87
    let TimelineItemContent::MsgLike(content) = event.content() else {
88
        return Vec::new();
89
    };
90
    match &content.kind {
91
        MsgLikeKind::Message(message) => media_registration(&identity, message.msgtype())
92
            .into_iter()
93
            .chain(
94
                sender_previews(Some(&identity), message.msgtype())
95
                    .into_iter()
96
                    .filter_map(|(_, registration)| registration),
97
            )
98
            .collect(),
99
        MsgLikeKind::Sticker(sticker) => media_sticker_registration(&identity, sticker.content())
100
            .into_iter()
101
            .collect(),
102
        _ => Vec::new(),
103
    }
104
}
105
106
pub(super) fn media_identity(item: &SdkTimelineItem, scope: &MediaScope) -> String {
107
    format!("{}#{}", scope.nonce, item.unique_id().0)
108
}
109
110
pub(super) fn media_from_message(identity: &str, message: &MessageType) -> Option<MediaDescriptor> {
111
    let descriptor =
112
        |kind, source: &MediaSource, filename: &str, caption: Option<&str>| MediaDescriptor {
113
            id: media_id(identity, "media"),
114
            kind,
115
            mxc: plain_mxc(source),
116
            filename: filename.to_owned(),
117
            caption: caption.map(str::to_owned),
118
            mimetype: None,
119
            size_bytes: None,
120
            width: None,
121
            height: None,
122
            duration_ms: None,
123
            blurhash: None,
124
            has_thumbnail: false,
125
        };
126
    let millis = |duration: std::time::Duration| u64::try_from(duration.as_millis()).ok();
127
    Some(match message {
128
        MessageType::Image(content) => {
129
            let info = content.info.as_deref();
130
            MediaDescriptor {
131
                mimetype: info.and_then(|info| info.mimetype.clone()),
132
                size_bytes: info.and_then(|info| info.size).map(u64::from),
133
                width: info.and_then(|info| info.width).map(u64::from),
134
                height: info.and_then(|info| info.height).map(u64::from),
135
                blurhash: info.and_then(|info| info.blurhash.clone()),
136
                has_thumbnail: info.is_some_and(|info| info.thumbnail_source.is_some()),
137
                ..descriptor(
138
                    MediaKind::Image,
139
                    &content.source,
140
                    content.filename(),
141
                    content.caption(),
142
                )
143
            }
144
        }
145
        MessageType::Video(content) => {
146
            let info = content.info.as_deref();
147
            MediaDescriptor {
148
                mimetype: info.and_then(|info| info.mimetype.clone()),
149
                size_bytes: info.and_then(|info| info.size).map(u64::from),
150
                width: info.and_then(|info| info.width).map(u64::from),
151
                height: info.and_then(|info| info.height).map(u64::from),
152
                duration_ms: info.and_then(|info| info.duration).and_then(millis),
153
                blurhash: info.and_then(|info| info.blurhash.clone()),
154
                has_thumbnail: info.is_some_and(|info| info.thumbnail_source.is_some()),
155
                ..descriptor(
156
                    MediaKind::Video,
157
                    &content.source,
158
                    content.filename(),
159
                    content.caption(),
160
                )
161
            }
162
        }
163
        MessageType::Audio(content) => {
164
            let info = content.info.as_deref();
165
            MediaDescriptor {
166
                mimetype: info.and_then(|info| info.mimetype.clone()),
167
                size_bytes: info.and_then(|info| info.size).map(u64::from),
168
                duration_ms: info.and_then(|info| info.duration).and_then(millis),
169
                ..descriptor(
170
                    MediaKind::Audio,
171
                    &content.source,
172
                    content.filename(),
173
                    content.caption(),
174
                )
175
            }
176
        }
177
        MessageType::File(content) => {
178
            let info = content.info.as_deref();
179
            MediaDescriptor {
180
                mimetype: info.and_then(|info| info.mimetype.clone()),
181
                size_bytes: info.and_then(|info| info.size).map(u64::from),
182
                has_thumbnail: info.is_some_and(|info| info.thumbnail_source.is_some()),
183
                ..descriptor(
184
                    MediaKind::File,
185
                    &content.source,
186
                    content.filename(),
187
                    content.caption(),
188
                )
189
            }
190
        }
191
        _ => return None,
192
    })
193
}
194
195
pub(super) fn media_descriptor_for_sticker(
196
    identity: &str,
197
    content: &StickerEventContent,
198
) -> MediaDescriptor {
199
    MediaDescriptor {
200
        id: media_id(identity, "sticker"),
201
        kind: MediaKind::Sticker,
202
        mxc: match &content.source {
203
            StickerMediaSource::Plain(uri) => Some(uri.clone()),
204
            _ => None,
205
        },
206
        filename: content.body.clone(),
207
        caption: None,
208
        mimetype: content.info.mimetype.clone(),
209
        size_bytes: content.info.size.map(u64::from),
210
        width: content.info.width.map(u64::from),
211
        height: content.info.height.map(u64::from),
212
        duration_ms: None,
213
        blurhash: content.info.blurhash.clone(),
214
        has_thumbnail: content.info.thumbnail_source.is_some(),
215
    }
216
}
217
218
fn plain_mxc(source: &MediaSource) -> Option<OwnedMxcUri> {
219
    match source {
220
        MediaSource::Plain(uri) => Some(uri.clone()),
221
        MediaSource::Encrypted(_) => None,
222
    }
223
}
224
225
pub(super) fn media_id(identity: &str, suffix: &str) -> MediaId {
226
    MediaId::new(format!("{identity}#{suffix}"))
227
}
228
229
pub(super) fn media_sticker_registration(
230
    identity: &str,
231
    content: &StickerEventContent,
232
) -> Option<MediaRegistration> {
233
    Some(MediaRegistration {
234
        id: media_id(identity, "sticker"),
235
        kind: MediaKind::Sticker,
236
        full: MediaSourceInfo {
237
            source: content.source()?,
238
            mimetype: content.info.mimetype.clone(),
239
            declared_size: content.info.size.map(u64::from),
240
        },
241
        thumbnail: thumbnail_media_source(
242
            content.info.thumbnail_source.clone(),
243
            content.info.thumbnail_info.as_deref(),
244
        ),
245
    })
246
}
247
248
pub(super) fn media_registration(
249
    identity: &str,
250
    message: &MessageType,
251
) -> Option<MediaRegistration> {
252
    match message {
253
        MessageType::Image(content) => media_registration_with_info(
254
            identity,
255
            MediaKind::Image,
256
            content.source(),
257
            content.info.as_deref(),
258
        ),
259
        MessageType::Video(content) => media_registration_with_info(
260
            identity,
261
            MediaKind::Video,
262
            content.source(),
263
            content.info.as_deref(),
264
        ),
265
        MessageType::File(content) => media_registration_with_info(
266
            identity,
267
            MediaKind::File,
268
            content.source(),
269
            content.info.as_deref(),
270
        ),
271
        MessageType::Audio(content) => media_registration_with_parts(
272
            identity,
273
            MediaKind::Audio,
274
            content.source(),
275
            content.info.as_ref().and_then(|info| info.mimetype.clone()),
276
            content
277
                .info
278
                .as_ref()
279
                .and_then(|info| info.size)
280
                .map(u64::from),
281
            None,
282
        ),
283
        _ => None,
284
    }
285
}
286
287
fn media_registration_with_info<T: MediaInfo>(
288
    identity: &str,
289
    kind: MediaKind,
290
    source: Option<MediaSource>,
291
    info: Option<&T>,
292
) -> Option<MediaRegistration> {
293
    media_registration_with_parts(
294
        identity,
295
        kind,
296
        source,
297
        info.and_then(MediaInfo::mimetype),
298
        info.and_then(MediaInfo::declared_size),
299
        info.and_then(MediaInfo::thumbnail),
300
    )
301
}
302
303
fn media_registration_with_parts(
304
    identity: &str,
305
    kind: MediaKind,
306
    source: Option<MediaSource>,
307
    mimetype: Option<String>,
308
    declared_size: Option<u64>,
309
    thumbnail: Option<MediaSourceInfo>,
310
) -> Option<MediaRegistration> {
311
    Some(MediaRegistration {
312
        id: media_id(identity, "media"),
313
        kind,
314
        full: MediaSourceInfo {
315
            source: source?,
316
            mimetype,
317
            declared_size,
318
        },
319
        thumbnail,
320
    })
321
}
322
323
trait MediaInfo {
324
    fn mimetype(&self) -> Option<String>;
325
    fn declared_size(&self) -> Option<u64>;
326
    fn thumbnail(&self) -> Option<MediaSourceInfo>;
327
}
328
329
macro_rules! impl_media_info {
330
    ($type:ty) => {
331
        impl MediaInfo for $type {
332
            fn mimetype(&self) -> Option<String> {
333
                self.mimetype.clone()
334
            }
335
336
            fn declared_size(&self) -> Option<u64> {
337
                self.size.map(u64::from)
338
            }
339
340
            fn thumbnail(&self) -> Option<MediaSourceInfo> {
341
                thumbnail_media_source(
342
                    self.thumbnail_source.clone(),
343
                    self.thumbnail_info.as_deref(),
344
                )
345
            }
346
        }
347
    };
348
}
349
350
impl_media_info!(ImageInfo);
351
impl_media_info!(VideoInfo);
352
impl_media_info!(FileInfo);
353
354
fn thumbnail_media_source(
355
    source: Option<MediaSource>,
356
    info: Option<&ThumbnailInfo>,
357
) -> Option<MediaSourceInfo> {
358
    source.map(|source| MediaSourceInfo {
359
        source,
360
        mimetype: info.and_then(|info| info.mimetype.clone()),
361
        declared_size: info.and_then(|info| info.size).map(u64::from),
362
    })
363
}
364
365
#[cfg(test)]
366
mod tests {
367
    use matrix_sdk::ruma::{
368
        events::room::{ImageInfo, message::ImageMessageEventContent},
369
        mxc_uri,
370
    };
371
372
    use super::*;
373
374
    #[test]
375
    fn image_messages_keep_media_metadata() {
376
        let mut info = ImageInfo::new();
377
        info.mimetype = Some("image/png".to_owned());
378
        info.size = Some(65_536_u32.into());
379
        info.width = Some(640_u32.into());
380
        info.height = Some(480_u32.into());
381
        let content = ImageMessageEventContent::plain(
382
            "screen.png".to_owned(),
383
            mxc_uri!("mxc://example.org/screen").to_owned(),
384
        )
385
        .info(Box::new(info));
386
387
        let media = media_from_message("$event:example.org", &MessageType::Image(content)).unwrap();
388
        assert_eq!(media.id.as_str(), "$event:example.org#media");
389
        assert_eq!(media.kind, MediaKind::Image);
390
        assert_eq!(media.filename, "screen.png");
391
        assert_eq!(media.caption, None);
392
        assert_eq!(media.mimetype.as_deref(), Some("image/png"));
393
        assert_eq!(media.size_bytes, Some(65_536));
394
        assert_eq!((media.width, media.height), (Some(640), Some(480)));
395
    }
396
}