atagen

macltok

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