atagen

macltok

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