atagen

macltok

public
Star0
Watch0
fa51490b6ae98fdf5eeddb0799f4e3a160bf53cb
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/normalise/media.rsRust375 lines12.1 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
#[derive(Clone, Debug)]
28
pub(crate) struct MediaScope {
29
    nonce: u64,
30
}
31
32
impl MediaScope {
33
    pub(crate) fn new() -> Self {
34
        Self {
35
            nonce: NEXT_MEDIA_SCOPE.fetch_add(1, Ordering::Relaxed),
36
        }
37
    }
38
39
    pub(crate) fn owns(&self, id: &MediaId) -> bool {
40
        id.as_str()
41
            .split_once('#')
42
            .is_some_and(|(nonce, _)| nonce.parse() == Ok(self.nonce))
43
    }
44
}
45
46
#[derive(Clone)]
47
pub(crate) struct MediaRegistration {
48
    pub(crate) id: MediaId,
49
    pub(crate) kind: MediaKind,
50
    pub(crate) full: MediaSourceInfo,
51
    pub(crate) thumbnail: Option<MediaSourceInfo>,
52
}
53
54
#[derive(Clone)]
55
pub(crate) struct MediaSourceInfo {
56
    pub(crate) source: MediaSource,
57
    pub(crate) mimetype: Option<String>,
58
    pub(crate) declared_size: Option<u64>,
59
}
60
61
pub(super) fn media_registrations_for_item(
62
    item: &SdkTimelineItem,
63
    scope: &MediaScope,
64
) -> Vec<MediaRegistration> {
65
    let Some(event) = item.as_event() else {
66
        return Vec::new();
67
    };
68
    let identity = media_identity(item, scope);
69
    let TimelineItemContent::MsgLike(content) = event.content() else {
70
        return Vec::new();
71
    };
72
    match &content.kind {
73
        MsgLikeKind::Message(message) => media_registration(&identity, message.msgtype())
74
            .into_iter()
75
            .chain(
76
                sender_previews(Some(&identity), message.msgtype())
77
                    .into_iter()
78
                    .filter_map(|(_, registration)| registration),
79
            )
80
            .collect(),
81
        MsgLikeKind::Sticker(sticker) => media_sticker_registration(&identity, sticker.content())
82
            .into_iter()
83
            .collect(),
84
        _ => Vec::new(),
85
    }
86
}
87
88
pub(super) fn media_identity(item: &SdkTimelineItem, scope: &MediaScope) -> String {
89
    format!("{}#{}", scope.nonce, item.unique_id().0)
90
}
91
92
pub(super) fn media_from_message(identity: &str, message: &MessageType) -> Option<MediaDescriptor> {
93
    let descriptor =
94
        |kind, source: &MediaSource, filename: &str, caption: Option<&str>| MediaDescriptor {
95
            id: media_id(identity, "media"),
96
            kind,
97
            mxc: plain_mxc(source),
98
            filename: filename.to_owned(),
99
            caption: caption.map(str::to_owned),
100
            mimetype: None,
101
            size_bytes: None,
102
            width: None,
103
            height: None,
104
            duration_ms: None,
105
            blurhash: None,
106
            has_thumbnail: false,
107
        };
108
    let millis = |duration: std::time::Duration| u64::try_from(duration.as_millis()).ok();
109
    Some(match message {
110
        MessageType::Image(content) => {
111
            let info = content.info.as_deref();
112
            MediaDescriptor {
113
                mimetype: info.and_then(|info| info.mimetype.clone()),
114
                size_bytes: info.and_then(|info| info.size).map(u64::from),
115
                width: info.and_then(|info| info.width).map(u64::from),
116
                height: info.and_then(|info| info.height).map(u64::from),
117
                blurhash: info.and_then(|info| info.blurhash.clone()),
118
                has_thumbnail: info.is_some_and(|info| info.thumbnail_source.is_some()),
119
                ..descriptor(
120
                    MediaKind::Image,
121
                    &content.source,
122
                    content.filename(),
123
                    content.caption(),
124
                )
125
            }
126
        }
127
        MessageType::Video(content) => {
128
            let info = content.info.as_deref();
129
            MediaDescriptor {
130
                mimetype: info.and_then(|info| info.mimetype.clone()),
131
                size_bytes: info.and_then(|info| info.size).map(u64::from),
132
                width: info.and_then(|info| info.width).map(u64::from),
133
                height: info.and_then(|info| info.height).map(u64::from),
134
                duration_ms: info.and_then(|info| info.duration).and_then(millis),
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::Video,
139
                    &content.source,
140
                    content.filename(),
141
                    content.caption(),
142
                )
143
            }
144
        }
145
        MessageType::Audio(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
                duration_ms: info.and_then(|info| info.duration).and_then(millis),
151
                ..descriptor(
152
                    MediaKind::Audio,
153
                    &content.source,
154
                    content.filename(),
155
                    content.caption(),
156
                )
157
            }
158
        }
159
        MessageType::File(content) => {
160
            let info = content.info.as_deref();
161
            MediaDescriptor {
162
                mimetype: info.and_then(|info| info.mimetype.clone()),
163
                size_bytes: info.and_then(|info| info.size).map(u64::from),
164
                has_thumbnail: info.is_some_and(|info| info.thumbnail_source.is_some()),
165
                ..descriptor(
166
                    MediaKind::File,
167
                    &content.source,
168
                    content.filename(),
169
                    content.caption(),
170
                )
171
            }
172
        }
173
        _ => return None,
174
    })
