atagen

macltok

public
Star0
Watch0
5c749146259c5ffd2d87a86858afdde32013b6a0
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/packs.rsRust765 lines25.0 KB
1
use futures_util::{StreamExt, stream};
2
use matrix_sdk::{
3
    Client,
4
    ruma::{
5
        api::{client::state::get_state_event_for_key, error::ErrorKind as ApiErrorKind},
6
        events::{GlobalAccountDataEventType, StateEventType, room::MediaSource},
7
        serde::{JsonObject, Raw},
8
    },
9
};
10
use ruma::{OwnedEventId, OwnedMxcUri, OwnedRoomId};
11
use serde_json::{Value as JsonValue, json};
12
use tokio::sync::mpsc;
13
14
use super::{
15
    commands::{self, Job},
16
    enqueue, media,
17
    mutations::{Mutation, MutationJob},
18
};
19
use crate::{
20
    CommandOutput, Error, ErrorKind, ImagePackSource, PackCommand, PackImageInfo, PackUsage, Patch,
21
    TimelineId,
22
    normalise::{
23
        ENABLED_ROOMS, ROOM_PACKS, USER_PACK, effective_usage, resolve_pack_id, stored_pack,
24
    },
25
    transport::Responder,
26
};
27
28
/// read-modify-writes of one pack, which run in order per pack
29
pub(super) enum PackEdit {
30
    Create {
31
        display_name: String,
32
    },
33
    Delete,
34
    Meta {
35
        display_name: Patch<String>,
36
        avatar: Patch<OwnedMxcUri>,
37
        usage: Patch<Vec<PackUsage>>,
38
    },
39
    Put {
40
        shortcode: String,
41
        mxc: OwnedMxcUri,
42
        body: Option<String>,
43
        usage: Option<Vec<PackUsage>>,
44
        info: Option<PackImageInfo>,
45
    },
46
    Rename {
47
        from: String,
48
        to: String,
49
    },
50
    Remove {
51
        shortcode: String,
52
    },
53
    /// copies already on our homeserver. `create` is the pack meta for a room pack that doesn't exist yet
54
    Insert {
55
        images: Vec<(String, JsonObject)>,
56
        create: Option<JsonObject>,
57
    },
58
}
59
60
pub(super) async fn edit_pack(
61
    client: &Client,
62
    pack: &ImagePackSource,
63
    edit: PackEdit,
64
) -> Result<CommandOutput, Error> {
65
    match &edit {
66
        PackEdit::Meta {
67
            usage: Patch::Set(usage),
68
            ..
69
        } => validate_usage(Some(usage))?,
70
        PackEdit::Put { usage, .. } => validate_usage(usage.as_ref())?,
71
        _ => {}
72
    }
73
    let current = fetch_pack(client, pack).await?;
74
    let mut content = current.clone().unwrap_or_default();
75
    let mut output = CommandOutput::Done;
76
    match edit {
77
        PackEdit::Create { display_name } => {
78
            if !content.is_empty() {
79
                return Err(Error::new(
80
                    ErrorKind::Invalid,
81
                    "that state key already holds a pack",
82
                ));
83
            }
84
            content = object(json!({ "pack": { "display_name": display_name }, "images": {} }));
85
        }
86
        PackEdit::Delete => content.clear(),
87
        PackEdit::Meta {
88
            display_name,
89
            avatar,
90
            usage,
91
        } => {
92
            let meta = child(&mut content, "pack");
93
            patch(meta, "display_name", display_name, JsonValue::String);
94
            patch(meta, "avatar_url", avatar, |uri| uri.to_string().into());
95
            patch(meta, "usage", usage, usage_json);
96
        }
97
        PackEdit::Put {
98
            shortcode,
99
            mxc,
100
            body,
101
            usage,
102
            info,
103
        } => {
104
            let images = child(&mut content, "images");
105
            let mut image = match images.remove(&shortcode) {
106
                Some(JsonValue::Object(image)) => image,
107
                _ => JsonObject::new(),
108
            };
109
            let same_media = image.get("url").and_then(JsonValue::as_str) == Some(mxc.as_str());
110
            image.insert("url".to_owned(), mxc.to_string().into());
111
            if let Some(body) = body {
112
                image.insert("body".to_owned(), body.into());
113
            }
114
            if let Some(usage) = usage {
115
                image.insert("usage".to_owned(), usage_json(usage));
116
            }
117
            match info {
118
                Some(info) => {
119
                    image.insert("info".to_owned(), info_json(info));
120
                }
121
                None if !same_media => {
122
                    image.remove("info");
123
                }
124
                None => {}
125
            }
126
            images.insert(shortcode, image.into());
127
        }
128
        PackEdit::Rename { from, to } => {
129
            let images = child(&mut content, "images");
130
            if images.contains_key(&to) {
131
                return Err(Error::new(ErrorKind::Invalid, "that shortcode is taken"));
132
            }
133
            let image = images
134
                .remove(&from)
135
                .ok_or_else(|| Error::new(ErrorKind::Invalid, "no image has that shortcode"))?;
136
            images.insert(to, image);
137
        }
138
        PackEdit::Remove { shortcode } => {
139
            child(&mut content, "images").remove(&shortcode);
140
        }
141
        PackEdit::Insert { images, create } => {
142
            if content.is_empty() {
143
                match (create, pack) {
144
                    (Some(meta), _) => {
145
                        content.insert("pack".to_owned(), meta.into());
146
                    }
147
                    (None, ImagePackSource::Room { .. }) => {
148
                        return Err(Error::new(
149
                            ErrorKind::Invalid,
150
                            "there's no pack there to copy into",
151
                        ));
152
                    }
153
                    (None, ImagePackSource::User) => {}
154
                }
155
            }
156
            let target = child(&mut content, "images");
157
            let shortcodes = images
158
                .into_iter()
159
                .map(|(shortcode, image)| {
160
                    let free = free_shortcode(target, &shortcode);
161
                    target.insert(free.clone(), image.into());
162
                    free
163
                })
164
                .collect();
165
            output = CommandOutput::Cloned {
166
                shortcodes,
167
                skipped: Vec::new(),
168
            };
169
        }
170
    }
171
    if current.as_ref() != Some(&content) {
172
        write_pack(client, pack, content).await?;
173
    }
174
    Ok(output)
175
}
176
177
pub(super) async fn set_enabled_everywhere(
178
    client: &Client,
179
    room_id: OwnedRoomId,
180
    state_key: String,
181
    enabled: bool,
182
) -> Result<(), Error> {
183
    let account = client.account();
184
    let current = account
185
        .fetch_account_data(GlobalAccountDataEventType::from(ENABLED_ROOMS))
186
        .await
187
        .map_err(|source| Error::from_sdk("reading enabled packs failed", source))?
188
        .map(|raw| parse(raw.json().get()))
189
        .transpose()?;
190
    let mut content = current.clone().unwrap_or_default();
191
    let rooms = child(&mut content, "rooms");
192
    if enabled {
193
        child(rooms, room_id.as_str()).insert(state_key, JsonObject::new().into());
194
    } else {
195
        let state_keys = child(rooms, room_id.as_str());
196
        state_keys.remove(&state_key);
197
        if state_keys.is_empty() {
198
            rooms.remove(room_id.as_str());
199
        }
200
    }
201
    if current.as_ref() == Some(&content) {
202
        return Ok(());
203
    }
204
    account
205
        .set_account_data_raw(ENABLED_ROOMS.into(), raw(&content))
206
        .await
207
        .map(drop)
208
        .map_err(|source| Error::from_sdk("writing enabled packs failed", source))
209
}
210
211
/// the commands that download, upload or send, so they stay off the per-pack queue and only
212
/// take their turn for the final write
213
pub(super) async fn run_detached(
214
    client: Client,
215
    mutations: mpsc::Sender<MutationJob>,
216
    max_bytes: usize,
217
    command: PackCommand,
218
) -> Result<CommandOutput, Error> {
219
    if let PackCommand::CloneImage { usage, .. }
220
    | PackCommand::CloneSticker { usage, .. }
221
    | PackCommand::ClonePack { usage, .. } = &command
222
    {
223
        validate_usage(usage.as_ref())?;
224
    }
225
    match command {
226
        PackCommand::SendSticker {
227
            timeline_id,
228
            pack_id,
229
            shortcode,
230
        } => send_sticker(&client, &timeline_id, &pack_id, &shortcode)
231
            .await
232
            .map(|()| CommandOutput::Done),
233
        PackCommand::Rehost(mxc) => rehost(&client, mxc, None, max_bytes)
234
            .await
235
            .map(CommandOutput::Mxc),
236
        PackCommand::CloneImage {
237
            from_pack,
238
            shortcode,
239
            to,
240
            usage,
241
        } => {
242
            let mut image = pack_image(&client, &from_pack, &shortcode).await?;
243
            apply_usage(&mut image, usage);
244
            let image = rehost_image(&client, image, max_bytes).await?;
245
            insert(&mutations, to, vec![(shortcode, image)], None, Vec::new()).await
246
        }
247
        PackCommand::CloneSticker {
248
            room_id,
249
            event_id,
250
            to,
251
            shortcode,
252
            usage,
253
        } => {
254
            let mut image = sticker_image(&client, room_id, event_id).await?;
255
            let body = sticker_body(&image, &shortcode);
256
            image.insert("body".to_owned(), body.into());
257
            apply_usage(&mut image, usage);
258
            let image = rehost_image(&client, image, max_bytes).await?;
259
            insert(&mutations, to, vec![(shortcode, image)], None, Vec::new()).await
260
        }
261
        PackCommand::ClonePack {
262
            from_pack,
263
            to,
264
            display_name,
265
            usage,
266
        } => {
267
            clone_pack(
268
                &client,
269
                &mutations,
270
                max_bytes,
271
                &from_pack,
272
                to,
273
                display_name,
274
                usage,
275
            )
276
            .await
277
        }
278
        _ => Err(Error::new(ErrorKind::Other, "pack command was misrouted")),
279
    }
280
}
281
282
async fn clone_pack(
283
    client: &Client,
284
    mutations: &mpsc::Sender<MutationJob>,
285
    max_bytes: usize,
286
    from_pack: &str,
287
    to: ImagePackSource,
288
    display_name: Option<String>,
289
    usage: Option<Vec<PackUsage>>,
290
) -> Result<CommandOutput, Error> {
291
    let source = resolve_pack_id(client, from_pack)
292
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "no pack has that id"))?;
293
    let content = stored_pack(client, &source)
