atagen

macltok

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