atagen

macltok

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