294
        .await?
295
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "that pack is empty"))?;
296
    let mut meta = match content.get("pack") {
297
        Some(JsonValue::Object(meta)) => meta.clone(),
298
        _ => JsonObject::new(),
299
    };
300
    let images: Vec<(String, JsonObject)> = match content.get("images") {
301
        Some(JsonValue::Object(images)) => images
302
            .iter()
303
            .filter_map(|(shortcode, image)| {
304
                let mut image = with_pack_usage(image.as_object()?.clone(), &meta);
305
                apply_usage(&mut image, usage.clone());
306
                Some((shortcode.clone(), image))
307
            })
308
            .collect(),
309
        _ => Vec::new(),
310
    };
311
    let results: Vec<_> = stream::iter(images)
312
        .map(|(shortcode, image)| async move {
313
            match rehost_image(client, image, max_bytes).await {
314
                Ok(image) => Ok((shortcode, image)),
315
                Err(error) => {
316
                    tracing::debug!(%error, shortcode, "skipping an image that wouldn't rehost");
317
                    Err(shortcode)
318
                }
319
            }
320
        })
321
        .buffered(4)
322
        .collect()
323
        .await;
324
    let (copies, skipped): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok);
325
    let copies = copies.into_iter().filter_map(Result::ok).collect();
