atagen

macltok

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