atagen

macltok

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