326
    let skipped = skipped.into_iter().filter_map(Result::err).collect();
327
    let create = match to {
328
        ImagePackSource::User => None,
329
        ImagePackSource::Room { .. } => {
330
            if let Some(display_name) = display_name {
331
                meta.insert("display_name".to_owned(), display_name.into());
332
            }
333
            meta.remove("usage");
334
            if let Some(avatar) = meta.remove("avatar_url").and_then(|uri| mxc(&uri))
335
                && let Ok(avatar) = rehost(client, avatar, None, max_bytes).await
336
            {
337
                meta.insert("avatar_url".to_owned(), avatar.to_string().into());
338
            }
339
            Some(meta)
340
        }
341
    };
342
    insert(mutations, to, copies, create, skipped).await
343
}
344
345
async fn insert(
346
    mutations: &mpsc::Sender<MutationJob>,
347
    pack: ImagePackSource,
348
    images: Vec<(String, JsonObject)>,
349
    create: Option<JsonObject>,
350
    skipped: Vec<String>,
351
) -> Result<CommandOutput, Error> {
352
    let (reply, answer) = Responder::channel();
353
    let command = Mutation::Pack {
354
        pack,
355
        edit: PackEdit::Insert { images, create },
356
    };
357
    enqueue(mutations, Job { command, reply });
358
    match answer.await? {
359
        CommandOutput::Cloned { shortcodes, .. } => Ok(CommandOutput::Cloned {
360
            shortcodes,
361
            skipped,
362
        }),
363
        output => Ok(output),
364
    }
365
}
366
367
async fn send_sticker(
368
    client: &Client,
369
    timeline_id: &TimelineId,
370
    pack_id: &str,
371
    shortcode: &str,
372
) -> Result<(), Error> {
373
    if matches!(timeline_id, TimelineId::Pinned(_)) {
374
        return Err(Error::new(
375
            ErrorKind::Invalid,
376
            "the pinned timeline can't send or paginate",
377
        ));
378
    }
379
    let room = client
380
        .get_room(timeline_id.room_id())
381
        .ok_or_else(Error::room_unknown)?;
382
    let image = pack_image(client, pack_id, shortcode).await?;
383
    let url = image
384
        .get("url")
385
        .and_then(mxc)
386
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "that image has no usable url"))?;
387
    let mut content = json!({
388
        "body": sticker_body(&image, shortcode),
389
        "url": url,
390
        "info": image.get("info").cloned().unwrap_or_else(|| json!({})),
391
    });
