atagen

macltok

public
Star0
Watch0
fa51490b6ae98fdf5eeddb0799f4e3a160bf53cb
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/engine/packs.rs

fa51490b6ae9atagen2026-10-24 08:41 UTC1
use futures_util::{StreamExt, stream};
fa51490b6ae9atagen2026-10-24 08:41 UTC2
use matrix_sdk::{
fa51490b6ae9atagen2026-10-24 08:41 UTC3
    Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC4
    ruma::{
fa51490b6ae9atagen2026-10-24 08:41 UTC5
        api::{client::state::get_state_event_for_key, error::ErrorKind as ApiErrorKind},
fa51490b6ae9atagen2026-10-24 08:41 UTC6
        events::{GlobalAccountDataEventType, StateEventType, room::MediaSource},
fa51490b6ae9atagen2026-10-24 08:41 UTC7
        serde::{JsonObject, Raw},
fa51490b6ae9atagen2026-10-24 08:41 UTC8
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC9
};
fa51490b6ae9atagen2026-10-24 08:41 UTC10
use ruma::{OwnedEventId, OwnedMxcUri, OwnedRoomId};
fa51490b6ae9atagen2026-10-24 08:41 UTC11
use serde_json::{Value as JsonValue, json};
fa51490b6ae9atagen2026-10-24 08:41 UTC12
use tokio::sync::mpsc;
fa51490b6ae9atagen2026-10-24 08:41 UTC13
fa51490b6ae9atagen2026-10-24 08:41 UTC14
use super::{
fa51490b6ae9atagen2026-10-24 08:41 UTC15
    commands::{self, Job},
fa51490b6ae9atagen2026-10-24 08:41 UTC16
    enqueue, media,
fa51490b6ae9atagen2026-10-24 08:41 UTC17
    mutations::{Mutation, MutationJob},
fa51490b6ae9atagen2026-10-24 08:41 UTC18
};
fa51490b6ae9atagen2026-10-24 08:41 UTC19
use crate::{
fa51490b6ae9atagen2026-10-24 08:41 UTC20
    CommandOutput, Error, ErrorKind, ImagePackSource, PackCommand, PackImageInfo, PackUsage, Patch,
fa51490b6ae9atagen2026-10-24 08:41 UTC21
    normalise::{ENABLED_ROOMS, ROOM_PACKS, USER_PACK, resolve_pack_id, stored_pack},
fa51490b6ae9atagen2026-10-24 08:41 UTC22
    transport::Responder,
fa51490b6ae9atagen2026-10-24 08:41 UTC23
};
fa51490b6ae9atagen2026-10-24 08:41 UTC24
fa51490b6ae9atagen2026-10-24 08:41 UTC25
/// read-modify-writes of one pack, which run in order per pack
fa51490b6ae9atagen2026-10-24 08:41 UTC26
pub(super) enum PackEdit {
fa51490b6ae9atagen2026-10-24 08:41 UTC27
    Create {
fa51490b6ae9atagen2026-10-24 08:41 UTC28
        display_name: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC29
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC30
    Delete,
fa51490b6ae9atagen2026-10-24 08:41 UTC31
    Meta {
fa51490b6ae9atagen2026-10-24 08:41 UTC32
        display_name: Patch<String>,
fa51490b6ae9atagen2026-10-24 08:41 UTC33
        avatar: Patch<OwnedMxcUri>,
fa51490b6ae9atagen2026-10-24 08:41 UTC34
        usage: Patch<Vec<PackUsage>>,
fa51490b6ae9atagen2026-10-24 08:41 UTC35
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC36
    Put {
fa51490b6ae9atagen2026-10-24 08:41 UTC37
        shortcode: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC38
        mxc: OwnedMxcUri,
fa51490b6ae9atagen2026-10-24 08:41 UTC39
        body: Option<String>,
fa51490b6ae9atagen2026-10-24 08:41 UTC40
        usage: Option<Vec<PackUsage>>,
fa51490b6ae9atagen2026-10-24 08:41 UTC41
        info: Option<PackImageInfo>,
fa51490b6ae9atagen2026-10-24 08:41 UTC42
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC43
    Rename {
fa51490b6ae9atagen2026-10-24 08:41 UTC44
        from: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC45
        to: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC46
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC47
    Remove {
fa51490b6ae9atagen2026-10-24 08:41 UTC48
        shortcode: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC49
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC50
    /// copies already on our homeserver. `create` is the pack meta for a room pack that doesn't exist yet
fa51490b6ae9atagen2026-10-24 08:41 UTC51
    Insert {
fa51490b6ae9atagen2026-10-24 08:41 UTC52
        images: Vec<(String, JsonObject)>,
fa51490b6ae9atagen2026-10-24 08:41 UTC53
        create: Option<JsonObject>,
fa51490b6ae9atagen2026-10-24 08:41 UTC54
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC55
}
fa51490b6ae9atagen2026-10-24 08:41 UTC56
fa51490b6ae9atagen2026-10-24 08:41 UTC57
pub(super) async fn edit_pack(
fa51490b6ae9atagen2026-10-24 08:41 UTC58
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC59
    pack: &ImagePackSource,
fa51490b6ae9atagen2026-10-24 08:41 UTC60
    edit: PackEdit,
fa51490b6ae9atagen2026-10-24 08:41 UTC61
) -> Result<CommandOutput, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC62
    let current = fetch_pack(client, pack).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC63
    let mut content = current.clone().unwrap_or_default();
fa51490b6ae9atagen2026-10-24 08:41 UTC64
    let mut output = CommandOutput::Done;
fa51490b6ae9atagen2026-10-24 08:41 UTC65
    match edit {
fa51490b6ae9atagen2026-10-24 08:41 UTC66
        PackEdit::Create { display_name } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC67
            if !content.is_empty() {
fa51490b6ae9atagen2026-10-24 08:41 UTC68
                return Err(Error::new(
fa51490b6ae9atagen2026-10-24 08:41 UTC69
                    ErrorKind::Invalid,
fa51490b6ae9atagen2026-10-24 08:41 UTC70
                    "that state key already holds a pack",
fa51490b6ae9atagen2026-10-24 08:41 UTC71
                ));
fa51490b6ae9atagen2026-10-24 08:41 UTC72
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC73
            content = object(json!({ "pack": { "display_name": display_name }, "images": {} }));
fa51490b6ae9atagen2026-10-24 08:41 UTC74
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC75
        PackEdit::Delete => content.clear(),
fa51490b6ae9atagen2026-10-24 08:41 UTC76
        PackEdit::Meta {
fa51490b6ae9atagen2026-10-24 08:41 UTC77
            display_name,
fa51490b6ae9atagen2026-10-24 08:41 UTC78
            avatar,
fa51490b6ae9atagen2026-10-24 08:41 UTC79
            usage,
fa51490b6ae9atagen2026-10-24 08:41 UTC80
        } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC81
            let meta = child(&mut content, "pack");
fa51490b6ae9atagen2026-10-24 08:41 UTC82
            patch(meta, "display_name", display_name, JsonValue::String);
fa51490b6ae9atagen2026-10-24 08:41 UTC83
            patch(meta, "avatar_url", avatar, |uri| uri.to_string().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC84
            patch(meta, "usage", usage, usage_json);
fa51490b6ae9atagen2026-10-24 08:41 UTC85
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC86
        PackEdit::Put {
fa51490b6ae9atagen2026-10-24 08:41 UTC87
            shortcode,
fa51490b6ae9atagen2026-10-24 08:41 UTC88
            mxc,
fa51490b6ae9atagen2026-10-24 08:41 UTC89
            body,
fa51490b6ae9atagen2026-10-24 08:41 UTC90
            usage,
fa51490b6ae9atagen2026-10-24 08:41 UTC91
            info,
fa51490b6ae9atagen2026-10-24 08:41 UTC92
        } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC93
            let images = child(&mut content, "images");
fa51490b6ae9atagen2026-10-24 08:41 UTC94
            let mut image = match images.remove(&shortcode) {
fa51490b6ae9atagen2026-10-24 08:41 UTC95
                Some(JsonValue::Object(image)) => image,
fa51490b6ae9atagen2026-10-24 08:41 UTC96
                _ => JsonObject::new(),
fa51490b6ae9atagen2026-10-24 08:41 UTC97
            };
fa51490b6ae9atagen2026-10-24 08:41 UTC98
            let same_media = image.get("url").and_then(JsonValue::as_str) == Some(mxc.as_str());
fa51490b6ae9atagen2026-10-24 08:41 UTC99
            image.insert("url".to_owned(), mxc.to_string().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC100
            if let Some(body) = body {
fa51490b6ae9atagen2026-10-24 08:41 UTC101
                image.insert("body".to_owned(), body.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC102
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC103
            if let Some(usage) = usage {
fa51490b6ae9atagen2026-10-24 08:41 UTC104
                image.insert("usage".to_owned(), usage_json(usage));
fa51490b6ae9atagen2026-10-24 08:41 UTC105
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC106
            match info {
fa51490b6ae9atagen2026-10-24 08:41 UTC107
                Some(info) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC108
                    image.insert("info".to_owned(), info_json(info));
fa51490b6ae9atagen2026-10-24 08:41 UTC109
                }
fa51490b6ae9atagen2026-10-24 08:41 UTC110
                None if !same_media => {
fa51490b6ae9atagen2026-10-24 08:41 UTC111
                    image.remove("info");
fa51490b6ae9atagen2026-10-24 08:41 UTC112
                }
fa51490b6ae9atagen2026-10-24 08:41 UTC113
                None => {}
fa51490b6ae9atagen2026-10-24 08:41 UTC114
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC115
            images.insert(shortcode, image.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC116
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC117
        PackEdit::Rename { from, to } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC118
            let images = child(&mut content, "images");
fa51490b6ae9atagen2026-10-24 08:41 UTC119
            if images.contains_key(&to) {
fa51490b6ae9atagen2026-10-24 08:41 UTC120
                return Err(Error::new(ErrorKind::Invalid, "that shortcode is taken"));
fa51490b6ae9atagen2026-10-24 08:41 UTC121
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC122
            let image = images
fa51490b6ae9atagen2026-10-24 08:41 UTC123
                .remove(&from)
fa51490b6ae9atagen2026-10-24 08:41 UTC124
                .ok_or_else(|| Error::new(ErrorKind::Invalid, "no image has that shortcode"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC125
            images.insert(to, image);
fa51490b6ae9atagen2026-10-24 08:41 UTC126
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC127
        PackEdit::Remove { shortcode } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC128
            child(&mut content, "images").remove(&shortcode);
fa51490b6ae9atagen2026-10-24 08:41 UTC129
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC130
        PackEdit::Insert { images, create } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC131
            if content.is_empty() {
fa51490b6ae9atagen2026-10-24 08:41 UTC132
                match (create, pack) {
fa51490b6ae9atagen2026-10-24 08:41 UTC133
                    (Some(meta), _) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC134
                        content.insert("pack".to_owned(), meta.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC135
                    }
fa51490b6ae9atagen2026-10-24 08:41 UTC136
                    (None, ImagePackSource::Room { .. }) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC137
                        return Err(Error::new(
fa51490b6ae9atagen2026-10-24 08:41 UTC138
                            ErrorKind::Invalid,
fa51490b6ae9atagen2026-10-24 08:41 UTC139
                            "there's no pack there to copy into",
fa51490b6ae9atagen2026-10-24 08:41 UTC140
                        ));
fa51490b6ae9atagen2026-10-24 08:41 UTC141
                    }
fa51490b6ae9atagen2026-10-24 08:41 UTC142
                    (None, ImagePackSource::User) => {}
fa51490b6ae9atagen2026-10-24 08:41 UTC143
                }
fa51490b6ae9atagen2026-10-24 08:41 UTC144
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC145
            let target = child(&mut content, "images");
fa51490b6ae9atagen2026-10-24 08:41 UTC146
            let shortcodes = images
fa51490b6ae9atagen2026-10-24 08:41 UTC147
                .into_iter()
fa51490b6ae9atagen2026-10-24 08:41 UTC148
                .map(|(shortcode, image)| {
fa51490b6ae9atagen2026-10-24 08:41 UTC149
                    let free = free_shortcode(target, &shortcode);
fa51490b6ae9atagen2026-10-24 08:41 UTC150
                    target.insert(free.clone(), image.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC151
                    free
fa51490b6ae9atagen2026-10-24 08:41 UTC152
                })
fa51490b6ae9atagen2026-10-24 08:41 UTC153
                .collect();
fa51490b6ae9atagen2026-10-24 08:41 UTC154
            output = CommandOutput::Cloned {
fa51490b6ae9atagen2026-10-24 08:41 UTC155
                shortcodes,
fa51490b6ae9atagen2026-10-24 08:41 UTC156
                skipped: Vec::new(),
fa51490b6ae9atagen2026-10-24 08:41 UTC157
            };
fa51490b6ae9atagen2026-10-24 08:41 UTC158
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC159
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC160
    if current.as_ref() != Some(&content) {
fa51490b6ae9atagen2026-10-24 08:41 UTC161
        write_pack(client, pack, content).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC162
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC163
    Ok(output)
fa51490b6ae9atagen2026-10-24 08:41 UTC164
}
fa51490b6ae9atagen2026-10-24 08:41 UTC165
fa51490b6ae9atagen2026-10-24 08:41 UTC166
pub(super) async fn set_enabled_everywhere(
fa51490b6ae9atagen2026-10-24 08:41 UTC167
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC168
    room_id: OwnedRoomId,
fa51490b6ae9atagen2026-10-24 08:41 UTC169
    state_key: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC170
    enabled: bool,
fa51490b6ae9atagen2026-10-24 08:41 UTC171
) -> Result<(), Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC172
    let account = client.account();
fa51490b6ae9atagen2026-10-24 08:41 UTC173
    let current = account
fa51490b6ae9atagen2026-10-24 08:41 UTC174
        .fetch_account_data(GlobalAccountDataEventType::from(ENABLED_ROOMS))
fa51490b6ae9atagen2026-10-24 08:41 UTC175
        .await
fa51490b6ae9atagen2026-10-24 08:41 UTC176
        .map_err(|source| Error::from_sdk("reading enabled packs failed", source))?
fa51490b6ae9atagen2026-10-24 08:41 UTC177
        .map(|raw| parse(raw.json().get()))
fa51490b6ae9atagen2026-10-24 08:41 UTC178
        .transpose()?;
fa51490b6ae9atagen2026-10-24 08:41 UTC179
    let mut content = current.clone().unwrap_or_default();
fa51490b6ae9atagen2026-10-24 08:41 UTC180
    let rooms = child(&mut content, "rooms");
fa51490b6ae9atagen2026-10-24 08:41 UTC181
    if enabled {
fa51490b6ae9atagen2026-10-24 08:41 UTC182
        child(rooms, room_id.as_str()).insert(state_key, JsonObject::new().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC183
    } else {
fa51490b6ae9atagen2026-10-24 08:41 UTC184
        let state_keys = child(rooms, room_id.as_str());
fa51490b6ae9atagen2026-10-24 08:41 UTC185
        state_keys.remove(&state_key);
fa51490b6ae9atagen2026-10-24 08:41 UTC186
        if state_keys.is_empty() {
fa51490b6ae9atagen2026-10-24 08:41 UTC187
            rooms.remove(room_id.as_str());
fa51490b6ae9atagen2026-10-24 08:41 UTC188
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC189
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC190
    if current.as_ref() == Some(&content) {
fa51490b6ae9atagen2026-10-24 08:41 UTC191
        return Ok(());
fa51490b6ae9atagen2026-10-24 08:41 UTC192
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC193
    account
fa51490b6ae9atagen2026-10-24 08:41 UTC194
        .set_account_data_raw(ENABLED_ROOMS.into(), raw(&content))
fa51490b6ae9atagen2026-10-24 08:41 UTC195
        .await
fa51490b6ae9atagen2026-10-24 08:41 UTC196
        .map(drop)
fa51490b6ae9atagen2026-10-24 08:41 UTC197
        .map_err(|source| Error::from_sdk("writing enabled packs failed", source))
fa51490b6ae9atagen2026-10-24 08:41 UTC198
}
fa51490b6ae9atagen2026-10-24 08:41 UTC199
fa51490b6ae9atagen2026-10-24 08:41 UTC200
/// the commands that download, upload or send, so they stay off the per-pack queue and only
fa51490b6ae9atagen2026-10-24 08:41 UTC201
/// take their turn for the final write
fa51490b6ae9atagen2026-10-24 08:41 UTC202
pub(super) async fn run_detached(
fa51490b6ae9atagen2026-10-24 08:41 UTC203
    client: Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC204
    mutations: mpsc::Sender<MutationJob>,
fa51490b6ae9atagen2026-10-24 08:41 UTC205
    max_bytes: usize,
fa51490b6ae9atagen2026-10-24 08:41 UTC206
    command: PackCommand,
fa51490b6ae9atagen2026-10-24 08:41 UTC207
) -> Result<CommandOutput, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC208
    match command {
fa51490b6ae9atagen2026-10-24 08:41 UTC209
        PackCommand::SendSticker {
fa51490b6ae9atagen2026-10-24 08:41 UTC210
            room_id,
fa51490b6ae9atagen2026-10-24 08:41 UTC211
            pack_id,
fa51490b6ae9atagen2026-10-24 08:41 UTC212
            shortcode,
fa51490b6ae9atagen2026-10-24 08:41 UTC213
        } => send_sticker(&client, room_id, &pack_id, &shortcode)
fa51490b6ae9atagen2026-10-24 08:41 UTC214
            .await
fa51490b6ae9atagen2026-10-24 08:41 UTC215
            .map(|()| CommandOutput::Done),
fa51490b6ae9atagen2026-10-24 08:41 UTC216
        PackCommand::Rehost(mxc) => rehost(&client, mxc, None, max_bytes)
fa51490b6ae9atagen2026-10-24 08:41 UTC217
            .await
fa51490b6ae9atagen2026-10-24 08:41 UTC218
            .map(CommandOutput::Mxc),
fa51490b6ae9atagen2026-10-24 08:41 UTC219
        PackCommand::CloneImage {
fa51490b6ae9atagen2026-10-24 08:41 UTC220
            from_pack,
fa51490b6ae9atagen2026-10-24 08:41 UTC221
            shortcode,
fa51490b6ae9atagen2026-10-24 08:41 UTC222
            to,
fa51490b6ae9atagen2026-10-24 08:41 UTC223
        } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC224
            let image = pack_image(&client, &from_pack, &shortcode).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC225
            let image = rehost_image(&client, image, max_bytes).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC226
            insert(&mutations, to, vec![(shortcode, image)], None, Vec::new()).await
fa51490b6ae9atagen2026-10-24 08:41 UTC227
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC228
        PackCommand::CloneSticker {
fa51490b6ae9atagen2026-10-24 08:41 UTC229
            room_id,
fa51490b6ae9atagen2026-10-24 08:41 UTC230
            event_id,
fa51490b6ae9atagen2026-10-24 08:41 UTC231
            to,
fa51490b6ae9atagen2026-10-24 08:41 UTC232
            shortcode,
fa51490b6ae9atagen2026-10-24 08:41 UTC233
        } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC234
            let image = sticker_image(&client, room_id, event_id).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC235
            let image = rehost_image(&client, image, max_bytes).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC236
            insert(&mutations, to, vec![(shortcode, image)], None, Vec::new()).await
fa51490b6ae9atagen2026-10-24 08:41 UTC237
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC238
        PackCommand::ClonePack {
fa51490b6ae9atagen2026-10-24 08:41 UTC239
            from_pack,
fa51490b6ae9atagen2026-10-24 08:41 UTC240
            to,
fa51490b6ae9atagen2026-10-24 08:41 UTC241
            display_name,
fa51490b6ae9atagen2026-10-24 08:41 UTC242
        } => clone_pack(&client, &mutations, max_bytes, &from_pack, to, display_name).await,
fa51490b6ae9atagen2026-10-24 08:41 UTC243
        _ => Err(Error::new(ErrorKind::Other, "pack command was misrouted")),
fa51490b6ae9atagen2026-10-24 08:41 UTC244
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC245
}
fa51490b6ae9atagen2026-10-24 08:41 UTC246
fa51490b6ae9atagen2026-10-24 08:41 UTC247
async fn clone_pack(
fa51490b6ae9atagen2026-10-24 08:41 UTC248
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC249
    mutations: &mpsc::Sender<MutationJob>,
fa51490b6ae9atagen2026-10-24 08:41 UTC250
    max_bytes: usize,
fa51490b6ae9atagen2026-10-24 08:41 UTC251
    from_pack: &str,
fa51490b6ae9atagen2026-10-24 08:41 UTC252
    to: ImagePackSource,
fa51490b6ae9atagen2026-10-24 08:41 UTC253
    display_name: Option<String>,
fa51490b6ae9atagen2026-10-24 08:41 UTC254
) -> Result<CommandOutput, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC255
    let source = resolve_pack_id(client, from_pack)
fa51490b6ae9atagen2026-10-24 08:41 UTC256
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "no pack has that id"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC257
    let content = stored_pack(client, &source)
fa51490b6ae9atagen2026-10-24 08:41 UTC258
        .await?
fa51490b6ae9atagen2026-10-24 08:41 UTC259
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "that pack is empty"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC260
    let mut meta = match content.get("pack") {
fa51490b6ae9atagen2026-10-24 08:41 UTC261
        Some(JsonValue::Object(meta)) => meta.clone(),
fa51490b6ae9atagen2026-10-24 08:41 UTC262
        _ => JsonObject::new(),
fa51490b6ae9atagen2026-10-24 08:41 UTC263
    };
fa51490b6ae9atagen2026-10-24 08:41 UTC264
    let images: Vec<(String, JsonObject)> = match content.get("images") {
fa51490b6ae9atagen2026-10-24 08:41 UTC265
        Some(JsonValue::Object(images)) => images
fa51490b6ae9atagen2026-10-24 08:41 UTC266
            .iter()
fa51490b6ae9atagen2026-10-24 08:41 UTC267
            .filter_map(|(shortcode, image)| {
fa51490b6ae9atagen2026-10-24 08:41 UTC268
                let image = with_pack_usage(image.as_object()?.clone(), &meta);
fa51490b6ae9atagen2026-10-24 08:41 UTC269
                Some((shortcode.clone(), image))
fa51490b6ae9atagen2026-10-24 08:41 UTC270
            })
fa51490b6ae9atagen2026-10-24 08:41 UTC271
            .collect(),
fa51490b6ae9atagen2026-10-24 08:41 UTC272
        _ => Vec::new(),
fa51490b6ae9atagen2026-10-24 08:41 UTC273
    };
fa51490b6ae9atagen2026-10-24 08:41 UTC274
    let results: Vec<_> = stream::iter(images)
fa51490b6ae9atagen2026-10-24 08:41 UTC275
        .map(|(shortcode, image)| async move {
fa51490b6ae9atagen2026-10-24 08:41 UTC276
            match rehost_image(client, image, max_bytes).await {
fa51490b6ae9atagen2026-10-24 08:41 UTC277
                Ok(image) => Ok((shortcode, image)),
fa51490b6ae9atagen2026-10-24 08:41 UTC278
                Err(error) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC279
                    tracing::debug!(%error, shortcode, "skipping an image that wouldn't rehost");
fa51490b6ae9atagen2026-10-24 08:41 UTC280
                    Err(shortcode)
fa51490b6ae9atagen2026-10-24 08:41 UTC281
                }
fa51490b6ae9atagen2026-10-24 08:41 UTC282
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC283
        })
fa51490b6ae9atagen2026-10-24 08:41 UTC284
        .buffered(4)
fa51490b6ae9atagen2026-10-24 08:41 UTC285
        .collect()
fa51490b6ae9atagen2026-10-24 08:41 UTC286
        .await;
fa51490b6ae9atagen2026-10-24 08:41 UTC287
    let (copies, skipped): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok);
fa51490b6ae9atagen2026-10-24 08:41 UTC288
    let copies = copies.into_iter().filter_map(Result::ok).collect();
fa51490b6ae9atagen2026-10-24 08:41 UTC289
    let skipped = skipped.into_iter().filter_map(Result::err).collect();
fa51490b6ae9atagen2026-10-24 08:41 UTC290
    let create = match to {
fa51490b6ae9atagen2026-10-24 08:41 UTC291
        ImagePackSource::User => None,
fa51490b6ae9atagen2026-10-24 08:41 UTC292
        ImagePackSource::Room { .. } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC293
            if let Some(display_name) = display_name {
fa51490b6ae9atagen2026-10-24 08:41 UTC294
                meta.insert("display_name".to_owned(), display_name.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC295
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC296
            meta.remove("usage");
fa51490b6ae9atagen2026-10-24 08:41 UTC297
            if let Some(avatar) = meta.remove("avatar_url").and_then(|uri| mxc(&uri))
fa51490b6ae9atagen2026-10-24 08:41 UTC298
                && let Ok(avatar) = rehost(client, avatar, None, max_bytes).await
fa51490b6ae9atagen2026-10-24 08:41 UTC299
            {
fa51490b6ae9atagen2026-10-24 08:41 UTC300
                meta.insert("avatar_url".to_owned(), avatar.to_string().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC301
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC302
            Some(meta)
fa51490b6ae9atagen2026-10-24 08:41 UTC303
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC304
    };
fa51490b6ae9atagen2026-10-24 08:41 UTC305
    insert(mutations, to, copies, create, skipped).await
fa51490b6ae9atagen2026-10-24 08:41 UTC306
}
fa51490b6ae9atagen2026-10-24 08:41 UTC307
fa51490b6ae9atagen2026-10-24 08:41 UTC308
async fn insert(
fa51490b6ae9atagen2026-10-24 08:41 UTC309
    mutations: &mpsc::Sender<MutationJob>,
fa51490b6ae9atagen2026-10-24 08:41 UTC310
    pack: ImagePackSource,
fa51490b6ae9atagen2026-10-24 08:41 UTC311
    images: Vec<(String, JsonObject)>,
fa51490b6ae9atagen2026-10-24 08:41 UTC312
    create: Option<JsonObject>,
fa51490b6ae9atagen2026-10-24 08:41 UTC313
    skipped: Vec<String>,
fa51490b6ae9atagen2026-10-24 08:41 UTC314
) -> Result<CommandOutput, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC315
    let (reply, answer) = Responder::channel();
fa51490b6ae9atagen2026-10-24 08:41 UTC316
    let command = Mutation::Pack {
fa51490b6ae9atagen2026-10-24 08:41 UTC317
        pack,
fa51490b6ae9atagen2026-10-24 08:41 UTC318
        edit: PackEdit::Insert { images, create },
fa51490b6ae9atagen2026-10-24 08:41 UTC319
    };
fa51490b6ae9atagen2026-10-24 08:41 UTC320
    enqueue(mutations, Job { command, reply });
fa51490b6ae9atagen2026-10-24 08:41 UTC321
    match answer.await? {
fa51490b6ae9atagen2026-10-24 08:41 UTC322
        CommandOutput::Cloned { shortcodes, .. } => Ok(CommandOutput::Cloned {
fa51490b6ae9atagen2026-10-24 08:41 UTC323
            shortcodes,
fa51490b6ae9atagen2026-10-24 08:41 UTC324
            skipped,
fa51490b6ae9atagen2026-10-24 08:41 UTC325
        }),
fa51490b6ae9atagen2026-10-24 08:41 UTC326
        output => Ok(output),
fa51490b6ae9atagen2026-10-24 08:41 UTC327
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC328
}
fa51490b6ae9atagen2026-10-24 08:41 UTC329
fa51490b6ae9atagen2026-10-24 08:41 UTC330
async fn send_sticker(
fa51490b6ae9atagen2026-10-24 08:41 UTC331
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC332
    room_id: OwnedRoomId,
fa51490b6ae9atagen2026-10-24 08:41 UTC333
    pack_id: &str,
fa51490b6ae9atagen2026-10-24 08:41 UTC334
    shortcode: &str,
fa51490b6ae9atagen2026-10-24 08:41 UTC335
) -> Result<(), Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC336
    let room = client.get_room(&room_id).ok_or_else(Error::room_unknown)?;
fa51490b6ae9atagen2026-10-24 08:41 UTC337
    let image = pack_image(client, pack_id, shortcode).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC338
    let url = image
fa51490b6ae9atagen2026-10-24 08:41 UTC339
        .get("url")
fa51490b6ae9atagen2026-10-24 08:41 UTC340
        .and_then(mxc)
fa51490b6ae9atagen2026-10-24 08:41 UTC341
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "that image has no usable url"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC342
    let content = json!({
fa51490b6ae9atagen2026-10-24 08:41 UTC343
        "body": image.get("body").and_then(JsonValue::as_str).unwrap_or(shortcode),
fa51490b6ae9atagen2026-10-24 08:41 UTC344
        "url": url,
fa51490b6ae9atagen2026-10-24 08:41 UTC345
        "info": image.get("info").cloned().unwrap_or_else(|| json!({})),
fa51490b6ae9atagen2026-10-24 08:41 UTC346
    });
fa51490b6ae9atagen2026-10-24 08:41 UTC347
    room.send_queue()
fa51490b6ae9atagen2026-10-24 08:41 UTC348
        .send_raw(raw(&object(content)), "m.sticker".to_owned())
fa51490b6ae9atagen2026-10-24 08:41 UTC349
        .await
fa51490b6ae9atagen2026-10-24 08:41 UTC350
        .map(drop)
fa51490b6ae9atagen2026-10-24 08:41 UTC351
        .map_err(|source| {
fa51490b6ae9atagen2026-10-24 08:41 UTC352
            Error::with_source(ErrorKind::Store, "queueing the sticker failed", source)
fa51490b6ae9atagen2026-10-24 08:41 UTC353
        })
fa51490b6ae9atagen2026-10-24 08:41 UTC354
}
fa51490b6ae9atagen2026-10-24 08:41 UTC355
fa51490b6ae9atagen2026-10-24 08:41 UTC356
/// the image with its pack's usage folded in, so it means the same once it leaves the pack
fa51490b6ae9atagen2026-10-24 08:41 UTC357
async fn pack_image(client: &Client, pack_id: &str, shortcode: &str) -> Result<JsonObject, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC358
    let source = resolve_pack_id(client, pack_id)
fa51490b6ae9atagen2026-10-24 08:41 UTC359
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "no pack has that id"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC360
    let content = stored_pack(client, &source).await?.unwrap_or_default();
fa51490b6ae9atagen2026-10-24 08:41 UTC361
    let image = content
fa51490b6ae9atagen2026-10-24 08:41 UTC362
        .get("images")
fa51490b6ae9atagen2026-10-24 08:41 UTC363
        .and_then(|images| images.get(shortcode))
fa51490b6ae9atagen2026-10-24 08:41 UTC364
        .and_then(JsonValue::as_object)
fa51490b6ae9atagen2026-10-24 08:41 UTC365
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "no image has that shortcode"))?
fa51490b6ae9atagen2026-10-24 08:41 UTC366
        .clone();
fa51490b6ae9atagen2026-10-24 08:41 UTC367
    let meta = content
fa51490b6ae9atagen2026-10-24 08:41 UTC368
        .get("pack")
fa51490b6ae9atagen2026-10-24 08:41 UTC369
        .and_then(JsonValue::as_object)
fa51490b6ae9atagen2026-10-24 08:41 UTC370
        .cloned()
fa51490b6ae9atagen2026-10-24 08:41 UTC371
        .unwrap_or_default();
fa51490b6ae9atagen2026-10-24 08:41 UTC372
    Ok(with_pack_usage(image, &meta))
fa51490b6ae9atagen2026-10-24 08:41 UTC373
}
fa51490b6ae9atagen2026-10-24 08:41 UTC374
fa51490b6ae9atagen2026-10-24 08:41 UTC375
async fn sticker_image(
fa51490b6ae9atagen2026-10-24 08:41 UTC376
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC377
    room_id: OwnedRoomId,
fa51490b6ae9atagen2026-10-24 08:41 UTC378
    event_id: OwnedEventId,
fa51490b6ae9atagen2026-10-24 08:41 UTC379
) -> Result<JsonObject, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC380
    let room = client.get_room(&room_id).ok_or_else(Error::room_unknown)?;
fa51490b6ae9atagen2026-10-24 08:41 UTC381
    let event = room
fa51490b6ae9atagen2026-10-24 08:41 UTC382
        .load_or_fetch_event(&event_id, None)
fa51490b6ae9atagen2026-10-24 08:41 UTC383
        .await
fa51490b6ae9atagen2026-10-24 08:41 UTC384
        .map_err(|source| Error::from_sdk("loading the sticker failed", source))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC385
    let raw = event.raw();
fa51490b6ae9atagen2026-10-24 08:41 UTC386
    if raw.get_field::<String>("type").ok().flatten().as_deref() != Some("m.sticker") {
fa51490b6ae9atagen2026-10-24 08:41 UTC387
        return Err(Error::new(ErrorKind::Invalid, "that event isn't a sticker"));
fa51490b6ae9atagen2026-10-24 08:41 UTC388
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC389
    let content = raw
fa51490b6ae9atagen2026-10-24 08:41 UTC390
        .get_field::<JsonObject>("content")
fa51490b6ae9atagen2026-10-24 08:41 UTC391
        .ok()
fa51490b6ae9atagen2026-10-24 08:41 UTC392
        .flatten()
fa51490b6ae9atagen2026-10-24 08:41 UTC393
        .unwrap_or_default();
fa51490b6ae9atagen2026-10-24 08:41 UTC394
    let mut image = JsonObject::new();
fa51490b6ae9atagen2026-10-24 08:41 UTC395
    for key in ["url", "body", "info"] {
fa51490b6ae9atagen2026-10-24 08:41 UTC396
        if let Some(value) = content.get(key) {
fa51490b6ae9atagen2026-10-24 08:41 UTC397
            image.insert(key.to_owned(), value.clone());
fa51490b6ae9atagen2026-10-24 08:41 UTC398
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC399
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC400
    image.insert("usage".to_owned(), usage_json(vec![PackUsage::Sticker]));
fa51490b6ae9atagen2026-10-24 08:41 UTC401
    Ok(image)
fa51490b6ae9atagen2026-10-24 08:41 UTC402
}
fa51490b6ae9atagen2026-10-24 08:41 UTC403
fa51490b6ae9atagen2026-10-24 08:41 UTC404
async fn rehost_image(
fa51490b6ae9atagen2026-10-24 08:41 UTC405
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC406
    mut image: JsonObject,
fa51490b6ae9atagen2026-10-24 08:41 UTC407
    max_bytes: usize,
fa51490b6ae9atagen2026-10-24 08:41 UTC408
) -> Result<JsonObject, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC409
    let url = image
fa51490b6ae9atagen2026-10-24 08:41 UTC410
        .get("url")
fa51490b6ae9atagen2026-10-24 08:41 UTC411
        .and_then(mxc)
fa51490b6ae9atagen2026-10-24 08:41 UTC412
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "that image has no usable url"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC413
    let mimetype = image
fa51490b6ae9atagen2026-10-24 08:41 UTC414
        .get("info")
fa51490b6ae9atagen2026-10-24 08:41 UTC415
        .and_then(|info| info.get("mimetype"))
fa51490b6ae9atagen2026-10-24 08:41 UTC416
        .and_then(JsonValue::as_str)
fa51490b6ae9atagen2026-10-24 08:41 UTC417
        .map(str::to_owned);
fa51490b6ae9atagen2026-10-24 08:41 UTC418
    let url = rehost(client, url, mimetype, max_bytes).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC419
    image.insert("url".to_owned(), url.to_string().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC420
    Ok(image)
fa51490b6ae9atagen2026-10-24 08:41 UTC421
}
fa51490b6ae9atagen2026-10-24 08:41 UTC422
fa51490b6ae9atagen2026-10-24 08:41 UTC423
pub(super) async fn rehost(
fa51490b6ae9atagen2026-10-24 08:41 UTC424
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC425
    mxc: OwnedMxcUri,
fa51490b6ae9atagen2026-10-24 08:41 UTC426
    mimetype: Option<String>,
fa51490b6ae9atagen2026-10-24 08:41 UTC427
    max_bytes: usize,
fa51490b6ae9atagen2026-10-24 08:41 UTC428
) -> Result<OwnedMxcUri, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC429
    let homeserver = client.user_id().map(ruma::UserId::server_name);
fa51490b6ae9atagen2026-10-24 08:41 UTC430
    if mxc
fa51490b6ae9atagen2026-10-24 08:41 UTC431
        .server_name()
fa51490b6ae9atagen2026-10-24 08:41 UTC432
        .ok()
fa51490b6ae9atagen2026-10-24 08:41 UTC433
        .is_some_and(|server| Some(server) == homeserver)
fa51490b6ae9atagen2026-10-24 08:41 UTC434
    {
fa51490b6ae9atagen2026-10-24 08:41 UTC435
        return Ok(mxc);
fa51490b6ae9atagen2026-10-24 08:41 UTC436
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC437
    let media = media::download_file(client, MediaSource::Plain(mxc), mimetype, max_bytes).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC438
    commands::upload_media(client, &media.mimetype, media.bytes.to_vec(), max_bytes).await
fa51490b6ae9atagen2026-10-24 08:41 UTC439
}
fa51490b6ae9atagen2026-10-24 08:41 UTC440
fa51490b6ae9atagen2026-10-24 08:41 UTC441
/// the server's copy, since the store lags a sync behind and an edit made just before would be lost
fa51490b6ae9atagen2026-10-24 08:41 UTC442
async fn fetch_pack(client: &Client, pack: &ImagePackSource) -> Result<Option<JsonObject>, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC443
    match pack {
fa51490b6ae9atagen2026-10-24 08:41 UTC444
        ImagePackSource::User => client
fa51490b6ae9atagen2026-10-24 08:41 UTC445
            .account()
fa51490b6ae9atagen2026-10-24 08:41 UTC446
            .fetch_account_data(GlobalAccountDataEventType::from(USER_PACK))
fa51490b6ae9atagen2026-10-24 08:41 UTC447
            .await
fa51490b6ae9atagen2026-10-24 08:41 UTC448
            .map_err(|source| Error::from_sdk("reading the pack failed", source))?
fa51490b6ae9atagen2026-10-24 08:41 UTC449
            .map(|raw| parse(raw.json().get()))
fa51490b6ae9atagen2026-10-24 08:41 UTC450
            .transpose(),
fa51490b6ae9atagen2026-10-24 08:41 UTC451
        ImagePackSource::Room { room_id, state_key } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC452
            let request = get_state_event_for_key::v3::Request::new(
fa51490b6ae9atagen2026-10-24 08:41 UTC453
                room_id.clone(),
fa51490b6ae9atagen2026-10-24 08:41 UTC454
                StateEventType::from(ROOM_PACKS),
fa51490b6ae9atagen2026-10-24 08:41 UTC455
                state_key.clone(),
fa51490b6ae9atagen2026-10-24 08:41 UTC456
            );
fa51490b6ae9atagen2026-10-24 08:41 UTC457
            match client.send(request).await {
fa51490b6ae9atagen2026-10-24 08:41 UTC458
                Ok(response) => parse(response.event_or_content.get()).map(Some),
fa51490b6ae9atagen2026-10-24 08:41 UTC459
                Err(error) if error.client_api_error_kind() == Some(&ApiErrorKind::NotFound) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC460
                    Ok(None)
fa51490b6ae9atagen2026-10-24 08:41 UTC461
                }
fa51490b6ae9atagen2026-10-24 08:41 UTC462
                Err(source) => Err(Error::from_http("reading the pack failed", source)),
fa51490b6ae9atagen2026-10-24 08:41 UTC463
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC464
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC465
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC466
}
fa51490b6ae9atagen2026-10-24 08:41 UTC467
fa51490b6ae9atagen2026-10-24 08:41 UTC468
async fn write_pack(
fa51490b6ae9atagen2026-10-24 08:41 UTC469
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC470
    pack: &ImagePackSource,
fa51490b6ae9atagen2026-10-24 08:41 UTC471
    content: JsonObject,
fa51490b6ae9atagen2026-10-24 08:41 UTC472
) -> Result<(), Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC473
    let written = match pack {
fa51490b6ae9atagen2026-10-24 08:41 UTC474
        ImagePackSource::User => client
fa51490b6ae9atagen2026-10-24 08:41 UTC475
            .account()
fa51490b6ae9atagen2026-10-24 08:41 UTC476
            .set_account_data_raw(USER_PACK.into(), raw(&content))
fa51490b6ae9atagen2026-10-24 08:41 UTC477
            .await
fa51490b6ae9atagen2026-10-24 08:41 UTC478
            .map(drop),
fa51490b6ae9atagen2026-10-24 08:41 UTC479
        ImagePackSource::Room { room_id, state_key } => client
fa51490b6ae9atagen2026-10-24 08:41 UTC480
            .get_room(room_id)
fa51490b6ae9atagen2026-10-24 08:41 UTC481
            .ok_or_else(Error::room_unknown)?
fa51490b6ae9atagen2026-10-24 08:41 UTC482
            .send_state_event_raw(ROOM_PACKS, state_key, JsonValue::Object(content))
fa51490b6ae9atagen2026-10-24 08:41 UTC483
            .await
fa51490b6ae9atagen2026-10-24 08:41 UTC484
            .map(drop),
fa51490b6ae9atagen2026-10-24 08:41 UTC485
    };
fa51490b6ae9atagen2026-10-24 08:41 UTC486
    written.map_err(|source| Error::from_sdk("writing the pack failed", source))
fa51490b6ae9atagen2026-10-24 08:41 UTC487
}
fa51490b6ae9atagen2026-10-24 08:41 UTC488
fa51490b6ae9atagen2026-10-24 08:41 UTC489
fn with_pack_usage(mut image: JsonObject, meta: &JsonObject) -> JsonObject {
fa51490b6ae9atagen2026-10-24 08:41 UTC490
    if !image.contains_key("usage")
fa51490b6ae9atagen2026-10-24 08:41 UTC491
        && let Some(usage) = meta.get("usage")
fa51490b6ae9atagen2026-10-24 08:41 UTC492
    {
fa51490b6ae9atagen2026-10-24 08:41 UTC493
        image.insert("usage".to_owned(), usage.clone());
fa51490b6ae9atagen2026-10-24 08:41 UTC494
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC495
    image
fa51490b6ae9atagen2026-10-24 08:41 UTC496
}
fa51490b6ae9atagen2026-10-24 08:41 UTC497
fa51490b6ae9atagen2026-10-24 08:41 UTC498
fn free_shortcode(images: &JsonObject, wanted: &str) -> String {
fa51490b6ae9atagen2026-10-24 08:41 UTC499
    if !images.contains_key(wanted) {
fa51490b6ae9atagen2026-10-24 08:41 UTC500
        return wanted.to_owned();
fa51490b6ae9atagen2026-10-24 08:41 UTC501
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC502
    (2..)
fa51490b6ae9atagen2026-10-24 08:41 UTC503
        .map(|n| format!("{wanted}-{n}"))
fa51490b6ae9atagen2026-10-24 08:41 UTC504
        .find(|candidate| !images.contains_key(candidate))
fa51490b6ae9atagen2026-10-24 08:41 UTC505
        .expect("an unbounded range finds a free name")
fa51490b6ae9atagen2026-10-24 08:41 UTC506
}
fa51490b6ae9atagen2026-10-24 08:41 UTC507
fa51490b6ae9atagen2026-10-24 08:41 UTC508
/// the object at `key`, made or replaced if it was missing or malformed
fa51490b6ae9atagen2026-10-24 08:41 UTC509
fn child<'a>(parent: &'a mut JsonObject, key: &str) -> &'a mut JsonObject {
fa51490b6ae9atagen2026-10-24 08:41 UTC510
    let slot = parent
fa51490b6ae9atagen2026-10-24 08:41 UTC511
        .entry(key.to_owned())
fa51490b6ae9atagen2026-10-24 08:41 UTC512
        .or_insert_with(|| JsonObject::new().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC513
    if !slot.is_object() {
fa51490b6ae9atagen2026-10-24 08:41 UTC514
        *slot = JsonObject::new().into();
fa51490b6ae9atagen2026-10-24 08:41 UTC515
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC516
    slot.as_object_mut().expect("just made it an object")
fa51490b6ae9atagen2026-10-24 08:41 UTC517
}
fa51490b6ae9atagen2026-10-24 08:41 UTC518
fa51490b6ae9atagen2026-10-24 08:41 UTC519
fn patch<T>(
fa51490b6ae9atagen2026-10-24 08:41 UTC520
    meta: &mut JsonObject,
fa51490b6ae9atagen2026-10-24 08:41 UTC521
    key: &str,
fa51490b6ae9atagen2026-10-24 08:41 UTC522
    change: Patch<T>,
fa51490b6ae9atagen2026-10-24 08:41 UTC523
    to_json: impl FnOnce(T) -> JsonValue,
fa51490b6ae9atagen2026-10-24 08:41 UTC524
) {
fa51490b6ae9atagen2026-10-24 08:41 UTC525
    match change {
fa51490b6ae9atagen2026-10-24 08:41 UTC526
        Patch::Keep => {}
fa51490b6ae9atagen2026-10-24 08:41 UTC527
        Patch::Clear => {
fa51490b6ae9atagen2026-10-24 08:41 UTC528
            meta.remove(key);
fa51490b6ae9atagen2026-10-24 08:41 UTC529
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC530
        Patch::Set(value) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC531
            meta.insert(key.to_owned(), to_json(value));
fa51490b6ae9atagen2026-10-24 08:41 UTC532
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC533
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC534
}
fa51490b6ae9atagen2026-10-24 08:41 UTC535
fa51490b6ae9atagen2026-10-24 08:41 UTC536
fn usage_json(usage: Vec<PackUsage>) -> JsonValue {
fa51490b6ae9atagen2026-10-24 08:41 UTC537
    usage
fa51490b6ae9atagen2026-10-24 08:41 UTC538
        .into_iter()
fa51490b6ae9atagen2026-10-24 08:41 UTC539
        .map(|usage| match usage {
fa51490b6ae9atagen2026-10-24 08:41 UTC540
            PackUsage::Emoticon => "emoticon",
fa51490b6ae9atagen2026-10-24 08:41 UTC541
            PackUsage::Sticker => "sticker",
fa51490b6ae9atagen2026-10-24 08:41 UTC542
        })
fa51490b6ae9atagen2026-10-24 08:41 UTC543
        .collect::<Vec<_>>()
fa51490b6ae9atagen2026-10-24 08:41 UTC544
        .into()
fa51490b6ae9atagen2026-10-24 08:41 UTC545
}
fa51490b6ae9atagen2026-10-24 08:41 UTC546
fa51490b6ae9atagen2026-10-24 08:41 UTC547
fn info_json(info: PackImageInfo) -> JsonValue {
fa51490b6ae9atagen2026-10-24 08:41 UTC548
    let mut out = JsonObject::new();
fa51490b6ae9atagen2026-10-24 08:41 UTC549
    if let Some(mimetype) = info.mimetype {
fa51490b6ae9atagen2026-10-24 08:41 UTC550
        out.insert("mimetype".to_owned(), mimetype.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC551
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC552
    for (key, value) in [
fa51490b6ae9atagen2026-10-24 08:41 UTC553
        ("size", info.size_bytes),
fa51490b6ae9atagen2026-10-24 08:41 UTC554
        ("w", info.width),
fa51490b6ae9atagen2026-10-24 08:41 UTC555
        ("h", info.height),
fa51490b6ae9atagen2026-10-24 08:41 UTC556
    ] {
fa51490b6ae9atagen2026-10-24 08:41 UTC557
        if let Some(value) = value {
fa51490b6ae9atagen2026-10-24 08:41 UTC558
            out.insert(key.to_owned(), value.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC559
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC560
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC561
    out.into()
fa51490b6ae9atagen2026-10-24 08:41 UTC562
}
fa51490b6ae9atagen2026-10-24 08:41 UTC563
fa51490b6ae9atagen2026-10-24 08:41 UTC564
fn mxc(value: &JsonValue) -> Option<OwnedMxcUri> {
fa51490b6ae9atagen2026-10-24 08:41 UTC565
    let uri = OwnedMxcUri::from(value.as_str()?);
fa51490b6ae9atagen2026-10-24 08:41 UTC566
    uri.is_valid().then_some(uri)
fa51490b6ae9atagen2026-10-24 08:41 UTC567
}
fa51490b6ae9atagen2026-10-24 08:41 UTC568
fa51490b6ae9atagen2026-10-24 08:41 UTC569
fn object(value: JsonValue) -> JsonObject {
fa51490b6ae9atagen2026-10-24 08:41 UTC570
    match value {
fa51490b6ae9atagen2026-10-24 08:41 UTC571
        JsonValue::Object(object) => object,
fa51490b6ae9atagen2026-10-24 08:41 UTC572
        _ => JsonObject::new(),
fa51490b6ae9atagen2026-10-24 08:41 UTC573
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC574
}
fa51490b6ae9atagen2026-10-24 08:41 UTC575
fa51490b6ae9atagen2026-10-24 08:41 UTC576
fn parse(json: &str) -> Result<JsonObject, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC577
    serde_json::from_str(json)
fa51490b6ae9atagen2026-10-24 08:41 UTC578
        .map_err(|source| Error::with_source(ErrorKind::Server, "the pack is malformed", source))
fa51490b6ae9atagen2026-10-24 08:41 UTC579
}
fa51490b6ae9atagen2026-10-24 08:41 UTC580
fa51490b6ae9atagen2026-10-24 08:41 UTC581
fn raw<T>(content: &JsonObject) -> Raw<T> {
fa51490b6ae9atagen2026-10-24 08:41 UTC582
    Raw::new(content)
fa51490b6ae9atagen2026-10-24 08:41 UTC583
        .expect("a json object always serialises")
fa51490b6ae9atagen2026-10-24 08:41 UTC584
        .cast_unchecked()
fa51490b6ae9atagen2026-10-24 08:41 UTC585
}