atagen

macltok

public
Star0
Watch0
d8e76b56867c28deec549cb435c865e4f830e4bf
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,
d8e76b56867catagen2026-10-25 09:08 UTC21
    normalise::{
d8e76b56867catagen2026-10-25 09:08 UTC22
        ENABLED_ROOMS, ROOM_PACKS, USER_PACK, effective_usage, resolve_pack_id, stored_pack,
d8e76b56867catagen2026-10-25 09:08 UTC23
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC24
    transport::Responder,
fa51490b6ae9atagen2026-10-24 08:41 UTC25
};
fa51490b6ae9atagen2026-10-24 08:41 UTC26
fa51490b6ae9atagen2026-10-24 08:41 UTC27
/// read-modify-writes of one pack, which run in order per pack
fa51490b6ae9atagen2026-10-24 08:41 UTC28
pub(super) enum PackEdit {
fa51490b6ae9atagen2026-10-24 08:41 UTC29
    Create {
fa51490b6ae9atagen2026-10-24 08:41 UTC30
        display_name: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC31
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC32
    Delete,
fa51490b6ae9atagen2026-10-24 08:41 UTC33
    Meta {
fa51490b6ae9atagen2026-10-24 08:41 UTC34
        display_name: Patch<String>,
fa51490b6ae9atagen2026-10-24 08:41 UTC35
        avatar: Patch<OwnedMxcUri>,
fa51490b6ae9atagen2026-10-24 08:41 UTC36
        usage: Patch<Vec<PackUsage>>,
fa51490b6ae9atagen2026-10-24 08:41 UTC37
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC38
    Put {
fa51490b6ae9atagen2026-10-24 08:41 UTC39
        shortcode: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC40
        mxc: OwnedMxcUri,
fa51490b6ae9atagen2026-10-24 08:41 UTC41
        body: Option<String>,
fa51490b6ae9atagen2026-10-24 08:41 UTC42
        usage: Option<Vec<PackUsage>>,
fa51490b6ae9atagen2026-10-24 08:41 UTC43
        info: Option<PackImageInfo>,
fa51490b6ae9atagen2026-10-24 08:41 UTC44
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC45
    Rename {
fa51490b6ae9atagen2026-10-24 08:41 UTC46
        from: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC47
        to: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC48
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC49
    Remove {
fa51490b6ae9atagen2026-10-24 08:41 UTC50
        shortcode: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC51
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC52
    /// copies already on our homeserver. `create` is the pack meta for a room pack that doesn't exist yet
fa51490b6ae9atagen2026-10-24 08:41 UTC53
    Insert {
fa51490b6ae9atagen2026-10-24 08:41 UTC54
        images: Vec<(String, JsonObject)>,
fa51490b6ae9atagen2026-10-24 08:41 UTC55
        create: Option<JsonObject>,
fa51490b6ae9atagen2026-10-24 08:41 UTC56
    },
fa51490b6ae9atagen2026-10-24 08:41 UTC57
}
fa51490b6ae9atagen2026-10-24 08:41 UTC58
fa51490b6ae9atagen2026-10-24 08:41 UTC59
pub(super) async fn edit_pack(
fa51490b6ae9atagen2026-10-24 08:41 UTC60
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC61
    pack: &ImagePackSource,
fa51490b6ae9atagen2026-10-24 08:41 UTC62
    edit: PackEdit,
fa51490b6ae9atagen2026-10-24 08:41 UTC63
) -> Result<CommandOutput, Error> {
d8e76b56867catagen2026-10-25 09:08 UTC64
    match &edit {
d8e76b56867catagen2026-10-25 09:08 UTC65
        PackEdit::Meta {
d8e76b56867catagen2026-10-25 09:08 UTC66
            usage: Patch::Set(usage),
d8e76b56867catagen2026-10-25 09:08 UTC67
            ..
d8e76b56867catagen2026-10-25 09:08 UTC68
        } => validate_usage(Some(usage))?,
d8e76b56867catagen2026-10-25 09:08 UTC69
        PackEdit::Put { usage, .. } => validate_usage(usage.as_ref())?,
d8e76b56867catagen2026-10-25 09:08 UTC70
        _ => {}
d8e76b56867catagen2026-10-25 09:08 UTC71
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC72
    let current = fetch_pack(client, pack).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC73
    let mut content = current.clone().unwrap_or_default();
fa51490b6ae9atagen2026-10-24 08:41 UTC74
    let mut output = CommandOutput::Done;
fa51490b6ae9atagen2026-10-24 08:41 UTC75
    match edit {
fa51490b6ae9atagen2026-10-24 08:41 UTC76
        PackEdit::Create { display_name } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC77
            if !content.is_empty() {
fa51490b6ae9atagen2026-10-24 08:41 UTC78
                return Err(Error::new(
fa51490b6ae9atagen2026-10-24 08:41 UTC79
                    ErrorKind::Invalid,
fa51490b6ae9atagen2026-10-24 08:41 UTC80
                    "that state key already holds a pack",
fa51490b6ae9atagen2026-10-24 08:41 UTC81
                ));
fa51490b6ae9atagen2026-10-24 08:41 UTC82
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC83
            content = object(json!({ "pack": { "display_name": display_name }, "images": {} }));
fa51490b6ae9atagen2026-10-24 08:41 UTC84
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC85
        PackEdit::Delete => content.clear(),
fa51490b6ae9atagen2026-10-24 08:41 UTC86
        PackEdit::Meta {
fa51490b6ae9atagen2026-10-24 08:41 UTC87
            display_name,
fa51490b6ae9atagen2026-10-24 08:41 UTC88
            avatar,
fa51490b6ae9atagen2026-10-24 08:41 UTC89
            usage,
fa51490b6ae9atagen2026-10-24 08:41 UTC90
        } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC91
            let meta = child(&mut content, "pack");
fa51490b6ae9atagen2026-10-24 08:41 UTC92
            patch(meta, "display_name", display_name, JsonValue::String);
fa51490b6ae9atagen2026-10-24 08:41 UTC93
            patch(meta, "avatar_url", avatar, |uri| uri.to_string().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC94
            patch(meta, "usage", usage, usage_json);
fa51490b6ae9atagen2026-10-24 08:41 UTC95
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC96
        PackEdit::Put {
fa51490b6ae9atagen2026-10-24 08:41 UTC97
            shortcode,
fa51490b6ae9atagen2026-10-24 08:41 UTC98
            mxc,
fa51490b6ae9atagen2026-10-24 08:41 UTC99
            body,
fa51490b6ae9atagen2026-10-24 08:41 UTC100
            usage,
fa51490b6ae9atagen2026-10-24 08:41 UTC101
            info,
fa51490b6ae9atagen2026-10-24 08:41 UTC102
        } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC103
            let images = child(&mut content, "images");
fa51490b6ae9atagen2026-10-24 08:41 UTC104
            let mut image = match images.remove(&shortcode) {
fa51490b6ae9atagen2026-10-24 08:41 UTC105
                Some(JsonValue::Object(image)) => image,
fa51490b6ae9atagen2026-10-24 08:41 UTC106
                _ => JsonObject::new(),
fa51490b6ae9atagen2026-10-24 08:41 UTC107
            };
fa51490b6ae9atagen2026-10-24 08:41 UTC108
            let same_media = image.get("url").and_then(JsonValue::as_str) == Some(mxc.as_str());
fa51490b6ae9atagen2026-10-24 08:41 UTC109
            image.insert("url".to_owned(), mxc.to_string().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC110
            if let Some(body) = body {
fa51490b6ae9atagen2026-10-24 08:41 UTC111
                image.insert("body".to_owned(), body.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC112
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC113
            if let Some(usage) = usage {
fa51490b6ae9atagen2026-10-24 08:41 UTC114
                image.insert("usage".to_owned(), usage_json(usage));
fa51490b6ae9atagen2026-10-24 08:41 UTC115
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC116
            match info {
fa51490b6ae9atagen2026-10-24 08:41 UTC117
                Some(info) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC118
                    image.insert("info".to_owned(), info_json(info));
fa51490b6ae9atagen2026-10-24 08:41 UTC119
                }
fa51490b6ae9atagen2026-10-24 08:41 UTC120
                None if !same_media => {
fa51490b6ae9atagen2026-10-24 08:41 UTC121
                    image.remove("info");
fa51490b6ae9atagen2026-10-24 08:41 UTC122
                }
fa51490b6ae9atagen2026-10-24 08:41 UTC123
                None => {}
fa51490b6ae9atagen2026-10-24 08:41 UTC124
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC125
            images.insert(shortcode, image.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC126
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC127
        PackEdit::Rename { from, to } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC128
            let images = child(&mut content, "images");
fa51490b6ae9atagen2026-10-24 08:41 UTC129
            if images.contains_key(&to) {
fa51490b6ae9atagen2026-10-24 08:41 UTC130
                return Err(Error::new(ErrorKind::Invalid, "that shortcode is taken"));
fa51490b6ae9atagen2026-10-24 08:41 UTC131
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC132
            let image = images
fa51490b6ae9atagen2026-10-24 08:41 UTC133
                .remove(&from)
fa51490b6ae9atagen2026-10-24 08:41 UTC134
                .ok_or_else(|| Error::new(ErrorKind::Invalid, "no image has that shortcode"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC135
            images.insert(to, image);
fa51490b6ae9atagen2026-10-24 08:41 UTC136
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC137
        PackEdit::Remove { shortcode } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC138
            child(&mut content, "images").remove(&shortcode);
fa51490b6ae9atagen2026-10-24 08:41 UTC139
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC140
        PackEdit::Insert { images, create } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC141
            if content.is_empty() {
fa51490b6ae9atagen2026-10-24 08:41 UTC142
                match (create, pack) {
fa51490b6ae9atagen2026-10-24 08:41 UTC143
                    (Some(meta), _) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC144
                        content.insert("pack".to_owned(), meta.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC145
                    }
fa51490b6ae9atagen2026-10-24 08:41 UTC146
                    (None, ImagePackSource::Room { .. }) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC147
                        return Err(Error::new(
fa51490b6ae9atagen2026-10-24 08:41 UTC148
                            ErrorKind::Invalid,
fa51490b6ae9atagen2026-10-24 08:41 UTC149
                            "there's no pack there to copy into",
fa51490b6ae9atagen2026-10-24 08:41 UTC150
                        ));
fa51490b6ae9atagen2026-10-24 08:41 UTC151
                    }
fa51490b6ae9atagen2026-10-24 08:41 UTC152
                    (None, ImagePackSource::User) => {}
fa51490b6ae9atagen2026-10-24 08:41 UTC153
                }
fa51490b6ae9atagen2026-10-24 08:41 UTC154
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC155
            let target = child(&mut content, "images");
fa51490b6ae9atagen2026-10-24 08:41 UTC156
            let shortcodes = images
fa51490b6ae9atagen2026-10-24 08:41 UTC157
                .into_iter()
fa51490b6ae9atagen2026-10-24 08:41 UTC158
                .map(|(shortcode, image)| {
fa51490b6ae9atagen2026-10-24 08:41 UTC159
                    let free = free_shortcode(target, &shortcode);
fa51490b6ae9atagen2026-10-24 08:41 UTC160
                    target.insert(free.clone(), image.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC161
                    free
fa51490b6ae9atagen2026-10-24 08:41 UTC162
                })
fa51490b6ae9atagen2026-10-24 08:41 UTC163
                .collect();
fa51490b6ae9atagen2026-10-24 08:41 UTC164
            output = CommandOutput::Cloned {
fa51490b6ae9atagen2026-10-24 08:41 UTC165
                shortcodes,
fa51490b6ae9atagen2026-10-24 08:41 UTC166
                skipped: Vec::new(),
fa51490b6ae9atagen2026-10-24 08:41 UTC167
            };
fa51490b6ae9atagen2026-10-24 08:41 UTC168
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC169
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC170
    if current.as_ref() != Some(&content) {
fa51490b6ae9atagen2026-10-24 08:41 UTC171
        write_pack(client, pack, content).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC172
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC173
    Ok(output)
fa51490b6ae9atagen2026-10-24 08:41 UTC174
}
fa51490b6ae9atagen2026-10-24 08:41 UTC175
fa51490b6ae9atagen2026-10-24 08:41 UTC176
pub(super) async fn set_enabled_everywhere(
fa51490b6ae9atagen2026-10-24 08:41 UTC177
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC178
    room_id: OwnedRoomId,
fa51490b6ae9atagen2026-10-24 08:41 UTC179
    state_key: String,
fa51490b6ae9atagen2026-10-24 08:41 UTC180
    enabled: bool,
fa51490b6ae9atagen2026-10-24 08:41 UTC181
) -> Result<(), Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC182
    let account = client.account();
fa51490b6ae9atagen2026-10-24 08:41 UTC183
    let current = account
fa51490b6ae9atagen2026-10-24 08:41 UTC184
        .fetch_account_data(GlobalAccountDataEventType::from(ENABLED_ROOMS))
fa51490b6ae9atagen2026-10-24 08:41 UTC185
        .await
fa51490b6ae9atagen2026-10-24 08:41 UTC186
        .map_err(|source| Error::from_sdk("reading enabled packs failed", source))?
fa51490b6ae9atagen2026-10-24 08:41 UTC187
        .map(|raw| parse(raw.json().get()))
fa51490b6ae9atagen2026-10-24 08:41 UTC188
        .transpose()?;
fa51490b6ae9atagen2026-10-24 08:41 UTC189
    let mut content = current.clone().unwrap_or_default();
fa51490b6ae9atagen2026-10-24 08:41 UTC190
    let rooms = child(&mut content, "rooms");
fa51490b6ae9atagen2026-10-24 08:41 UTC191
    if enabled {
fa51490b6ae9atagen2026-10-24 08:41 UTC192
        child(rooms, room_id.as_str()).insert(state_key, JsonObject::new().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC193
    } else {
fa51490b6ae9atagen2026-10-24 08:41 UTC194
        let state_keys = child(rooms, room_id.as_str());
fa51490b6ae9atagen2026-10-24 08:41 UTC195
        state_keys.remove(&state_key);
fa51490b6ae9atagen2026-10-24 08:41 UTC196
        if state_keys.is_empty() {
fa51490b6ae9atagen2026-10-24 08:41 UTC197
            rooms.remove(room_id.as_str());
fa51490b6ae9atagen2026-10-24 08:41 UTC198
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC199
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC200
    if current.as_ref() == Some(&content) {
fa51490b6ae9atagen2026-10-24 08:41 UTC201
        return Ok(());
fa51490b6ae9atagen2026-10-24 08:41 UTC202
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC203
    account
fa51490b6ae9atagen2026-10-24 08:41 UTC204
        .set_account_data_raw(ENABLED_ROOMS.into(), raw(&content))
fa51490b6ae9atagen2026-10-24 08:41 UTC205
        .await
fa51490b6ae9atagen2026-10-24 08:41 UTC206
        .map(drop)
fa51490b6ae9atagen2026-10-24 08:41 UTC207
        .map_err(|source| Error::from_sdk("writing enabled packs failed", source))
fa51490b6ae9atagen2026-10-24 08:41 UTC208
}
fa51490b6ae9atagen2026-10-24 08:41 UTC209
fa51490b6ae9atagen2026-10-24 08:41 UTC210
/// the commands that download, upload or send, so they stay off the per-pack queue and only
fa51490b6ae9atagen2026-10-24 08:41 UTC211
/// take their turn for the final write
fa51490b6ae9atagen2026-10-24 08:41 UTC212
pub(super) async fn run_detached(
fa51490b6ae9atagen2026-10-24 08:41 UTC213
    client: Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC214
    mutations: mpsc::Sender<MutationJob>,
fa51490b6ae9atagen2026-10-24 08:41 UTC215
    max_bytes: usize,
fa51490b6ae9atagen2026-10-24 08:41 UTC216
    command: PackCommand,
fa51490b6ae9atagen2026-10-24 08:41 UTC217
) -> Result<CommandOutput, Error> {
d8e76b56867catagen2026-10-25 09:08 UTC218
    if let PackCommand::CloneImage { usage, .. }
d8e76b56867catagen2026-10-25 09:08 UTC219
    | PackCommand::CloneSticker { usage, .. }
d8e76b56867catagen2026-10-25 09:08 UTC220
    | PackCommand::ClonePack { usage, .. } = &command
d8e76b56867catagen2026-10-25 09:08 UTC221
    {
d8e76b56867catagen2026-10-25 09:08 UTC222
        validate_usage(usage.as_ref())?;
d8e76b56867catagen2026-10-25 09:08 UTC223
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC224
    match command {
fa51490b6ae9atagen2026-10-24 08:41 UTC225
        PackCommand::SendSticker {
fa51490b6ae9atagen2026-10-24 08:41 UTC226
            room_id,
fa51490b6ae9atagen2026-10-24 08:41 UTC227
            pack_id,
fa51490b6ae9atagen2026-10-24 08:41 UTC228
            shortcode,
fa51490b6ae9atagen2026-10-24 08:41 UTC229
        } => send_sticker(&client, room_id, &pack_id, &shortcode)
fa51490b6ae9atagen2026-10-24 08:41 UTC230
            .await
fa51490b6ae9atagen2026-10-24 08:41 UTC231
            .map(|()| CommandOutput::Done),
fa51490b6ae9atagen2026-10-24 08:41 UTC232
        PackCommand::Rehost(mxc) => rehost(&client, mxc, None, max_bytes)
fa51490b6ae9atagen2026-10-24 08:41 UTC233
            .await
fa51490b6ae9atagen2026-10-24 08:41 UTC234
            .map(CommandOutput::Mxc),
fa51490b6ae9atagen2026-10-24 08:41 UTC235
        PackCommand::CloneImage {
fa51490b6ae9atagen2026-10-24 08:41 UTC236
            from_pack,
fa51490b6ae9atagen2026-10-24 08:41 UTC237
            shortcode,
fa51490b6ae9atagen2026-10-24 08:41 UTC238
            to,
2656c1d26d6batagen2026-10-25 05:44 UTC239
            usage,
fa51490b6ae9atagen2026-10-24 08:41 UTC240
        } => {
2656c1d26d6batagen2026-10-25 05:44 UTC241
            let mut image = pack_image(&client, &from_pack, &shortcode).await?;
2656c1d26d6batagen2026-10-25 05:44 UTC242
            apply_usage(&mut image, usage);
fa51490b6ae9atagen2026-10-24 08:41 UTC243
            let image = rehost_image(&client, image, max_bytes).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC244
            insert(&mutations, to, vec![(shortcode, image)], None, Vec::new()).await
fa51490b6ae9atagen2026-10-24 08:41 UTC245
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC246
        PackCommand::CloneSticker {
fa51490b6ae9atagen2026-10-24 08:41 UTC247
            room_id,
fa51490b6ae9atagen2026-10-24 08:41 UTC248
            event_id,
fa51490b6ae9atagen2026-10-24 08:41 UTC249
            to,
fa51490b6ae9atagen2026-10-24 08:41 UTC250
            shortcode,
2656c1d26d6batagen2026-10-25 05:44 UTC251
            usage,
fa51490b6ae9atagen2026-10-24 08:41 UTC252
        } => {
2656c1d26d6batagen2026-10-25 05:44 UTC253
            let mut image = sticker_image(&client, room_id, event_id).await?;
2656c1d26d6batagen2026-10-25 05:44 UTC254
            let body = sticker_body(&image, &shortcode);
2656c1d26d6batagen2026-10-25 05:44 UTC255
            image.insert("body".to_owned(), body.into());
2656c1d26d6batagen2026-10-25 05:44 UTC256
            apply_usage(&mut image, usage);
fa51490b6ae9atagen2026-10-24 08:41 UTC257
            let image = rehost_image(&client, image, max_bytes).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC258
            insert(&mutations, to, vec![(shortcode, image)], None, Vec::new()).await
fa51490b6ae9atagen2026-10-24 08:41 UTC259
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC260
        PackCommand::ClonePack {
fa51490b6ae9atagen2026-10-24 08:41 UTC261
            from_pack,
fa51490b6ae9atagen2026-10-24 08:41 UTC262
            to,
fa51490b6ae9atagen2026-10-24 08:41 UTC263
            display_name,
2656c1d26d6batagen2026-10-25 05:44 UTC264
            usage,
2656c1d26d6batagen2026-10-25 05:44 UTC265
        } => {
2656c1d26d6batagen2026-10-25 05:44 UTC266
            clone_pack(
2656c1d26d6batagen2026-10-25 05:44 UTC267
                &client,
2656c1d26d6batagen2026-10-25 05:44 UTC268
                &mutations,
2656c1d26d6batagen2026-10-25 05:44 UTC269
                max_bytes,
2656c1d26d6batagen2026-10-25 05:44 UTC270
                &from_pack,
2656c1d26d6batagen2026-10-25 05:44 UTC271
                to,
2656c1d26d6batagen2026-10-25 05:44 UTC272
                display_name,
2656c1d26d6batagen2026-10-25 05:44 UTC273
                usage,
2656c1d26d6batagen2026-10-25 05:44 UTC274
            )
2656c1d26d6batagen2026-10-25 05:44 UTC275
            .await
2656c1d26d6batagen2026-10-25 05:44 UTC276
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC277
        _ => Err(Error::new(ErrorKind::Other, "pack command was misrouted")),
fa51490b6ae9atagen2026-10-24 08:41 UTC278
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC279
}
fa51490b6ae9atagen2026-10-24 08:41 UTC280
fa51490b6ae9atagen2026-10-24 08:41 UTC281
async fn clone_pack(
fa51490b6ae9atagen2026-10-24 08:41 UTC282
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC283
    mutations: &mpsc::Sender<MutationJob>,
fa51490b6ae9atagen2026-10-24 08:41 UTC284
    max_bytes: usize,
fa51490b6ae9atagen2026-10-24 08:41 UTC285
    from_pack: &str,
fa51490b6ae9atagen2026-10-24 08:41 UTC286
    to: ImagePackSource,
fa51490b6ae9atagen2026-10-24 08:41 UTC287
    display_name: Option<String>,
2656c1d26d6batagen2026-10-25 05:44 UTC288
    usage: Option<Vec<PackUsage>>,
fa51490b6ae9atagen2026-10-24 08:41 UTC289
) -> Result<CommandOutput, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC290
    let source = resolve_pack_id(client, from_pack)
fa51490b6ae9atagen2026-10-24 08:41 UTC291
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "no pack has that id"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC292
    let content = stored_pack(client, &source)
fa51490b6ae9atagen2026-10-24 08:41 UTC293
        .await?
fa51490b6ae9atagen2026-10-24 08:41 UTC294
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "that pack is empty"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC295
    let mut meta = match content.get("pack") {
fa51490b6ae9atagen2026-10-24 08:41 UTC296
        Some(JsonValue::Object(meta)) => meta.clone(),
fa51490b6ae9atagen2026-10-24 08:41 UTC297
        _ => JsonObject::new(),
fa51490b6ae9atagen2026-10-24 08:41 UTC298
    };
fa51490b6ae9atagen2026-10-24 08:41 UTC299
    let images: Vec<(String, JsonObject)> = match content.get("images") {
fa51490b6ae9atagen2026-10-24 08:41 UTC300
        Some(JsonValue::Object(images)) => images
fa51490b6ae9atagen2026-10-24 08:41 UTC301
            .iter()
fa51490b6ae9atagen2026-10-24 08:41 UTC302
            .filter_map(|(shortcode, image)| {
2656c1d26d6batagen2026-10-25 05:44 UTC303
                let mut image = with_pack_usage(image.as_object()?.clone(), &meta);
2656c1d26d6batagen2026-10-25 05:44 UTC304
                apply_usage(&mut image, usage.clone());
fa51490b6ae9atagen2026-10-24 08:41 UTC305
                Some((shortcode.clone(), image))
fa51490b6ae9atagen2026-10-24 08:41 UTC306
            })
fa51490b6ae9atagen2026-10-24 08:41 UTC307
            .collect(),
fa51490b6ae9atagen2026-10-24 08:41 UTC308
        _ => Vec::new(),
fa51490b6ae9atagen2026-10-24 08:41 UTC309
    };
fa51490b6ae9atagen2026-10-24 08:41 UTC310
    let results: Vec<_> = stream::iter(images)
fa51490b6ae9atagen2026-10-24 08:41 UTC311
        .map(|(shortcode, image)| async move {
fa51490b6ae9atagen2026-10-24 08:41 UTC312
            match rehost_image(client, image, max_bytes).await {
fa51490b6ae9atagen2026-10-24 08:41 UTC313
                Ok(image) => Ok((shortcode, image)),
fa51490b6ae9atagen2026-10-24 08:41 UTC314
                Err(error) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC315
                    tracing::debug!(%error, shortcode, "skipping an image that wouldn't rehost");
fa51490b6ae9atagen2026-10-24 08:41 UTC316
                    Err(shortcode)
fa51490b6ae9atagen2026-10-24 08:41 UTC317
                }
fa51490b6ae9atagen2026-10-24 08:41 UTC318
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC319
        })
fa51490b6ae9atagen2026-10-24 08:41 UTC320
        .buffered(4)
fa51490b6ae9atagen2026-10-24 08:41 UTC321
        .collect()
fa51490b6ae9atagen2026-10-24 08:41 UTC322
        .await;
fa51490b6ae9atagen2026-10-24 08:41 UTC323
    let (copies, skipped): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok);
fa51490b6ae9atagen2026-10-24 08:41 UTC324
    let copies = copies.into_iter().filter_map(Result::ok).collect();
fa51490b6ae9atagen2026-10-24 08:41 UTC325
    let skipped = skipped.into_iter().filter_map(Result::err).collect();
fa51490b6ae9atagen2026-10-24 08:41 UTC326
    let create = match to {
fa51490b6ae9atagen2026-10-24 08:41 UTC327
        ImagePackSource::User => None,
fa51490b6ae9atagen2026-10-24 08:41 UTC328
        ImagePackSource::Room { .. } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC329
            if let Some(display_name) = display_name {
fa51490b6ae9atagen2026-10-24 08:41 UTC330
                meta.insert("display_name".to_owned(), display_name.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC331
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC332
            meta.remove("usage");
fa51490b6ae9atagen2026-10-24 08:41 UTC333
            if let Some(avatar) = meta.remove("avatar_url").and_then(|uri| mxc(&uri))
fa51490b6ae9atagen2026-10-24 08:41 UTC334
                && let Ok(avatar) = rehost(client, avatar, None, max_bytes).await
fa51490b6ae9atagen2026-10-24 08:41 UTC335
            {
fa51490b6ae9atagen2026-10-24 08:41 UTC336
                meta.insert("avatar_url".to_owned(), avatar.to_string().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC337
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC338
            Some(meta)
fa51490b6ae9atagen2026-10-24 08:41 UTC339
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC340
    };
fa51490b6ae9atagen2026-10-24 08:41 UTC341
    insert(mutations, to, copies, create, skipped).await
fa51490b6ae9atagen2026-10-24 08:41 UTC342
}
fa51490b6ae9atagen2026-10-24 08:41 UTC343
fa51490b6ae9atagen2026-10-24 08:41 UTC344
async fn insert(
fa51490b6ae9atagen2026-10-24 08:41 UTC345
    mutations: &mpsc::Sender<MutationJob>,
fa51490b6ae9atagen2026-10-24 08:41 UTC346
    pack: ImagePackSource,
fa51490b6ae9atagen2026-10-24 08:41 UTC347
    images: Vec<(String, JsonObject)>,
fa51490b6ae9atagen2026-10-24 08:41 UTC348
    create: Option<JsonObject>,
fa51490b6ae9atagen2026-10-24 08:41 UTC349
    skipped: Vec<String>,
fa51490b6ae9atagen2026-10-24 08:41 UTC350
) -> Result<CommandOutput, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC351
    let (reply, answer) = Responder::channel();
fa51490b6ae9atagen2026-10-24 08:41 UTC352
    let command = Mutation::Pack {
fa51490b6ae9atagen2026-10-24 08:41 UTC353
        pack,
fa51490b6ae9atagen2026-10-24 08:41 UTC354
        edit: PackEdit::Insert { images, create },
fa51490b6ae9atagen2026-10-24 08:41 UTC355
    };
fa51490b6ae9atagen2026-10-24 08:41 UTC356
    enqueue(mutations, Job { command, reply });
fa51490b6ae9atagen2026-10-24 08:41 UTC357
    match answer.await? {
fa51490b6ae9atagen2026-10-24 08:41 UTC358
        CommandOutput::Cloned { shortcodes, .. } => Ok(CommandOutput::Cloned {
fa51490b6ae9atagen2026-10-24 08:41 UTC359
            shortcodes,
fa51490b6ae9atagen2026-10-24 08:41 UTC360
            skipped,
fa51490b6ae9atagen2026-10-24 08:41 UTC361
        }),
fa51490b6ae9atagen2026-10-24 08:41 UTC362
        output => Ok(output),
fa51490b6ae9atagen2026-10-24 08:41 UTC363
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC364
}
fa51490b6ae9atagen2026-10-24 08:41 UTC365
fa51490b6ae9atagen2026-10-24 08:41 UTC366
async fn send_sticker(
fa51490b6ae9atagen2026-10-24 08:41 UTC367
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC368
    room_id: OwnedRoomId,
fa51490b6ae9atagen2026-10-24 08:41 UTC369
    pack_id: &str,
fa51490b6ae9atagen2026-10-24 08:41 UTC370
    shortcode: &str,
fa51490b6ae9atagen2026-10-24 08:41 UTC371
) -> Result<(), Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC372
    let room = client.get_room(&room_id).ok_or_else(Error::room_unknown)?;
fa51490b6ae9atagen2026-10-24 08:41 UTC373
    let image = pack_image(client, pack_id, shortcode).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC374
    let url = image
fa51490b6ae9atagen2026-10-24 08:41 UTC375
        .get("url")
fa51490b6ae9atagen2026-10-24 08:41 UTC376
        .and_then(mxc)
fa51490b6ae9atagen2026-10-24 08:41 UTC377
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "that image has no usable url"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC378
    let content = json!({
2656c1d26d6batagen2026-10-25 05:44 UTC379
        "body": sticker_body(&image, shortcode),
fa51490b6ae9atagen2026-10-24 08:41 UTC380
        "url": url,
fa51490b6ae9atagen2026-10-24 08:41 UTC381
        "info": image.get("info").cloned().unwrap_or_else(|| json!({})),
fa51490b6ae9atagen2026-10-24 08:41 UTC382
    });
fa51490b6ae9atagen2026-10-24 08:41 UTC383
    room.send_queue()
fa51490b6ae9atagen2026-10-24 08:41 UTC384
        .send_raw(raw(&object(content)), "m.sticker".to_owned())
fa51490b6ae9atagen2026-10-24 08:41 UTC385
        .await
fa51490b6ae9atagen2026-10-24 08:41 UTC386
        .map(drop)
fa51490b6ae9atagen2026-10-24 08:41 UTC387
        .map_err(|source| {
fa51490b6ae9atagen2026-10-24 08:41 UTC388
            Error::with_source(ErrorKind::Store, "queueing the sticker failed", source)
fa51490b6ae9atagen2026-10-24 08:41 UTC389
        })
fa51490b6ae9atagen2026-10-24 08:41 UTC390
}
fa51490b6ae9atagen2026-10-24 08:41 UTC391
fa51490b6ae9atagen2026-10-24 08:41 UTC392
/// the image with its pack's usage folded in, so it means the same once it leaves the pack
fa51490b6ae9atagen2026-10-24 08:41 UTC393
async fn pack_image(client: &Client, pack_id: &str, shortcode: &str) -> Result<JsonObject, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC394
    let source = resolve_pack_id(client, pack_id)
fa51490b6ae9atagen2026-10-24 08:41 UTC395
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "no pack has that id"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC396
    let content = stored_pack(client, &source).await?.unwrap_or_default();
fa51490b6ae9atagen2026-10-24 08:41 UTC397
    let image = content
fa51490b6ae9atagen2026-10-24 08:41 UTC398
        .get("images")
fa51490b6ae9atagen2026-10-24 08:41 UTC399
        .and_then(|images| images.get(shortcode))
fa51490b6ae9atagen2026-10-24 08:41 UTC400
        .and_then(JsonValue::as_object)
fa51490b6ae9atagen2026-10-24 08:41 UTC401
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "no image has that shortcode"))?
fa51490b6ae9atagen2026-10-24 08:41 UTC402
        .clone();
fa51490b6ae9atagen2026-10-24 08:41 UTC403
    let meta = content
fa51490b6ae9atagen2026-10-24 08:41 UTC404
        .get("pack")
fa51490b6ae9atagen2026-10-24 08:41 UTC405
        .and_then(JsonValue::as_object)
fa51490b6ae9atagen2026-10-24 08:41 UTC406
        .cloned()
fa51490b6ae9atagen2026-10-24 08:41 UTC407
        .unwrap_or_default();
fa51490b6ae9atagen2026-10-24 08:41 UTC408
    Ok(with_pack_usage(image, &meta))
fa51490b6ae9atagen2026-10-24 08:41 UTC409
}
fa51490b6ae9atagen2026-10-24 08:41 UTC410
fa51490b6ae9atagen2026-10-24 08:41 UTC411
async fn sticker_image(
fa51490b6ae9atagen2026-10-24 08:41 UTC412
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC413
    room_id: OwnedRoomId,
fa51490b6ae9atagen2026-10-24 08:41 UTC414
    event_id: OwnedEventId,
fa51490b6ae9atagen2026-10-24 08:41 UTC415
) -> Result<JsonObject, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC416
    let room = client.get_room(&room_id).ok_or_else(Error::room_unknown)?;
fa51490b6ae9atagen2026-10-24 08:41 UTC417
    let event = room
fa51490b6ae9atagen2026-10-24 08:41 UTC418
        .load_or_fetch_event(&event_id, None)
fa51490b6ae9atagen2026-10-24 08:41 UTC419
        .await
fa51490b6ae9atagen2026-10-24 08:41 UTC420
        .map_err(|source| Error::from_sdk("loading the sticker failed", source))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC421
    let raw = event.raw();
fa51490b6ae9atagen2026-10-24 08:41 UTC422
    if raw.get_field::<String>("type").ok().flatten().as_deref() != Some("m.sticker") {
fa51490b6ae9atagen2026-10-24 08:41 UTC423
        return Err(Error::new(ErrorKind::Invalid, "that event isn't a sticker"));
fa51490b6ae9atagen2026-10-24 08:41 UTC424
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC425
    let content = raw
fa51490b6ae9atagen2026-10-24 08:41 UTC426
        .get_field::<JsonObject>("content")
fa51490b6ae9atagen2026-10-24 08:41 UTC427
        .ok()
fa51490b6ae9atagen2026-10-24 08:41 UTC428
        .flatten()
fa51490b6ae9atagen2026-10-24 08:41 UTC429
        .unwrap_or_default();
fa51490b6ae9atagen2026-10-24 08:41 UTC430
    let mut image = JsonObject::new();
fa51490b6ae9atagen2026-10-24 08:41 UTC431
    for key in ["url", "body", "info"] {
fa51490b6ae9atagen2026-10-24 08:41 UTC432
        if let Some(value) = content.get(key) {
fa51490b6ae9atagen2026-10-24 08:41 UTC433
            image.insert(key.to_owned(), value.clone());
fa51490b6ae9atagen2026-10-24 08:41 UTC434
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC435
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC436
    image.insert("usage".to_owned(), usage_json(vec![PackUsage::Sticker]));
fa51490b6ae9atagen2026-10-24 08:41 UTC437
    Ok(image)
fa51490b6ae9atagen2026-10-24 08:41 UTC438
}
fa51490b6ae9atagen2026-10-24 08:41 UTC439
fa51490b6ae9atagen2026-10-24 08:41 UTC440
async fn rehost_image(
fa51490b6ae9atagen2026-10-24 08:41 UTC441
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC442
    mut image: JsonObject,
fa51490b6ae9atagen2026-10-24 08:41 UTC443
    max_bytes: usize,
fa51490b6ae9atagen2026-10-24 08:41 UTC444
) -> Result<JsonObject, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC445
    let url = image
fa51490b6ae9atagen2026-10-24 08:41 UTC446
        .get("url")
fa51490b6ae9atagen2026-10-24 08:41 UTC447
        .and_then(mxc)
fa51490b6ae9atagen2026-10-24 08:41 UTC448
        .ok_or_else(|| Error::new(ErrorKind::Invalid, "that image has no usable url"))?;
fa51490b6ae9atagen2026-10-24 08:41 UTC449
    let mimetype = image
fa51490b6ae9atagen2026-10-24 08:41 UTC450
        .get("info")
fa51490b6ae9atagen2026-10-24 08:41 UTC451
        .and_then(|info| info.get("mimetype"))
fa51490b6ae9atagen2026-10-24 08:41 UTC452
        .and_then(JsonValue::as_str)
fa51490b6ae9atagen2026-10-24 08:41 UTC453
        .map(str::to_owned);
fa51490b6ae9atagen2026-10-24 08:41 UTC454
    let url = rehost(client, url, mimetype, max_bytes).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC455
    image.insert("url".to_owned(), url.to_string().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC456
    Ok(image)
fa51490b6ae9atagen2026-10-24 08:41 UTC457
}
fa51490b6ae9atagen2026-10-24 08:41 UTC458
fa51490b6ae9atagen2026-10-24 08:41 UTC459
pub(super) async fn rehost(
fa51490b6ae9atagen2026-10-24 08:41 UTC460
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC461
    mxc: OwnedMxcUri,
fa51490b6ae9atagen2026-10-24 08:41 UTC462
    mimetype: Option<String>,
fa51490b6ae9atagen2026-10-24 08:41 UTC463
    max_bytes: usize,
fa51490b6ae9atagen2026-10-24 08:41 UTC464
) -> Result<OwnedMxcUri, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC465
    let homeserver = client.user_id().map(ruma::UserId::server_name);
fa51490b6ae9atagen2026-10-24 08:41 UTC466
    if mxc
fa51490b6ae9atagen2026-10-24 08:41 UTC467
        .server_name()
fa51490b6ae9atagen2026-10-24 08:41 UTC468
        .ok()
fa51490b6ae9atagen2026-10-24 08:41 UTC469
        .is_some_and(|server| Some(server) == homeserver)
fa51490b6ae9atagen2026-10-24 08:41 UTC470
    {
fa51490b6ae9atagen2026-10-24 08:41 UTC471
        return Ok(mxc);
fa51490b6ae9atagen2026-10-24 08:41 UTC472
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC473
    let media = media::download_file(client, MediaSource::Plain(mxc), mimetype, max_bytes).await?;
fa51490b6ae9atagen2026-10-24 08:41 UTC474
    commands::upload_media(client, &media.mimetype, media.bytes.to_vec(), max_bytes).await
fa51490b6ae9atagen2026-10-24 08:41 UTC475
}
fa51490b6ae9atagen2026-10-24 08:41 UTC476
fa51490b6ae9atagen2026-10-24 08:41 UTC477
/// 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 UTC478
async fn fetch_pack(client: &Client, pack: &ImagePackSource) -> Result<Option<JsonObject>, Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC479
    match pack {
fa51490b6ae9atagen2026-10-24 08:41 UTC480
        ImagePackSource::User => client
fa51490b6ae9atagen2026-10-24 08:41 UTC481
            .account()
fa51490b6ae9atagen2026-10-24 08:41 UTC482
            .fetch_account_data(GlobalAccountDataEventType::from(USER_PACK))
fa51490b6ae9atagen2026-10-24 08:41 UTC483
            .await
fa51490b6ae9atagen2026-10-24 08:41 UTC484
            .map_err(|source| Error::from_sdk("reading the pack failed", source))?
fa51490b6ae9atagen2026-10-24 08:41 UTC485
            .map(|raw| parse(raw.json().get()))
fa51490b6ae9atagen2026-10-24 08:41 UTC486
            .transpose(),
fa51490b6ae9atagen2026-10-24 08:41 UTC487
        ImagePackSource::Room { room_id, state_key } => {
fa51490b6ae9atagen2026-10-24 08:41 UTC488
            let request = get_state_event_for_key::v3::Request::new(
fa51490b6ae9atagen2026-10-24 08:41 UTC489
                room_id.clone(),
fa51490b6ae9atagen2026-10-24 08:41 UTC490
                StateEventType::from(ROOM_PACKS),
fa51490b6ae9atagen2026-10-24 08:41 UTC491
                state_key.clone(),
fa51490b6ae9atagen2026-10-24 08:41 UTC492
            );
fa51490b6ae9atagen2026-10-24 08:41 UTC493
            match client.send(request).await {
fa51490b6ae9atagen2026-10-24 08:41 UTC494
                Ok(response) => parse(response.event_or_content.get()).map(Some),
fa51490b6ae9atagen2026-10-24 08:41 UTC495
                Err(error) if error.client_api_error_kind() == Some(&ApiErrorKind::NotFound) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC496
                    Ok(None)
fa51490b6ae9atagen2026-10-24 08:41 UTC497
                }
fa51490b6ae9atagen2026-10-24 08:41 UTC498
                Err(source) => Err(Error::from_http("reading the pack failed", source)),
fa51490b6ae9atagen2026-10-24 08:41 UTC499
            }
fa51490b6ae9atagen2026-10-24 08:41 UTC500
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC501
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC502
}
fa51490b6ae9atagen2026-10-24 08:41 UTC503
fa51490b6ae9atagen2026-10-24 08:41 UTC504
async fn write_pack(
fa51490b6ae9atagen2026-10-24 08:41 UTC505
    client: &Client,
fa51490b6ae9atagen2026-10-24 08:41 UTC506
    pack: &ImagePackSource,
fa51490b6ae9atagen2026-10-24 08:41 UTC507
    content: JsonObject,
fa51490b6ae9atagen2026-10-24 08:41 UTC508
) -> Result<(), Error> {
fa51490b6ae9atagen2026-10-24 08:41 UTC509
    let written = match pack {
fa51490b6ae9atagen2026-10-24 08:41 UTC510
        ImagePackSource::User => client
fa51490b6ae9atagen2026-10-24 08:41 UTC511
            .account()
fa51490b6ae9atagen2026-10-24 08:41 UTC512
            .set_account_data_raw(USER_PACK.into(), raw(&content))
fa51490b6ae9atagen2026-10-24 08:41 UTC513
            .await
fa51490b6ae9atagen2026-10-24 08:41 UTC514
            .map(drop),
fa51490b6ae9atagen2026-10-24 08:41 UTC515
        ImagePackSource::Room { room_id, state_key } => client
fa51490b6ae9atagen2026-10-24 08:41 UTC516
            .get_room(room_id)
fa51490b6ae9atagen2026-10-24 08:41 UTC517
            .ok_or_else(Error::room_unknown)?
fa51490b6ae9atagen2026-10-24 08:41 UTC518
            .send_state_event_raw(ROOM_PACKS, state_key, JsonValue::Object(content))
fa51490b6ae9atagen2026-10-24 08:41 UTC519
            .await
fa51490b6ae9atagen2026-10-24 08:41 UTC520
            .map(drop),
fa51490b6ae9atagen2026-10-24 08:41 UTC521
    };
fa51490b6ae9atagen2026-10-24 08:41 UTC522
    written.map_err(|source| Error::from_sdk("writing the pack failed", source))
fa51490b6ae9atagen2026-10-24 08:41 UTC523
}
fa51490b6ae9atagen2026-10-24 08:41 UTC524
d8e76b56867catagen2026-10-25 09:08 UTC525
/// values this crate doesn't know ride along untouched, they may mean something to someone
fa51490b6ae9atagen2026-10-24 08:41 UTC526
fn with_pack_usage(mut image: JsonObject, meta: &JsonObject) -> JsonObject {
d8e76b56867catagen2026-10-25 09:08 UTC527
    if known_usage(image.get("usage")).is_some() {
d8e76b56867catagen2026-10-25 09:08 UTC528
        return image;
d8e76b56867catagen2026-10-25 09:08 UTC529
    }
d8e76b56867catagen2026-10-25 09:08 UTC530
    let mut usage = match meta.get("usage") {
d8e76b56867catagen2026-10-25 09:08 UTC531
        Some(JsonValue::Array(values)) if known_usage(meta.get("usage")).is_some() => {
d8e76b56867catagen2026-10-25 09:08 UTC532
            values.clone()
d8e76b56867catagen2026-10-25 09:08 UTC533
        }
d8e76b56867catagen2026-10-25 09:08 UTC534
        _ => usage_values(effective_usage(None, None)),
d8e76b56867catagen2026-10-25 09:08 UTC535
    };
d8e76b56867catagen2026-10-25 09:08 UTC536
    if let Some(JsonValue::Array(own)) = image.get("usage") {
d8e76b56867catagen2026-10-25 09:08 UTC537
        for value in own {
d8e76b56867catagen2026-10-25 09:08 UTC538
            if !usage.contains(value) {
d8e76b56867catagen2026-10-25 09:08 UTC539
                usage.push(value.clone());
d8e76b56867catagen2026-10-25 09:08 UTC540
            }
d8e76b56867catagen2026-10-25 09:08 UTC541
        }
d8e76b56867catagen2026-10-25 09:08 UTC542
    }
d8e76b56867catagen2026-10-25 09:08 UTC543
    image.insert("usage".to_owned(), usage.into());
2656c1d26d6batagen2026-10-25 05:44 UTC544
    image
2656c1d26d6batagen2026-10-25 05:44 UTC545
}
2656c1d26d6batagen2026-10-25 05:44 UTC546
2656c1d26d6batagen2026-10-25 05:44 UTC547
fn known_usage(value: Option<&JsonValue>) -> Option<Vec<PackUsage>> {
2656c1d26d6batagen2026-10-25 05:44 UTC548
    let values = value?.as_array()?;
2656c1d26d6batagen2026-10-25 05:44 UTC549
    let usage = [
2656c1d26d6batagen2026-10-25 05:44 UTC550
        ("emoticon", PackUsage::Emoticon),
2656c1d26d6batagen2026-10-25 05:44 UTC551
        ("sticker", PackUsage::Sticker),
2656c1d26d6batagen2026-10-25 05:44 UTC552
    ]
2656c1d26d6batagen2026-10-25 05:44 UTC553
    .into_iter()
2656c1d26d6batagen2026-10-25 05:44 UTC554
    .filter_map(|(name, usage)| {
2656c1d26d6batagen2026-10-25 05:44 UTC555
        values
2656c1d26d6batagen2026-10-25 05:44 UTC556
            .iter()
2656c1d26d6batagen2026-10-25 05:44 UTC557
            .any(|value| value.as_str() == Some(name))
2656c1d26d6batagen2026-10-25 05:44 UTC558
            .then_some(usage)
2656c1d26d6batagen2026-10-25 05:44 UTC559
    })
2656c1d26d6batagen2026-10-25 05:44 UTC560
    .collect::<Vec<_>>();
2656c1d26d6batagen2026-10-25 05:44 UTC561
    (!usage.is_empty()).then_some(usage)
2656c1d26d6batagen2026-10-25 05:44 UTC562
}
2656c1d26d6batagen2026-10-25 05:44 UTC563
d8e76b56867catagen2026-10-25 09:08 UTC564
/// `Some` of nothing would read back as inheriting, which `None` and `Patch::Clear` already say
d8e76b56867catagen2026-10-25 09:08 UTC565
fn validate_usage(usage: Option<&Vec<PackUsage>>) -> Result<(), Error> {
d8e76b56867catagen2026-10-25 09:08 UTC566
    if usage.is_some_and(Vec::is_empty) {
d8e76b56867catagen2026-10-25 09:08 UTC567
        return Err(Error::new(
d8e76b56867catagen2026-10-25 09:08 UTC568
            ErrorKind::Invalid,
d8e76b56867catagen2026-10-25 09:08 UTC569
            "usage needs at least one value, leave it unset to inherit",
d8e76b56867catagen2026-10-25 09:08 UTC570
        ));
d8e76b56867catagen2026-10-25 09:08 UTC571
    }
d8e76b56867catagen2026-10-25 09:08 UTC572
    Ok(())
d8e76b56867catagen2026-10-25 09:08 UTC573
}
d8e76b56867catagen2026-10-25 09:08 UTC574
2656c1d26d6batagen2026-10-25 05:44 UTC575
fn apply_usage(image: &mut JsonObject, usage: Option<Vec<PackUsage>>) {
2656c1d26d6batagen2026-10-25 05:44 UTC576
    if let Some(usage) = usage {
2656c1d26d6batagen2026-10-25 05:44 UTC577
        image.insert("usage".to_owned(), usage_json(usage));
fa51490b6ae9atagen2026-10-24 08:41 UTC578
    }
2656c1d26d6batagen2026-10-25 05:44 UTC579
}
2656c1d26d6batagen2026-10-25 05:44 UTC580
2656c1d26d6batagen2026-10-25 05:44 UTC581
fn sticker_body(image: &JsonObject, shortcode: &str) -> String {
fa51490b6ae9atagen2026-10-24 08:41 UTC582
    image
2656c1d26d6batagen2026-10-25 05:44 UTC583
        .get("body")
2656c1d26d6batagen2026-10-25 05:44 UTC584
        .and_then(JsonValue::as_str)
2656c1d26d6batagen2026-10-25 05:44 UTC585
        .filter(|body| !body.trim().is_empty())
2656c1d26d6batagen2026-10-25 05:44 UTC586
        .unwrap_or(shortcode)
2656c1d26d6batagen2026-10-25 05:44 UTC587
        .to_owned()
fa51490b6ae9atagen2026-10-24 08:41 UTC588
}
fa51490b6ae9atagen2026-10-24 08:41 UTC589
fa51490b6ae9atagen2026-10-24 08:41 UTC590
fn free_shortcode(images: &JsonObject, wanted: &str) -> String {
fa51490b6ae9atagen2026-10-24 08:41 UTC591
    if !images.contains_key(wanted) {
fa51490b6ae9atagen2026-10-24 08:41 UTC592
        return wanted.to_owned();
fa51490b6ae9atagen2026-10-24 08:41 UTC593
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC594
    (2..)
fa51490b6ae9atagen2026-10-24 08:41 UTC595
        .map(|n| format!("{wanted}-{n}"))
fa51490b6ae9atagen2026-10-24 08:41 UTC596
        .find(|candidate| !images.contains_key(candidate))
fa51490b6ae9atagen2026-10-24 08:41 UTC597
        .expect("an unbounded range finds a free name")
fa51490b6ae9atagen2026-10-24 08:41 UTC598
}
fa51490b6ae9atagen2026-10-24 08:41 UTC599
fa51490b6ae9atagen2026-10-24 08:41 UTC600
/// the object at `key`, made or replaced if it was missing or malformed
fa51490b6ae9atagen2026-10-24 08:41 UTC601
fn child<'a>(parent: &'a mut JsonObject, key: &str) -> &'a mut JsonObject {
fa51490b6ae9atagen2026-10-24 08:41 UTC602
    let slot = parent
fa51490b6ae9atagen2026-10-24 08:41 UTC603
        .entry(key.to_owned())
fa51490b6ae9atagen2026-10-24 08:41 UTC604
        .or_insert_with(|| JsonObject::new().into());
fa51490b6ae9atagen2026-10-24 08:41 UTC605
    if !slot.is_object() {
fa51490b6ae9atagen2026-10-24 08:41 UTC606
        *slot = JsonObject::new().into();
fa51490b6ae9atagen2026-10-24 08:41 UTC607
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC608
    slot.as_object_mut().expect("just made it an object")
fa51490b6ae9atagen2026-10-24 08:41 UTC609
}
fa51490b6ae9atagen2026-10-24 08:41 UTC610
fa51490b6ae9atagen2026-10-24 08:41 UTC611
fn patch<T>(
fa51490b6ae9atagen2026-10-24 08:41 UTC612
    meta: &mut JsonObject,
fa51490b6ae9atagen2026-10-24 08:41 UTC613
    key: &str,
fa51490b6ae9atagen2026-10-24 08:41 UTC614
    change: Patch<T>,
fa51490b6ae9atagen2026-10-24 08:41 UTC615
    to_json: impl FnOnce(T) -> JsonValue,
fa51490b6ae9atagen2026-10-24 08:41 UTC616
) {
fa51490b6ae9atagen2026-10-24 08:41 UTC617
    match change {
fa51490b6ae9atagen2026-10-24 08:41 UTC618
        Patch::Keep => {}
fa51490b6ae9atagen2026-10-24 08:41 UTC619
        Patch::Clear => {
fa51490b6ae9atagen2026-10-24 08:41 UTC620
            meta.remove(key);
fa51490b6ae9atagen2026-10-24 08:41 UTC621
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC622
        Patch::Set(value) => {
fa51490b6ae9atagen2026-10-24 08:41 UTC623
            meta.insert(key.to_owned(), to_json(value));
fa51490b6ae9atagen2026-10-24 08:41 UTC624
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC625
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC626
}
fa51490b6ae9atagen2026-10-24 08:41 UTC627
fa51490b6ae9atagen2026-10-24 08:41 UTC628
fn usage_json(usage: Vec<PackUsage>) -> JsonValue {
d8e76b56867catagen2026-10-25 09:08 UTC629
    usage_values(usage).into()
d8e76b56867catagen2026-10-25 09:08 UTC630
}
d8e76b56867catagen2026-10-25 09:08 UTC631
d8e76b56867catagen2026-10-25 09:08 UTC632
fn usage_values(usage: Vec<PackUsage>) -> Vec<JsonValue> {
fa51490b6ae9atagen2026-10-24 08:41 UTC633
    usage
fa51490b6ae9atagen2026-10-24 08:41 UTC634
        .into_iter()
fa51490b6ae9atagen2026-10-24 08:41 UTC635
        .map(|usage| match usage {
d8e76b56867catagen2026-10-25 09:08 UTC636
            PackUsage::Emoticon => "emoticon".into(),
d8e76b56867catagen2026-10-25 09:08 UTC637
            PackUsage::Sticker => "sticker".into(),
fa51490b6ae9atagen2026-10-24 08:41 UTC638
        })
d8e76b56867catagen2026-10-25 09:08 UTC639
        .collect()
fa51490b6ae9atagen2026-10-24 08:41 UTC640
}
fa51490b6ae9atagen2026-10-24 08:41 UTC641
fa51490b6ae9atagen2026-10-24 08:41 UTC642
fn info_json(info: PackImageInfo) -> JsonValue {
fa51490b6ae9atagen2026-10-24 08:41 UTC643
    let mut out = JsonObject::new();
fa51490b6ae9atagen2026-10-24 08:41 UTC644
    if let Some(mimetype) = info.mimetype {
fa51490b6ae9atagen2026-10-24 08:41 UTC645
        out.insert("mimetype".to_owned(), mimetype.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC646
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC647
    for (key, value) in [
fa51490b6ae9atagen2026-10-24 08:41 UTC648
        ("size", info.size_bytes),
fa51490b6ae9atagen2026-10-24 08:41 UTC649
        ("w", info.width),
fa51490b6ae9atagen2026-10-24 08:41 UTC650
        ("h", info.height),
fa51490b6ae9atagen2026-10-24 08:41 UTC651
    ] {
fa51490b6ae9atagen2026-10-24 08:41 UTC652
        if let Some(value) = value {
fa51490b6ae9atagen2026-10-24 08:41 UTC653
            out.insert(key.to_owned(), value.into());
fa51490b6ae9atagen2026-10-24 08:41 UTC654
        }
fa51490b6ae9atagen2026-10-24 08:41 UTC655
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC656
    out.into()
fa51490b6ae9atagen2026-10-24 08:41 UTC657
}
fa51490b6ae9atagen2026-10-24 08:41 UTC658
fa51490b6ae9atagen2026-10-24 08:41 UTC659
fn mxc(value: &JsonValue) -> Option<OwnedMxcUri> {
fa51490b6ae9atagen2026-10-24 08:41 UTC660
    let uri = OwnedMxcUri::from(value.as_str()?);
fa51490b6ae9atagen2026-10-24 08:41 UTC661
    uri.is_valid().then_some(uri)
fa51490b6ae9atagen2026-10-24 08:41 UTC662
}
fa51490b6ae9atagen2026-10-24 08:41 UTC663
fa51490b6ae9atagen2026-10-24 08:41 UTC664
fn object(value: JsonValue) -> JsonObject {
fa51490b6ae9atagen2026-10-24 08:41 UTC665
    match value {
fa51490b6ae9atagen2026-10-24 08:41 UTC666
        JsonValue::Object(object) => object,
fa51490b6ae9atagen2026-10-24 08:41 UTC667
        _ => JsonObject::new(),
fa51490b6ae9atagen2026-10-24 08:41 UTC668
    }
fa51490b6ae9atagen2026-10-24 08:41 UTC669
}
fa51490b6ae9atagen2026-10-24 08:41 UTC670
d8e76b56867catagen2026-10-25 09:08 UTC671
fn parse(json: &str) -> Result<JsonObject, Error> {
d8e76b56867catagen2026-10-25 09:08 UTC672
    serde_json::from_str(json)
d8e76b56867catagen2026-10-25 09:08 UTC673
        .map_err(|source| Error::with_source(ErrorKind::Server, "the pack is malformed", source))
d8e76b56867catagen2026-10-25 09:08 UTC674
}
d8e76b56867catagen2026-10-25 09:08 UTC675
d8e76b56867catagen2026-10-25 09:08 UTC676
fn raw<T>(content: &JsonObject) -> Raw<T> {
d8e76b56867catagen2026-10-25 09:08 UTC677
    Raw::new(content)
d8e76b56867catagen2026-10-25 09:08 UTC678
        .expect("a json object always serialises")
d8e76b56867catagen2026-10-25 09:08 UTC679
        .cast_unchecked()
d8e76b56867catagen2026-10-25 09:08 UTC680
}
d8e76b56867catagen2026-10-25 09:08 UTC681
2656c1d26d6batagen2026-10-25 05:44 UTC682
#[cfg(test)]
2656c1d26d6batagen2026-10-25 05:44 UTC683
mod tests {
2656c1d26d6batagen2026-10-25 05:44 UTC684
    use super::*;
2656c1d26d6batagen2026-10-25 05:44 UTC685
2656c1d26d6batagen2026-10-25 05:44 UTC686
    #[test]
d8e76b56867catagen2026-10-25 09:08 UTC687
    fn pack_usage_inherits_known_values_then_defaults_and_keeps_unknown_ones() {
2656c1d26d6batagen2026-10-25 05:44 UTC688
        let pack = object(json!({ "usage": ["sticker", "unknown"] }));
2656c1d26d6batagen2026-10-25 05:44 UTC689
        let inherited = with_pack_usage(object(json!({ "usage": [] })), &pack);
d8e76b56867catagen2026-10-25 09:08 UTC690
        assert_eq!(inherited.get("usage"), Some(&json!(["sticker", "unknown"])));
2656c1d26d6batagen2026-10-25 05:44 UTC691
2656c1d26d6batagen2026-10-25 05:44 UTC692
        let pack = object(json!({ "usage": ["emoticon"] }));
2656c1d26d6batagen2026-10-25 05:44 UTC693
        let inherited = with_pack_usage(object(json!({ "usage": ["unknown"] })), &pack);
d8e76b56867catagen2026-10-25 09:08 UTC694
        assert_eq!(
d8e76b56867catagen2026-10-25 09:08 UTC695
            inherited.get("usage"),
d8e76b56867catagen2026-10-25 09:08 UTC696
            Some(&json!(["emoticon", "unknown"]))
d8e76b56867catagen2026-10-25 09:08 UTC697
        );
2656c1d26d6batagen2026-10-25 05:44 UTC698
2656c1d26d6batagen2026-10-25 05:44 UTC699
        let pack = object(json!({ "usage": ["sticker"] }));
2656c1d26d6batagen2026-10-25 05:44 UTC700
        let inherited = with_pack_usage(object(json!({ "usage": ["emoticon", "unknown"] })), &pack);
d8e76b56867catagen2026-10-25 09:08 UTC701
        assert_eq!(
d8e76b56867catagen2026-10-25 09:08 UTC702
            inherited.get("usage"),
d8e76b56867catagen2026-10-25 09:08 UTC703
            Some(&json!(["emoticon", "unknown"]))
d8e76b56867catagen2026-10-25 09:08 UTC704
        );
2656c1d26d6batagen2026-10-25 05:44 UTC705
2656c1d26d6batagen2026-10-25 05:44 UTC706
        let pack = object(json!({ "usage": [] }));
2656c1d26d6batagen2026-10-25 05:44 UTC707
        let inherited = with_pack_usage(object(json!({})), &pack);
2656c1d26d6batagen2026-10-25 05:44 UTC708
        assert_eq!(
2656c1d26d6batagen2026-10-25 05:44 UTC709
            inherited.get("usage"),
2656c1d26d6batagen2026-10-25 05:44 UTC710
            Some(&json!(["emoticon", "sticker"]))
2656c1d26d6batagen2026-10-25 05:44 UTC711
        );
2656c1d26d6batagen2026-10-25 05:44 UTC712
2656c1d26d6batagen2026-10-25 05:44 UTC713
        let pack = object(json!({ "usage": ["unknown"] }));
2656c1d26d6batagen2026-10-25 05:44 UTC714
        let inherited = with_pack_usage(object(json!({})), &pack);
2656c1d26d6batagen2026-10-25 05:44 UTC715
        assert_eq!(
2656c1d26d6batagen2026-10-25 05:44 UTC716
            inherited.get("usage"),
2656c1d26d6batagen2026-10-25 05:44 UTC717
            Some(&json!(["emoticon", "sticker"]))
2656c1d26d6batagen2026-10-25 05:44 UTC718
        );
2656c1d26d6batagen2026-10-25 05:44 UTC719
    }
2656c1d26d6batagen2026-10-25 05:44 UTC720
2656c1d26d6batagen2026-10-25 05:44 UTC721
    #[test]
2656c1d26d6batagen2026-10-25 05:44 UTC722
    fn sticker_body_falls_back_when_missing_or_blank() {
2656c1d26d6batagen2026-10-25 05:44 UTC723
        assert_eq!(sticker_body(&object(json!({})), "wave"), "wave");
2656c1d26d6batagen2026-10-25 05:44 UTC724
        assert_eq!(sticker_body(&object(json!({ "body": "" })), "wave"), "wave");
2656c1d26d6batagen2026-10-25 05:44 UTC725
        assert_eq!(
2656c1d26d6batagen2026-10-25 05:44 UTC726
            sticker_body(&object(json!({ "body": " \t\n " })), "wave"),
2656c1d26d6batagen2026-10-25 05:44 UTC727
            "wave"
2656c1d26d6batagen2026-10-25 05:44 UTC728
        );
2656c1d26d6batagen2026-10-25 05:44 UTC729
        assert_eq!(
2656c1d26d6batagen2026-10-25 05:44 UTC730
            sticker_body(&object(json!({ "body": "  hello  " })), "wave"),
2656c1d26d6batagen2026-10-25 05:44 UTC731
            "  hello  "
2656c1d26d6batagen2026-10-25 05:44 UTC732
        );
2656c1d26d6batagen2026-10-25 05:44 UTC733
    }
2656c1d26d6batagen2026-10-25 05:44 UTC734
}