392
    // clients without threads see a reply to the newest event in the thread, the way the
393
    // sdk's threaded timeline falls back, and the root when that isn't known
394
    if let Some(root) = timeline_id.thread_root() {
395
        let latest = room
396
            .load_or_fetch_event(root, None)
397
            .await
398
            .ok()
399
            .and_then(|event| event.raw().get_field::<JsonValue>("unsigned").ok().flatten())
400
            .and_then(|unsigned| {
401
                unsigned
402
                    .pointer("/m.relations/m.thread/latest_event/event_id")
403
                    .and_then(JsonValue::as_str)
404
                    .map(str::to_owned)
405
            })
406
            .unwrap_or_else(|| root.to_string());
407
        content["m.relates_to"] = json!({
408
            "rel_type": "m.thread",
409
            "event_id": root,
410
            "is_falling_back": true,
411
            "m.in_reply_to": { "event_id": latest },
412
        });
413
    }
414
    room.send_queue()
415
        .send_raw(raw(&object(content)), "m.sticker".to_owned())
416
        .await
417
        .map(drop)
418
        .map_err(|source| {
419
            Error::with_source(ErrorKind::Store, "queueing the sticker failed", source)
420
        })
421
}
422
423
/// the image with its pack's usage folded in, so it means the same once it leaves the pack
424
async fn pack_image(client: &Client, pack_id: &str, shortcode: &str) -> Result<JsonObject, Error> {
425
    let source = resolve_pack_id(client, pack_id)
426
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "no pack has that id"))?;
427
    let content = stored_pack(client, &source).await?.unwrap_or_default();
428
    let image = content
429
        .get("images")
430
        .and_then(|images| images.get(shortcode))
431
        .and_then(JsonValue::as_object)
432
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "no image has that shortcode"))?
433
        .clone();
434
    let meta = content
435
        .get("pack")
436
        .and_then(JsonValue::as_object)
437
        .cloned()
438
        .unwrap_or_default();
439
    Ok(with_pack_usage(image, &meta))