175
}
176
177
pub(super) fn media_descriptor_for_sticker(
178
    identity: &str,
179
    content: &StickerEventContent,
180
) -> MediaDescriptor {
181
    MediaDescriptor {
182
        id: media_id(identity, "sticker"),
183
        kind: MediaKind::Sticker,
184
        mxc: match &content.source {
185
            StickerMediaSource::Plain(uri) => Some(uri.clone()),
186
            _ => None,
187
        },
188
        filename: content.body.clone(),
189
        caption: None,
190
        mimetype: content.info.mimetype.clone(),
191
        size_bytes: content.info.size.map(u64::from),
192
        width: content.info.width.map(u64::from),
193
        height: content.info.height.map(u64::from),
194
        duration_ms: None,
195
        blurhash: content.info.blurhash.clone(),
196
        has_thumbnail: content.info.thumbnail_source.is_some(),
197
    }
198
}
199
200
fn plain_mxc(source: &MediaSource) -> Option<OwnedMxcUri> {
201
    match source {
202
        MediaSource::Plain(uri) => Some(uri.clone()),
203
        MediaSource::Encrypted(_) => None,
204
    }
205
}
206
207
pub(super) fn media_id(identity: &str, suffix: &str) -> MediaId {
208
    MediaId::new(format!("{identity}#{suffix}"))
209
}
210
211
fn media_sticker_registration(
212
    identity: &str,
213
    content: &StickerEventContent,
214
) -> Option<MediaRegistration> {
215
    Some(MediaRegistration {
216
        id: media_id(identity, "sticker"),
217
        kind: MediaKind::Sticker,
218
        full: MediaSourceInfo {
219
            source: content.source()?,
220
            mimetype: content.info.mimetype.clone(),
221
            declared_size: content.info.size.map(u64::from),
222
        },
223
        thumbnail: thumbnail_media_source(
224
            content.info.thumbnail_source.clone(),
225
            content.info.thumbnail_info.as_deref(),
226
        ),
227
    })
228
}
229
230
fn media_registration(identity: &str, message: &MessageType) -> Option<MediaRegistration> {
231
    match message {
232
        MessageType::Image(content) => media_registration_with_info(
233
            identity,
234
            MediaKind::Image,
235
            content.source(),
236
            content.info.as_deref(),
237
        ),
238
        MessageType::Video(content) => media_registration_with_info(
239
            identity,
240
            MediaKind::Video,
241
            content.source(),
242
            content.info.as_deref(),
243
        ),
244
        MessageType::File(content) => media_registration_with_info(
245
            identity,
246
            MediaKind::File,
247
            content.source(),
248
            content.info.as_deref(),
249
        ),
250
        MessageType::Audio(content) => media_registration_with_parts(
251
            identity,
252
            MediaKind::Audio,
253
            content.source(),
254
            content.info.as_ref().and_then(|info| info.mimetype.clone()),
255
            content
256
                .info
257
                .as_ref()
258
                .and_then(|info| info.size)
259
                .map(u64::from),
260
            None,
261
        ),
262
        _ => None,
263
    }
264
}
265
266
fn media_registration_with_info<T: MediaInfo>(
267
    identity: &str,
268
    kind: MediaKind,
269
    source: Option<MediaSource>,
270
    info: Option<&T>,
271
) -> Option<MediaRegistration> {
272
    media_registration_with_parts(
273
        identity,
274
        kind,
275
        source,
276
        info.and_then(MediaInfo::mimetype),
277
        info.and_then(MediaInfo::declared_size),
278
        info.and_then(MediaInfo::thumbnail),
279
    )
280
}
281
282
fn media_registration_with_parts(
283
    identity: &str,
284
    kind: MediaKind,
285
    source: Option<MediaSource>,
286
    mimetype: Option<String>,
287
    declared_size: Option<u64>,
288
    thumbnail: Option<MediaSourceInfo>,
289
) -> Option<MediaRegistration> {
290
    Some(MediaRegistration {
291
        id: media_id(identity, "media"),
292
        kind,
293
        full: MediaSourceInfo {
294
            source: source?,
295
            mimetype,
296
            declared_size,
297
        },
298
        thumbnail,
299
    })
300
}
301
302
trait MediaInfo {
303
    fn mimetype(&self) -> Option<String>;
304
    fn declared_size(&self) -> Option<u64>;
305
    fn thumbnail(&self) -> Option<MediaSourceInfo>;
306
}
307
308
macro_rules! impl_media_info {
309
    ($type:ty) => {
310
        impl MediaInfo for $type {
311
            fn mimetype(&self) -> Option<String> {
312
                self.mimetype.clone()
313
            }
314
315
            fn declared_size(&self) -> Option<u64> {
316
                self.size.map(u64::from)
317
            }
318
319
            fn thumbnail(&self) -> Option<MediaSourceInfo> {
320
                thumbnail_media_source(
321
                    self.thumbnail_source.clone(),
322
                    self.thumbnail_info.as_deref(),
323
                )
324
            }
325
        }
326
    };
327
}
328
329
impl_media_info!(ImageInfo);
330
impl_media_info!(VideoInfo);
331
impl_media_info!(FileInfo);
332
333
fn thumbnail_media_source(
334
    source: Option<MediaSource>,
335
    info: Option<&ThumbnailInfo>,
336
) -> Option<MediaSourceInfo> {
337
    source.map(|source| MediaSourceInfo {
338
        source,
339
        mimetype: info.and_then(|info| info.mimetype.clone()),
340
        declared_size: info.and_then(|info| info.size).map(u64::from),
341
    })
342
}
343
344
#[cfg(test)]
345
mod tests {
346
    use matrix_sdk::ruma::{
347
        events::room::{ImageInfo, message::ImageMessageEventContent},
348
        mxc_uri,
349
    };
350
351
    use super::*;
352
353
    #[test]
354
    fn image_messages_keep_media_metadata() {
355
        let mut info = ImageInfo::new();
356
        info.mimetype = Some("image/png".to_owned());
357
        info.size = Some(65_536_u32.into());
358
        info.width = Some(640_u32.into());
359
        info.height = Some(480_u32.into());
360
        let content = ImageMessageEventContent::plain(
361
            "screen.png".to_owned(),
362
            mxc_uri!("mxc://example.org/screen").to_owned(),
363
        )
364
        .info(Box::new(info));
365
366
        let media = media_from_message("$event:example.org", &MessageType::Image(content)).unwrap();
367
        assert_eq!(media.id.as_str(), "$event:example.org#media");
368
        assert_eq!(media.kind, MediaKind::Image);
369
        assert_eq!(media.filename, "screen.png");
370
        assert_eq!(media.caption, None);
371
        assert_eq!(media.mimetype.as_deref(), Some("image/png"));
372
        assert_eq!(media.size_bytes, Some(65_536));
373
        assert_eq!((media.width, media.height), (Some(640), Some(480)));
374
    }
375
}