atagen

macltok

public
Star0
Watch0
722b4c3d0a09ca2aa7b242170395338933a65d52
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/packs.rsRust752 lines24.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
    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
    // the same fallback the send queue gives a threaded attachment with no reply
393
    if let Some(root) = timeline_id.thread_root() {
394
        content["m.relates_to"] = json!({
395
            "rel_type": "m.thread",
396
            "event_id": root,
397
            "is_falling_back": true,
398
            "m.in_reply_to": { "event_id": root },
399
        });
400
    }
401
    room.send_queue()
402
        .send_raw(raw(&object(content)), "m.sticker".to_owned())
403
        .await
404
        .map(drop)
405
        .map_err(|source| {
406
            Error::with_source(ErrorKind::Store, "queueing the sticker failed", source)
407
        })
408
}
409
410
/// the image with its pack's usage folded in, so it means the same once it leaves the pack
411
async fn pack_image(client: &Client, pack_id: &str, shortcode: &str) -> Result<JsonObject, Error> {
412
    let source = resolve_pack_id(client, pack_id)
413
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "no pack has that id"))?;
414
    let content = stored_pack(client, &source).await?.unwrap_or_default();
415
    let image = content
416
        .get("images")
417
        .and_then(|images| images.get(shortcode))
418
        .and_then(JsonValue::as_object)
419
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "no image has that shortcode"))?
420
        .clone();
421
    let meta = content
422
        .get("pack")
423
        .and_then(JsonValue::as_object)
424
        .cloned()
425
        .unwrap_or_default();
426
    Ok(with_pack_usage(image, &meta))
427
}
428
429
async fn sticker_image(
430
    client: &Client,
431
    room_id: OwnedRoomId,
432
    event_id: OwnedEventId,
433
) -> Result<JsonObject, Error> {
434
    let room = client.get_room(&room_id).ok_or_else(Error::room_unknown)?;
435
    let event = room
436
        .load_or_fetch_event(&event_id, None)
437
        .await
438
        .map_err(|source| Error::from_sdk("loading the sticker failed", source))?;
439
    let raw = event.raw();
440
    if raw.get_field::<String>("type").ok().flatten().as_deref() != Some("m.sticker") {
441
        return Err(Error::new(ErrorKind::Invalid, "that event isn't a sticker"));
442
    }
443
    let content = raw
444
        .get_field::<JsonObject>("content")
445
        .ok()
446
        .flatten()
447
        .unwrap_or_default();
448
    let mut image = JsonObject::new();
449
    for key in ["url", "body", "info"] {
450
        if let Some(value) = content.get(key) {
451
            image.insert(key.to_owned(), value.clone());
452
        }
453
    }
454
    image.insert("usage".to_owned(), usage_json(vec![PackUsage::Sticker]));
455
    Ok(image)
456
}
457
458
async fn rehost_image(
459
    client: &Client,
460
    mut image: JsonObject,
461
    max_bytes: usize,
462
) -> Result<JsonObject, Error> {
463
    let url = image
464
        .get("url")
465
        .and_then(mxc)
466
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "that image has no usable url"))?;
467
    let mimetype = image
468
        .get("info")
469
        .and_then(|info| info.get("mimetype"))
470
        .and_then(JsonValue::as_str)
471
        .map(str::to_owned);
472
    let url = rehost(client, url, mimetype, max_bytes).await?;
473
    image.insert("url".to_owned(), url.to_string().into());
474
    Ok(image)