440
}
441
442
async fn sticker_image(
443
    client: &Client,
444
    room_id: OwnedRoomId,
445
    event_id: OwnedEventId,
446
) -> Result<JsonObject, Error> {
447
    let room = client.get_room(&room_id).ok_or_else(Error::room_unknown)?;
448
    let event = room
449
        .load_or_fetch_event(&event_id, None)
450
        .await
451
        .map_err(|source| Error::from_sdk("loading the sticker failed", source))?;
452
    let raw = event.raw();
453
    if raw.get_field::<String>("type").ok().flatten().as_deref() != Some("m.sticker") {
454
        return Err(Error::new(ErrorKind::Invalid, "that event isn't a sticker"));
455
    }
456
    let content = raw
457
        .get_field::<JsonObject>("content")
458
        .ok()
459
        .flatten()
460
        .unwrap_or_default();
461
    let mut image = JsonObject::new();
462
    for key in ["url", "body", "info"] {
463
        if let Some(value) = content.get(key) {
464
            image.insert(key.to_owned(), value.clone());
465
        }
466
    }
467
    image.insert("usage".to_owned(), usage_json(vec![PackUsage::Sticker]));
468
    Ok(image)
469
}
470
471
async fn rehost_image(
472
    client: &Client,
473
    mut image: JsonObject,
474
    max_bytes: usize,
475
) -> Result<JsonObject, Error> {
476
    let url = image
477
        .get("url")
478
        .and_then(mxc)
479
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "that image has no usable url"))?;
480
    let mimetype = image
481
        .get("info")
482
        .and_then(|info| info.get("mimetype"))
483
        .and_then(JsonValue::as_str)
484
        .map(str::to_owned);
485
    let url = rehost(client, url, mimetype, max_bytes).await?;
486
    image.insert("url".to_owned(), url.to_string().into());
487
    Ok(image)
