atagen

macltok

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