atagen

macltok

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