488
}
489
490
pub(super) async fn rehost(
491
    client: &Client,
492
    mxc: OwnedMxcUri,
493
    mimetype: Option<String>,
494
    max_bytes: usize,
495
) -> Result<OwnedMxcUri, Error> {
496
    let homeserver = client.user_id().map(ruma::UserId::server_name);
497
    if mxc
498
        .server_name()
499
        .ok()
500
        .is_some_and(|server| Some(server) == homeserver)
501
    {
502
        return Ok(mxc);
503
    }
504
    let media = media::download_file(client, MediaSource::Plain(mxc), mimetype, max_bytes).await?;
505
    commands::upload_media(client, &media.mimetype, media.bytes.to_vec(), max_bytes).await
506
}
507
508
/// the server's copy, since the store lags a sync behind and an edit made just before would be lost
509
async fn fetch_pack(client: &Client, pack: &ImagePackSource) -> Result<Option<JsonObject>, Error> {
510
    match pack {
511
        ImagePackSource::User => client
512
            .account()
513
            .fetch_account_data(GlobalAccountDataEventType::from(USER_PACK))
514
            .await
515
            .map_err(|source| Error::from_sdk("reading the pack failed", source))?
516
            .map(|raw| parse(raw.json().get()))
517
            .transpose(),
518
        ImagePackSource::Room { room_id, state_key } => {
519
            let request = get_state_event_for_key::v3::Request::new(
520
                room_id.clone(),
521
                StateEventType::from(ROOM_PACKS),
522
                state_key.clone(),
523
            );
524
            match client.send(request).await {
525
                Ok(response) => parse(response.event_or_content.get()).map(Some),
526
                Err(error) if error.client_api_error_kind() == Some(&ApiErrorKind::NotFound) => {
527
                    Ok(None)
528
                }
529
                Err(source) => Err(Error::from_http("reading the pack failed", source)),
530
            }
531
        }
532
    }
533
}
534
535
async fn write_pack(
536
    client: &Client,
537
    pack: &ImagePackSource,
538
    content: JsonObject,
539
) -> Result<(), Error> {
540
    let written = match pack {
541
        ImagePackSource::User => client
542
            .account()
543
            .set_account_data_raw(USER_PACK.into(), raw(&content))
544
            .await
545
            .map(drop),
546
        ImagePackSource::Room { room_id, state_key } => client
547
            .get_room(room_id)
548
            .ok_or_else(Error::room_unknown)?
549
            .send_state_event_raw(ROOM_PACKS, state_key, JsonValue::Object(content))
550
            .await
551
            .map(drop),
552
    };
553
    written.map_err(|source| Error::from_sdk("writing the pack failed", source))
554
}
555
556
/// values this crate doesn't know ride along untouched, they may mean something to someone
557
fn with_pack_usage(mut image: JsonObject, meta: &JsonObject) -> JsonObject {
558
    if known_usage(image.get("usage")).is_some() {
559
        return image;
560
    }
561
    let mut usage = match meta.get("usage") {
562
        Some(JsonValue::Array(values)) if known_usage(meta.get("usage")).is_some() => {
563
            values.clone()
564
        }
565
        _ => usage_values(effective_usage(None, None)),
566
    };
567
    if let Some(JsonValue::Array(own)) = image.get("usage") {
568
        for value in own {
569
            if !usage.contains(value) {
570
                usage.push(value.clone());
571
            }
572
        }
573
    }
574
    image.insert("usage".to_owned(), usage.into());
575
    image
576
}
577
578
fn known_usage(value: Option<&JsonValue>) -> Option<Vec<PackUsage>> {
579
    let values = value?.as_array()?;
580
    let usage = [
581
        ("emoticon", PackUsage::Emoticon),
582
        ("sticker", PackUsage::Sticker),
583
    ]
584
    .into_iter()
585
    .filter_map(|(name, usage)| {
586
        values
587
            .iter()
588
            .any(|value| value.as_str() == Some(name))
589
            .then_some(usage)
590
    })
591
    .collect::<Vec<_>>();
592
    (!usage.is_empty()).then_some(usage)
593
}
594
595
/// `Some` of nothing would read back as inheriting, which `None` and `Patch::Clear` already say
596
fn validate_usage(usage: Option<&Vec<PackUsage>>) -> Result<(), Error> {
597
    if usage.is_some_and(Vec::is_empty) {
598
        return Err(Error::new(
599
            ErrorKind::Invalid,
600
            "usage needs at least one value, leave it unset to inherit",
601
        ));
602
    }
603
    Ok(())
604
}
605
606
fn apply_usage(image: &mut JsonObject, usage: Option<Vec<PackUsage>>) {
607
    if let Some(usage) = usage {
608
        image.insert("usage".to_owned(), usage_json(usage));
609
    }
610
}
611
612
fn sticker_body(image: &JsonObject, shortcode: &str) -> String {
613
    image
614
        .get("body")
615
        .and_then(JsonValue::as_str)
616
        .filter(|body| !body.trim().is_empty())
617
        .unwrap_or(shortcode)
618
        .to_owned()
619
}
620
621
fn free_shortcode(images: &JsonObject, wanted: &str) -> String {
622
    if !images.contains_key(wanted) {
623
        return wanted.to_owned();
624
    }
625
    (2..)
626
        .map(|n| format!("{wanted}-{n}"))
627
        .find(|candidate| !images.contains_key(candidate))
628
        .expect("an unbounded range finds a free name")
629
}
630
631
/// the object at `key`, made or replaced if it was missing or malformed
632
fn child<'a>(parent: &'a mut JsonObject, key: &str) -> &'a mut JsonObject {
633
    let slot = parent
634
        .entry(key.to_owned())
635
        .or_insert_with(|| JsonObject::new().into());
636
    if !slot.is_object() {
637
        *slot = JsonObject::new().into();
638
    }
639
    slot.as_object_mut().expect("just made it an object")