475
}
476
477
pub(super) async fn rehost(
478
    client: &Client,
479
    mxc: OwnedMxcUri,
480
    mimetype: Option<String>,
481
    max_bytes: usize,
482
) -> Result<OwnedMxcUri, Error> {
483
    let homeserver = client.user_id().map(ruma::UserId::server_name);
484
    if mxc
485
        .server_name()
486
        .ok()
487
        .is_some_and(|server| Some(server) == homeserver)
488
    {
489
        return Ok(mxc);
490
    }
491
    let media = media::download_file(client, MediaSource::Plain(mxc), mimetype, max_bytes).await?;
492
    commands::upload_media(client, &media.mimetype, media.bytes.to_vec(), max_bytes).await
493
}
494
495
/// the server's copy, since the store lags a sync behind and an edit made just before would be lost
496
async fn fetch_pack(client: &Client, pack: &ImagePackSource) -> Result<Option<JsonObject>, Error> {
497
    match pack {
498
        ImagePackSource::User => client
499
            .account()
500
            .fetch_account_data(GlobalAccountDataEventType::from(USER_PACK))
501
            .await
502
            .map_err(|source| Error::from_sdk("reading the pack failed", source))?
503
            .map(|raw| parse(raw.json().get()))
504
            .transpose(),
505
        ImagePackSource::Room { room_id, state_key } => {
506
            let request = get_state_event_for_key::v3::Request::new(
507
                room_id.clone(),
508
                StateEventType::from(ROOM_PACKS),
509
                state_key.clone(),
510
            );
511
            match client.send(request).await {
512
                Ok(response) => parse(response.event_or_content.get()).map(Some),
513
                Err(error) if error.client_api_error_kind() == Some(&ApiErrorKind::NotFound) => {
514
                    Ok(None)
515
                }
516
                Err(source) => Err(Error::from_http("reading the pack failed", source)),
517
            }
518
        }
519
    }
520
}
521
522
async fn write_pack(
523
    client: &Client,
524
    pack: &ImagePackSource,
525
    content: JsonObject,
526
) -> Result<(), Error> {
527
    let written = match pack {
528
        ImagePackSource::User => client
529
            .account()
530
            .set_account_data_raw(USER_PACK.into(), raw(&content))
531
            .await
532
            .map(drop),
533
        ImagePackSource::Room { room_id, state_key } => client
534
            .get_room(room_id)
535
            .ok_or_else(Error::room_unknown)?
536
            .send_state_event_raw(ROOM_PACKS, state_key, JsonValue::Object(content))
537
            .await
538
            .map(drop),
539
    };
540
    written.map_err(|source| Error::from_sdk("writing the pack failed", source))
541
}
542
543
/// values this crate doesn't know ride along untouched, they may mean something to someone
544
fn with_pack_usage(mut image: JsonObject, meta: &JsonObject) -> JsonObject {
545
    if known_usage(image.get("usage")).is_some() {
546
        return image;
547
    }
548
    let mut usage = match meta.get("usage") {
549
        Some(JsonValue::Array(values)) if known_usage(meta.get("usage")).is_some() => {
550
            values.clone()
551
        }
552
        _ => usage_values(effective_usage(None, None)),
553
    };
554
    if let Some(JsonValue::Array(own)) = image.get("usage") {
555
        for value in own {
556
            if !usage.contains(value) {
557
                usage.push(value.clone());
558
            }
559
        }
560
    }
561
    image.insert("usage".to_owned(), usage.into());
562
    image
563
}
564
565
fn known_usage(value: Option<&JsonValue>) -> Option<Vec<PackUsage>> {
566
    let values = value?.as_array()?;
567
    let usage = [
568
        ("emoticon", PackUsage::Emoticon),
569
        ("sticker", PackUsage::Sticker),
570
    ]
571
    .into_iter()
572
    .filter_map(|(name, usage)| {
573
        values
574
            .iter()
575
            .any(|value| value.as_str() == Some(name))
576
            .then_some(usage)
577
    })
578
    .collect::<Vec<_>>();
579
    (!usage.is_empty()).then_some(usage)
580
}
581
582
/// `Some` of nothing would read back as inheriting, which `None` and `Patch::Clear` already say
583
fn validate_usage(usage: Option<&Vec<PackUsage>>) -> Result<(), Error> {
584
    if usage.is_some_and(Vec::is_empty) {
585
        return Err(Error::new(
586
            ErrorKind::Invalid,
587
            "usage needs at least one value, leave it unset to inherit",
588
        ));
589
    }
590
    Ok(())
591
}
592
593
fn apply_usage(image: &mut JsonObject, usage: Option<Vec<PackUsage>>) {
594
    if let Some(usage) = usage {
595
        image.insert("usage".to_owned(), usage_json(usage));
596
    }
597
}
598
599
fn sticker_body(image: &JsonObject, shortcode: &str) -> String {
600
    image
601
        .get("body")
602
        .and_then(JsonValue::as_str)
603
        .filter(|body| !body.trim().is_empty())
604
        .unwrap_or(shortcode)
605
        .to_owned()
606
}
607
608
fn free_shortcode(images: &JsonObject, wanted: &str) -> String {
609
    if !images.contains_key(wanted) {
610
        return wanted.to_owned();
611
    }
612
    (2..)
613
        .map(|n| format!("{wanted}-{n}"))
614
        .find(|candidate| !images.contains_key(candidate))
615
        .expect("an unbounded range finds a free name")
616
}
617
618
/// the object at `key`, made or replaced if it was missing or malformed
619
fn child<'a>(parent: &'a mut JsonObject, key: &str) -> &'a mut JsonObject {
620
    let slot = parent
621
        .entry(key.to_owned())
622
        .or_insert_with(|| JsonObject::new().into());
623
    if !slot.is_object() {
624
        *slot = JsonObject::new().into();
625
    }
