atagen

macltok

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

Blame: src/normalise/observed.rs

cbb715e11963atagen2026-10-24 23:24 UTC1
use ruma::{
cbb715e11963atagen2026-10-24 23:24 UTC2
    OwnedEventId, UserId,
cbb715e11963atagen2026-10-24 23:24 UTC3
    events::{
cbb715e11963atagen2026-10-24 23:24 UTC4
        poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent},
cbb715e11963atagen2026-10-24 23:24 UTC5
        room::message::{
cbb715e11963atagen2026-10-24 23:24 UTC6
            MessageType, Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
cbb715e11963atagen2026-10-24 23:24 UTC7
        },
cbb715e11963atagen2026-10-24 23:24 UTC8
        sticker::StickerEventContent,
cbb715e11963atagen2026-10-24 23:24 UTC9
    },
cbb715e11963atagen2026-10-24 23:24 UTC10
};
cbb715e11963atagen2026-10-24 23:24 UTC11
use serde_json::Value;
cbb715e11963atagen2026-10-24 23:24 UTC12
cbb715e11963atagen2026-10-24 23:24 UTC13
use super::{
cbb715e11963atagen2026-10-24 23:24 UTC14
    link_preview::sender_previews,
cbb715e11963atagen2026-10-24 23:24 UTC15
    media::{
cbb715e11963atagen2026-10-24 23:24 UTC16
        MediaRegistration, media_descriptor_for_sticker, media_registration,
cbb715e11963atagen2026-10-24 23:24 UTC17
        media_sticker_registration,
cbb715e11963atagen2026-10-24 23:24 UTC18
    },
cbb715e11963atagen2026-10-24 23:24 UTC19
    timeline::formatted_body,
cbb715e11963atagen2026-10-24 23:24 UTC20
};
cbb715e11963atagen2026-10-24 23:24 UTC21
use crate::{
cbb715e11963atagen2026-10-24 23:24 UTC22
    Message, MessageMentions, ReplyDetails, ReplyReference, SendState, Sender, TimelineContent,
cbb715e11963atagen2026-10-24 23:24 UTC23
    TimelineEvent, TimelineItem, TimelineItemId, UtdCause,
cbb715e11963atagen2026-10-24 23:24 UTC24
};
cbb715e11963atagen2026-10-24 23:24 UTC25
cbb715e11963atagen2026-10-24 23:24 UTC26
pub(crate) fn normalise_observed(
cbb715e11963atagen2026-10-24 23:24 UTC27
    value: &Value,
cbb715e11963atagen2026-10-24 23:24 UTC28
    current_user_id: Option<&UserId>,
cbb715e11963atagen2026-10-24 23:24 UTC29
    identity: &str,
cbb715e11963atagen2026-10-24 23:24 UTC30
) -> Option<(TimelineItem, Vec<MediaRegistration>)> {
cbb715e11963atagen2026-10-24 23:24 UTC31
    let event_id: OwnedEventId = serde_json::from_value(value.get("event_id")?.clone()).ok()?;
cbb715e11963atagen2026-10-24 23:24 UTC32
    let sender_id: ruma::OwnedUserId = serde_json::from_value(value.get("sender")?.clone()).ok()?;
cbb715e11963atagen2026-10-24 23:24 UTC33
    let timestamp_ms = value.get("origin_server_ts")?.as_u64()?;
cbb715e11963atagen2026-10-24 23:24 UTC34
    let event_type = value.get("type")?.as_str()?.to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC35
    let own = current_user_id.is_some_and(|user_id| user_id.as_str() == sender_id.as_str());
cbb715e11963atagen2026-10-24 23:24 UTC36
    let redacted = value
cbb715e11963atagen2026-10-24 23:24 UTC37
        .get("unsigned")
cbb715e11963atagen2026-10-24 23:24 UTC38
        .and_then(|unsigned| unsigned.get("redacted_because"))
cbb715e11963atagen2026-10-24 23:24 UTC39
        .is_some();
cbb715e11963atagen2026-10-24 23:24 UTC40
    let mut media = Vec::new();
cbb715e11963atagen2026-10-24 23:24 UTC41
    let (content, reply_to, thread_root) = if redacted {
cbb715e11963atagen2026-10-24 23:24 UTC42
        (TimelineContent::Redacted, None, None)
cbb715e11963atagen2026-10-24 23:24 UTC43
    } else if event_type == "m.room.message" {
cbb715e11963atagen2026-10-24 23:24 UTC44
        match value
cbb715e11963atagen2026-10-24 23:24 UTC45
            .get("content")
cbb715e11963atagen2026-10-24 23:24 UTC46
            .cloned()
cbb715e11963atagen2026-10-24 23:24 UTC47
            .and_then(|content| serde_json::from_value::<RoomMessageEventContent>(content).ok())
cbb715e11963atagen2026-10-24 23:24 UTC48
        {
cbb715e11963atagen2026-10-24 23:24 UTC49
            Some(mut message) => {
cbb715e11963atagen2026-10-24 23:24 UTC50
                let (reply_to, thread_root) = relation_refs(message.relates_to.as_ref());
cbb715e11963atagen2026-10-24 23:24 UTC51
                let relation_replacement = match &message.relates_to {
cbb715e11963atagen2026-10-24 23:24 UTC52
                    Some(Relation::Replacement(replacement)) => {
cbb715e11963atagen2026-10-24 23:24 UTC53
                        Some(replacement.new_content.clone())
cbb715e11963atagen2026-10-24 23:24 UTC54
                    }
cbb715e11963atagen2026-10-24 23:24 UTC55
                    _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC56
                };
cbb715e11963atagen2026-10-24 23:24 UTC57
                let bundled_replacement = value
cbb715e11963atagen2026-10-24 23:24 UTC58
                    .get("unsigned")
cbb715e11963atagen2026-10-24 23:24 UTC59
                    .and_then(|unsigned| unsigned.get("m.relations"))
cbb715e11963atagen2026-10-24 23:24 UTC60
                    .and_then(|relations| relations.get("m.replace"))
cbb715e11963atagen2026-10-24 23:24 UTC61
                    .and_then(|edit| {
cbb715e11963atagen2026-10-24 23:24 UTC62
                        let edit_sender = edit.get("sender")?.as_str()?;
cbb715e11963atagen2026-10-24 23:24 UTC63
                        if edit_sender != sender_id.as_str() {
cbb715e11963atagen2026-10-24 23:24 UTC64
                            return None;
cbb715e11963atagen2026-10-24 23:24 UTC65
                        }
cbb715e11963atagen2026-10-24 23:24 UTC66
                        edit.get("content")?.get("m.new_content").cloned()
cbb715e11963atagen2026-10-24 23:24 UTC67
                    })
cbb715e11963atagen2026-10-24 23:24 UTC68
                    .and_then(|content| {
cbb715e11963atagen2026-10-24 23:24 UTC69
                        serde_json::from_value::<RoomMessageEventContentWithoutRelation>(content)
cbb715e11963atagen2026-10-24 23:24 UTC70
                            .ok()
cbb715e11963atagen2026-10-24 23:24 UTC71
                    });
cbb715e11963atagen2026-10-24 23:24 UTC72
                let edited = relation_replacement.is_some()
cbb715e11963atagen2026-10-24 23:24 UTC73
                    || bundled_replacement.is_some()
cbb715e11963atagen2026-10-24 23:24 UTC74
                    || value
cbb715e11963atagen2026-10-24 23:24 UTC75
                        .get("unsigned")
cbb715e11963atagen2026-10-24 23:24 UTC76
                        .and_then(|unsigned| unsigned.get("macltok_edited"))
cbb715e11963atagen2026-10-24 23:24 UTC77
                        .and_then(Value::as_bool)
cbb715e11963atagen2026-10-24 23:24 UTC78
                        .unwrap_or(false);
cbb715e11963atagen2026-10-24 23:24 UTC79
                if let Some(replacement) = relation_replacement.or(bundled_replacement) {
cbb715e11963atagen2026-10-24 23:24 UTC80
                    message.apply_replacement(replacement);
cbb715e11963atagen2026-10-24 23:24 UTC81
                }
cbb715e11963atagen2026-10-24 23:24 UTC82
                let message_type = &message.msgtype;
cbb715e11963atagen2026-10-24 23:24 UTC83
                media.extend(media_registration(identity, message_type));
cbb715e11963atagen2026-10-24 23:24 UTC84
                let previews = sender_previews(Some(identity), message_type);
cbb715e11963atagen2026-10-24 23:24 UTC85
                media.extend(
cbb715e11963atagen2026-10-24 23:24 UTC86
                    previews
cbb715e11963atagen2026-10-24 23:24 UTC87
                        .iter()
cbb715e11963atagen2026-10-24 23:24 UTC88
                        .filter_map(|(_, registration)| registration.clone()),
cbb715e11963atagen2026-10-24 23:24 UTC89
                );
cbb715e11963atagen2026-10-24 23:24 UTC90
                (
cbb715e11963atagen2026-10-24 23:24 UTC91
                    TimelineContent::Message(Message {
cbb715e11963atagen2026-10-24 23:24 UTC92
                        kind: super::message_kind(message_type),
cbb715e11963atagen2026-10-24 23:24 UTC93
                        body: message.body().to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC94
                        geo_uri: match message_type {
cbb715e11963atagen2026-10-24 23:24 UTC95
                            MessageType::Location(location) => Some(location.geo_uri.clone()),
cbb715e11963atagen2026-10-24 23:24 UTC96
                            _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC97
                        },
cbb715e11963atagen2026-10-24 23:24 UTC98
                        formatted_body: formatted_body(message_type),
cbb715e11963atagen2026-10-24 23:24 UTC99
                        mentions: message.mentions.as_ref().map(|mentions| MessageMentions {
cbb715e11963atagen2026-10-24 23:24 UTC100
                            user_ids: mentions.user_ids.iter().cloned().collect(),
cbb715e11963atagen2026-10-24 23:24 UTC101
                            room: mentions.room,
cbb715e11963atagen2026-10-24 23:24 UTC102
                        }),
cbb715e11963atagen2026-10-24 23:24 UTC103
                        edited,
cbb715e11963atagen2026-10-24 23:24 UTC104
                        media: super::media::media_from_message(identity, message_type),
cbb715e11963atagen2026-10-24 23:24 UTC105
                        link_previews: previews.into_iter().map(|(preview, _)| preview).collect(),
cbb715e11963atagen2026-10-24 23:24 UTC106
                    }),
cbb715e11963atagen2026-10-24 23:24 UTC107
                    reply_to,
cbb715e11963atagen2026-10-24 23:24 UTC108
                    thread_root,
cbb715e11963atagen2026-10-24 23:24 UTC109
                )
cbb715e11963atagen2026-10-24 23:24 UTC110
            }
cbb715e11963atagen2026-10-24 23:24 UTC111
            None => (TimelineContent::Other { event_type }, None, None),
cbb715e11963atagen2026-10-24 23:24 UTC112
        }
cbb715e11963atagen2026-10-24 23:24 UTC113
    } else if event_type == "m.room.encrypted" {
cbb715e11963atagen2026-10-24 23:24 UTC114
        (
cbb715e11963atagen2026-10-24 23:24 UTC115
            TimelineContent::UnableToDecrypt(UtdCause::Unknown),
cbb715e11963atagen2026-10-24 23:24 UTC116
            None,
cbb715e11963atagen2026-10-24 23:24 UTC117
            None,
cbb715e11963atagen2026-10-24 23:24 UTC118
        )
cbb715e11963atagen2026-10-24 23:24 UTC119
    } else if event_type == "m.sticker" {
cbb715e11963atagen2026-10-24 23:24 UTC120
        match value
cbb715e11963atagen2026-10-24 23:24 UTC121
            .get("content")
cbb715e11963atagen2026-10-24 23:24 UTC122
            .cloned()
cbb715e11963atagen2026-10-24 23:24 UTC123
            .and_then(|content| serde_json::from_value::<StickerEventContent>(content).ok())
cbb715e11963atagen2026-10-24 23:24 UTC124
        {
cbb715e11963atagen2026-10-24 23:24 UTC125
            Some(sticker) => {
cbb715e11963atagen2026-10-24 23:24 UTC126
                let (reply_to, thread_root) = relation_refs(sticker.relates_to.as_ref());
cbb715e11963atagen2026-10-24 23:24 UTC127
                media.extend(media_sticker_registration(identity, &sticker));
cbb715e11963atagen2026-10-24 23:24 UTC128
                (
cbb715e11963atagen2026-10-24 23:24 UTC129
                    TimelineContent::Sticker {
cbb715e11963atagen2026-10-24 23:24 UTC130
                        body: sticker.body.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC131
                        media: media_descriptor_for_sticker(identity, &sticker),
cbb715e11963atagen2026-10-24 23:24 UTC132
                    },
cbb715e11963atagen2026-10-24 23:24 UTC133
                    reply_to,
cbb715e11963atagen2026-10-24 23:24 UTC134
                    thread_root,
cbb715e11963atagen2026-10-24 23:24 UTC135
                )
cbb715e11963atagen2026-10-24 23:24 UTC136
            }
cbb715e11963atagen2026-10-24 23:24 UTC137
            None => (TimelineContent::Other { event_type }, None, None),
cbb715e11963atagen2026-10-24 23:24 UTC138
        }
cbb715e11963atagen2026-10-24 23:24 UTC139
    } else if event_type == "m.poll.start" || event_type == "org.matrix.msc3381.poll.start" {
cbb715e11963atagen2026-10-24 23:24 UTC140
        let poll_content = value.get("content");
cbb715e11963atagen2026-10-24 23:24 UTC141
        let question = poll_content.and_then(|content| poll_question(content, &event_type));
cbb715e11963atagen2026-10-24 23:24 UTC142
        match question {
cbb715e11963atagen2026-10-24 23:24 UTC143
            Some(question) => {
cbb715e11963atagen2026-10-24 23:24 UTC144
                let (reply_to, thread_root) =
cbb715e11963atagen2026-10-24 23:24 UTC145
                    poll_content.map(raw_relation_refs).unwrap_or_default();
cbb715e11963atagen2026-10-24 23:24 UTC146
                (TimelineContent::Poll { question }, reply_to, thread_root)
cbb715e11963atagen2026-10-24 23:24 UTC147
            }
cbb715e11963atagen2026-10-24 23:24 UTC148
            None => (TimelineContent::Other { event_type }, None, None),
cbb715e11963atagen2026-10-24 23:24 UTC149
        }
cbb715e11963atagen2026-10-24 23:24 UTC150
    } else if let Some(state_key) = value.get("state_key").and_then(Value::as_str) {
cbb715e11963atagen2026-10-24 23:24 UTC151
        (
cbb715e11963atagen2026-10-24 23:24 UTC152
            TimelineContent::OtherState {
cbb715e11963atagen2026-10-24 23:24 UTC153
                event_type,
cbb715e11963atagen2026-10-24 23:24 UTC154
                state_key: state_key.to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC155
            },
cbb715e11963atagen2026-10-24 23:24 UTC156
            None,
cbb715e11963atagen2026-10-24 23:24 UTC157
            None,
cbb715e11963atagen2026-10-24 23:24 UTC158
        )
cbb715e11963atagen2026-10-24 23:24 UTC159
    } else {
cbb715e11963atagen2026-10-24 23:24 UTC160
        (TimelineContent::Other { event_type }, None, None)
cbb715e11963atagen2026-10-24 23:24 UTC161
    };
cbb715e11963atagen2026-10-24 23:24 UTC162
    let event = TimelineEvent {
cbb715e11963atagen2026-10-24 23:24 UTC163
        item_id: TimelineItemId::new(identity.to_owned()),
cbb715e11963atagen2026-10-24 23:24 UTC164
        event_id: Some(event_id),
cbb715e11963atagen2026-10-24 23:24 UTC165
        transaction_id: None,
cbb715e11963atagen2026-10-24 23:24 UTC166
        sender: Sender {
cbb715e11963atagen2026-10-24 23:24 UTC167
            user_id: sender_id,
cbb715e11963atagen2026-10-24 23:24 UTC168
            display_name: None,
cbb715e11963atagen2026-10-24 23:24 UTC169
            avatar: None,
cbb715e11963atagen2026-10-24 23:24 UTC170
            name_ambiguous: false,
cbb715e11963atagen2026-10-24 23:24 UTC171
        },
cbb715e11963atagen2026-10-24 23:24 UTC172
        timestamp_ms,
cbb715e11963atagen2026-10-24 23:24 UTC173
        own,
cbb715e11963atagen2026-10-24 23:24 UTC174
        send_state: SendState::Sent,
cbb715e11963atagen2026-10-24 23:24 UTC175
        content,
cbb715e11963atagen2026-10-24 23:24 UTC176
        reply_to: reply_to.map(|event_id| ReplyReference {
cbb715e11963atagen2026-10-24 23:24 UTC177
            event_id,
cbb715e11963atagen2026-10-24 23:24 UTC178
            details: ReplyDetails::Pending,
cbb715e11963atagen2026-10-24 23:24 UTC179
        }),
cbb715e11963atagen2026-10-24 23:24 UTC180
        thread_root,
cbb715e11963atagen2026-10-24 23:24 UTC181
        thread_summary: None,
cbb715e11963atagen2026-10-24 23:24 UTC182
        reactions: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC183
        read_by: Vec::new(),
cbb715e11963atagen2026-10-24 23:24 UTC184
        editable: false,
cbb715e11963atagen2026-10-24 23:24 UTC185
    };
cbb715e11963atagen2026-10-24 23:24 UTC186
    Some((TimelineItem::Event(Box::new(event)), media))
cbb715e11963atagen2026-10-24 23:24 UTC187
}
cbb715e11963atagen2026-10-24 23:24 UTC188
cbb715e11963atagen2026-10-24 23:24 UTC189
fn relation_refs<C>(
cbb715e11963atagen2026-10-24 23:24 UTC190
    relation: Option<&Relation<C>>,
cbb715e11963atagen2026-10-24 23:24 UTC191
) -> (Option<OwnedEventId>, Option<OwnedEventId>) {
cbb715e11963atagen2026-10-24 23:24 UTC192
    match relation {
cbb715e11963atagen2026-10-24 23:24 UTC193
        Some(Relation::Reply(reply)) => (Some(reply.in_reply_to.event_id.clone()), None),
cbb715e11963atagen2026-10-24 23:24 UTC194
        Some(Relation::Thread(thread)) => (
cbb715e11963atagen2026-10-24 23:24 UTC195
            thread
cbb715e11963atagen2026-10-24 23:24 UTC196
                .in_reply_to
cbb715e11963atagen2026-10-24 23:24 UTC197
                .as_ref()
cbb715e11963atagen2026-10-24 23:24 UTC198
                .map(|reply| reply.event_id.clone()),
cbb715e11963atagen2026-10-24 23:24 UTC199
            Some(thread.event_id.clone()),
cbb715e11963atagen2026-10-24 23:24 UTC200
        ),
cbb715e11963atagen2026-10-24 23:24 UTC201
        _ => (None, None),
cbb715e11963atagen2026-10-24 23:24 UTC202
    }
cbb715e11963atagen2026-10-24 23:24 UTC203
}
cbb715e11963atagen2026-10-24 23:24 UTC204
cbb715e11963atagen2026-10-24 23:24 UTC205
fn raw_relation_refs(content: &Value) -> (Option<OwnedEventId>, Option<OwnedEventId>) {
cbb715e11963atagen2026-10-24 23:24 UTC206
    let Some(relation) = content.get("m.relates_to") else {
cbb715e11963atagen2026-10-24 23:24 UTC207
        return (None, None);
cbb715e11963atagen2026-10-24 23:24 UTC208
    };
cbb715e11963atagen2026-10-24 23:24 UTC209
    let reply_to = relation
cbb715e11963atagen2026-10-24 23:24 UTC210
        .get("m.in_reply_to")
cbb715e11963atagen2026-10-24 23:24 UTC211
        .and_then(|reply| reply.get("event_id"))
cbb715e11963atagen2026-10-24 23:24 UTC212
        .and_then(Value::as_str)
cbb715e11963atagen2026-10-24 23:24 UTC213
        .and_then(|event_id| event_id.parse().ok());
cbb715e11963atagen2026-10-24 23:24 UTC214
    let thread_root = (relation.get("rel_type").and_then(Value::as_str) == Some("m.thread"))
cbb715e11963atagen2026-10-24 23:24 UTC215
        .then(|| relation.get("event_id").and_then(Value::as_str))
cbb715e11963atagen2026-10-24 23:24 UTC216
        .flatten()
cbb715e11963atagen2026-10-24 23:24 UTC217
        .and_then(|event_id| event_id.parse().ok());
cbb715e11963atagen2026-10-24 23:24 UTC218
    (reply_to, thread_root)
cbb715e11963atagen2026-10-24 23:24 UTC219
}
cbb715e11963atagen2026-10-24 23:24 UTC220
cbb715e11963atagen2026-10-24 23:24 UTC221
fn poll_question(content: &Value, event_type: &str) -> Option<String> {
cbb715e11963atagen2026-10-24 23:24 UTC222
    let typed_question = match event_type {
cbb715e11963atagen2026-10-24 23:24 UTC223
        "m.poll.start" => serde_json::from_value::<PollStartEventContent>(content.clone())
cbb715e11963atagen2026-10-24 23:24 UTC224
            .ok()
cbb715e11963atagen2026-10-24 23:24 UTC225
            .and_then(|poll| poll.poll.question.text.find_plain().map(str::to_owned)),
cbb715e11963atagen2026-10-24 23:24 UTC226
        "org.matrix.msc3381.poll.start" => {
cbb715e11963atagen2026-10-24 23:24 UTC227
            serde_json::from_value::<UnstablePollStartEventContent>(content.clone())
cbb715e11963atagen2026-10-24 23:24 UTC228
                .ok()
cbb715e11963atagen2026-10-24 23:24 UTC229
                .map(|poll| poll.poll_start().question.text.clone())
cbb715e11963atagen2026-10-24 23:24 UTC230
        }
cbb715e11963atagen2026-10-24 23:24 UTC231
        _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC232
    };
cbb715e11963atagen2026-10-24 23:24 UTC233
    typed_question
cbb715e11963atagen2026-10-24 23:24 UTC234
        .or_else(|| {
cbb715e11963atagen2026-10-24 23:24 UTC235
            content
cbb715e11963atagen2026-10-24 23:24 UTC236
                .get("m.poll")
cbb715e11963atagen2026-10-24 23:24 UTC237
                .and_then(|poll| poll.get("question"))
cbb715e11963atagen2026-10-24 23:24 UTC238
                .and_then(|question| question.get("m.text"))
cbb715e11963atagen2026-10-24 23:24 UTC239
                .and_then(generic_text)
cbb715e11963atagen2026-10-24 23:24 UTC240
        })
cbb715e11963atagen2026-10-24 23:24 UTC241
        .or_else(|| {
cbb715e11963atagen2026-10-24 23:24 UTC242
            content
cbb715e11963atagen2026-10-24 23:24 UTC243
                .get("org.matrix.msc3381.poll.start")
cbb715e11963atagen2026-10-24 23:24 UTC244
                .and_then(|poll| poll.get("question"))
cbb715e11963atagen2026-10-24 23:24 UTC245
                .and_then(|question| question.get("org.matrix.msc1767.text"))
cbb715e11963atagen2026-10-24 23:24 UTC246
                .and_then(generic_text)
cbb715e11963atagen2026-10-24 23:24 UTC247
        })
cbb715e11963atagen2026-10-24 23:24 UTC248
        .or_else(|| {
cbb715e11963atagen2026-10-24 23:24 UTC249
            ["org.matrix.msc1767.text", "m.text", "question", "body"]
cbb715e11963atagen2026-10-24 23:24 UTC250
                .iter()
cbb715e11963atagen2026-10-24 23:24 UTC251
                .find_map(|key| content.get(*key).and_then(generic_text))
cbb715e11963atagen2026-10-24 23:24 UTC252
        })
cbb715e11963atagen2026-10-24 23:24 UTC253
}
cbb715e11963atagen2026-10-24 23:24 UTC254
cbb715e11963atagen2026-10-24 23:24 UTC255
fn generic_text(value: &Value) -> Option<String> {
cbb715e11963atagen2026-10-24 23:24 UTC256
    match value {
cbb715e11963atagen2026-10-24 23:24 UTC257
        Value::String(text) => Some(text.clone()),
cbb715e11963atagen2026-10-24 23:24 UTC258
        Value::Array(representations) => representations
cbb715e11963atagen2026-10-24 23:24 UTC259
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC260
            .find(|representation| {
cbb715e11963atagen2026-10-24 23:24 UTC261
                representation.get("mimetype").and_then(Value::as_str) == Some("text/plain")
cbb715e11963atagen2026-10-24 23:24 UTC262
            })
cbb715e11963atagen2026-10-24 23:24 UTC263
            .or_else(|| representations.first())
cbb715e11963atagen2026-10-24 23:24 UTC264
            .and_then(|representation| representation.get("body"))
cbb715e11963atagen2026-10-24 23:24 UTC265
            .and_then(Value::as_str)
cbb715e11963atagen2026-10-24 23:24 UTC266
            .map(str::to_owned),
cbb715e11963atagen2026-10-24 23:24 UTC267
        Value::Object(_) => value.get("body").and_then(Value::as_str).map(str::to_owned),
cbb715e11963atagen2026-10-24 23:24 UTC268
        _ => None,
cbb715e11963atagen2026-10-24 23:24 UTC269
    }
cbb715e11963atagen2026-10-24 23:24 UTC270
}