atagen

macltok

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