640
}
641
642
fn patch<T>(
643
    meta: &mut JsonObject,
644
    key: &str,
645
    change: Patch<T>,
646
    to_json: impl FnOnce(T) -> JsonValue,
647
) {
648
    match change {
649
        Patch::Keep => {}
650
        Patch::Clear => {
651
            meta.remove(key);
652
        }
653
        Patch::Set(value) => {
654
            meta.insert(key.to_owned(), to_json(value));
655
        }
656
    }
657
}
658
659
fn usage_json(usage: Vec<PackUsage>) -> JsonValue {
660
    usage_values(usage).into()
661
}
662
663
fn usage_values(usage: Vec<PackUsage>) -> Vec<JsonValue> {
664
    usage
665
        .into_iter()
666
        .map(|usage| match usage {
667
            PackUsage::Emoticon => "emoticon".into(),
668
            PackUsage::Sticker => "sticker".into(),
669
        })
670
        .collect()
671
}
672
673
fn info_json(info: PackImageInfo) -> JsonValue {
674
    let mut out = JsonObject::new();
675
    if let Some(mimetype) = info.mimetype {
676
        out.insert("mimetype".to_owned(), mimetype.into());
677
    }
678
    for (key, value) in [
679
        ("size", info.size_bytes),
680
        ("w", info.width),
681
        ("h", info.height),
682
    ] {
683
        if let Some(value) = value {
684
            out.insert(key.to_owned(), value.into());
685
        }
686
    }
687
    out.into()
688
}
689
690
fn mxc(value: &JsonValue) -> Option<OwnedMxcUri> {
691
    let uri = OwnedMxcUri::from(value.as_str()?);
692
    uri.is_valid().then_some(uri)
693
}
694
695
fn object(value: JsonValue) -> JsonObject {
696
    match value {
697
        JsonValue::Object(object) => object,
698
        _ => JsonObject::new(),
699
    }
700
}
701
702
fn parse(json: &str) -> Result<JsonObject, Error> {
703
    serde_json::from_str(json)
704
        .map_err(|source| Error::with_source(ErrorKind::Server, "the pack is malformed", source))
705
}
706
707
fn raw<T>(content: &JsonObject) -> Raw<T> {
708
    Raw::new(content)
709
        .expect("a json object always serialises")
710
        .cast_unchecked()
711
}
712
713
#[cfg(test)]
714
mod tests {
715
    use super::*;
716
717
    #[test]
718
    fn pack_usage_inherits_known_values_then_defaults_and_keeps_unknown_ones() {
719
        let pack = object(json!({ "usage": ["sticker", "unknown"] }));
720
        let inherited = with_pack_usage(object(json!({ "usage": [] })), &pack);
721
        assert_eq!(inherited.get("usage"), Some(&json!(["sticker", "unknown"])));
722
723
        let pack = object(json!({ "usage": ["emoticon"] }));
724
        let inherited = with_pack_usage(object(json!({ "usage": ["unknown"] })), &pack);
725
        assert_eq!(
726
            inherited.get("usage"),
727
            Some(&json!(["emoticon", "unknown"]))
728
        );
729
730
        let pack = object(json!({ "usage": ["sticker"] }));
731
        let inherited = with_pack_usage(object(json!({ "usage": ["emoticon", "unknown"] })), &pack);
732
        assert_eq!(
733
            inherited.get("usage"),
734
            Some(&json!(["emoticon", "unknown"]))
735
        );
736
737
        let pack = object(json!({ "usage": [] }));
738
        let inherited = with_pack_usage(object(json!({})), &pack);
739
        assert_eq!(
740
            inherited.get("usage"),
741
            Some(&json!(["emoticon", "sticker"]))
742
        );
743
744
        let pack = object(json!({ "usage": ["unknown"] }));
745
        let inherited = with_pack_usage(object(json!({})), &pack);
746
        assert_eq!(
747
            inherited.get("usage"),
748
            Some(&json!(["emoticon", "sticker"]))
749
        );
750
    }
751
752
    #[test]
753
    fn sticker_body_falls_back_when_missing_or_blank() {
754
        assert_eq!(sticker_body(&object(json!({})), "wave"), "wave");
755
        assert_eq!(sticker_body(&object(json!({ "body": "" })), "wave"), "wave");
756
        assert_eq!(
757
            sticker_body(&object(json!({ "body": " \t\n " })), "wave"),
758
            "wave"
759
        );
760
        assert_eq!(
761
            sticker_body(&object(json!({ "body": "  hello  " })), "wave"),
762
            "  hello  "
763
        );
764
    }
765
}