626
    slot.as_object_mut().expect("just made it an object")
627
}
628
629
fn patch<T>(
630
    meta: &mut JsonObject,
631
    key: &str,
632
    change: Patch<T>,
633
    to_json: impl FnOnce(T) -> JsonValue,
634
) {
635
    match change {
636
        Patch::Keep => {}
637
        Patch::Clear => {
638
            meta.remove(key);
639
        }
640
        Patch::Set(value) => {
641
            meta.insert(key.to_owned(), to_json(value));
642
        }
643
    }
644
}
645
646
fn usage_json(usage: Vec<PackUsage>) -> JsonValue {
647
    usage_values(usage).into()
648
}
649
650
fn usage_values(usage: Vec<PackUsage>) -> Vec<JsonValue> {
651
    usage
652
        .into_iter()
653
        .map(|usage| match usage {
654
            PackUsage::Emoticon => "emoticon".into(),
655
            PackUsage::Sticker => "sticker".into(),
656
        })
657
        .collect()
658
}
659
660
fn info_json(info: PackImageInfo) -> JsonValue {
661
    let mut out = JsonObject::new();
662
    if let Some(mimetype) = info.mimetype {
663
        out.insert("mimetype".to_owned(), mimetype.into());
664
    }
665
    for (key, value) in [
666
        ("size", info.size_bytes),
667
        ("w", info.width),
668
        ("h", info.height),
669
    ] {
670
        if let Some(value) = value {
671
            out.insert(key.to_owned(), value.into());
672
        }
673
    }
674
    out.into()
675
}
676
677
fn mxc(value: &JsonValue) -> Option<OwnedMxcUri> {
678
    let uri = OwnedMxcUri::from(value.as_str()?);
679
    uri.is_valid().then_some(uri)
680
}
681
682
fn object(value: JsonValue) -> JsonObject {
683
    match value {
684
        JsonValue::Object(object) => object,
685
        _ => JsonObject::new(),
686
    }
687
}
688
689
fn parse(json: &str) -> Result<JsonObject, Error> {
690
    serde_json::from_str(json)
691
        .map_err(|source| Error::with_source(ErrorKind::Server, "the pack is malformed", source))
692
}
693
694
fn raw<T>(content: &JsonObject) -> Raw<T> {
695
    Raw::new(content)
696
        .expect("a json object always serialises")
697
        .cast_unchecked()
698
}
699
700
#[cfg(test)]
701
mod tests {
702
    use super::*;
703
704
    #[test]
705
    fn pack_usage_inherits_known_values_then_defaults_and_keeps_unknown_ones() {
706
        let pack = object(json!({ "usage": ["sticker", "unknown"] }));
707
        let inherited = with_pack_usage(object(json!({ "usage": [] })), &pack);
708
        assert_eq!(inherited.get("usage"), Some(&json!(["sticker", "unknown"])));
709
710
        let pack = object(json!({ "usage": ["emoticon"] }));
711
        let inherited = with_pack_usage(object(json!({ "usage": ["unknown"] })), &pack);
712
        assert_eq!(
713
            inherited.get("usage"),
714
            Some(&json!(["emoticon", "unknown"]))
715
        );
716
717
        let pack = object(json!({ "usage": ["sticker"] }));
718
        let inherited = with_pack_usage(object(json!({ "usage": ["emoticon", "unknown"] })), &pack);
719
        assert_eq!(
720
            inherited.get("usage"),
721
            Some(&json!(["emoticon", "unknown"]))
722
        );
723
724
        let pack = object(json!({ "usage": [] }));
725
        let inherited = with_pack_usage(object(json!({})), &pack);
726
        assert_eq!(
727
            inherited.get("usage"),
728
            Some(&json!(["emoticon", "sticker"]))
729
        );
730
731
        let pack = object(json!({ "usage": ["unknown"] }));
732
        let inherited = with_pack_usage(object(json!({})), &pack);
733
        assert_eq!(
734
            inherited.get("usage"),
735
            Some(&json!(["emoticon", "sticker"]))
736
        );
737
    }
738
739
    #[test]
740
    fn sticker_body_falls_back_when_missing_or_blank() {
741
        assert_eq!(sticker_body(&object(json!({})), "wave"), "wave");
742
        assert_eq!(sticker_body(&object(json!({ "body": "" })), "wave"), "wave");
743
        assert_eq!(
744
            sticker_body(&object(json!({ "body": " \t\n " })), "wave"),
745
            "wave"
746
        );
747
        assert_eq!(
748
            sticker_body(&object(json!({ "body": "  hello  " })), "wave"),
749
            "  hello  "
750
        );
751
    }
752
}