atagen

macltok

public
Star0
Watch0
e5c6427adb3514db788582c8ae384f4643c84757
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/normalise/room.rsRust258 lines9.5 KB
1
use futures_util::{StreamExt, stream};
2
use matrix_sdk::{
3
    Client, Room, RoomState,
4
    deserialized_responses::SyncOrStrippedState,
5
    latest_events::LatestEventValue,
6
    ruma::events::{
7
        AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, StateEventType,
8
        SyncStateEvent,
9
        room::message::{Relation, RoomMessageEventContent},
10
        space::child::SpaceChildEventContent,
11
    },
12
};
13
use matrix_sdk_ui::spaces::{SpaceRoom, SpaceService};
14
use ruma::{
15
    OwnedRoomId,
16
    serde::{JsonObject, Raw},
17
};
18
19
use super::message_kind;
20
use crate::{
21
    LatestEvent, MediaId, MessageKind, RoomHero, RoomMembership, RoomNotificationMode, RoomSummary,
22
    SendState, SpaceChild, space_order,
23
};
24
25
pub(crate) async fn room_summaries(client: &Client, spaces: &SpaceService) -> Vec<RoomSummary> {
26
    let mut summaries = stream::iter(client.rooms())
27
        .map(|room| async move { room_summary(&room, spaces).await })
28
        .buffer_unordered(16)
29
        .collect::<Vec<_>>()
30
        .await;
31
    summaries.sort_by(|left, right| {
32
        right
33
            .latest_event_timestamp_ms
34
            .cmp(&left.latest_event_timestamp_ms)
35
            .then_with(|| left.name.to_lowercase().cmp(&right.name.to_lowercase()))
36
            .then_with(|| left.room_id.cmp(&right.room_id))
37
    });
38
    summaries
39
}
40
41
async fn room_summary(room: &Room, spaces: &SpaceService) -> RoomSummary {
42
    let name = room
43
        .display_name()
44
        .await
45
        .map(|name| name.to_string())
46
        .unwrap_or_else(|_| room.room_id().to_string());
47
    let custom_notification_mode = room.user_defined_notification_mode().await;
48
    let effective_notification_mode = match custom_notification_mode {
49
        Some(mode) => Some(mode),
50
        None => room.notification_mode().await,
51
    };
52
    let inviter = match room.state() {
53
        RoomState::Invited => room
54
            .invite_details()
55
            .await
56
            .ok()
57
            .map(|invite| invite.inviter_id),
58
        _ => None,
59
    };
60
    let children = if room.is_space() {
61
        space_children(room).await
62
    } else {
63
        Vec::new()
64
    };
65
    let thread_unreads = super::thread_unreads(room).await;
66
    RoomSummary {
67
        room_id: room.room_id().to_owned(),
68
        name,
69
        membership: membership(room.state()),
70
        unread_messages: room.num_unread_messages(),
71
        unread_mentions: room.num_unread_mentions()
72
            + thread_unreads
73
                .values()
74
                .map(|unread| unread.mentions)
75
                .sum::<u64>(),
76
        unread_notifications: room.num_unread_notifications(),
77
        thread_unreads,
78
        encrypted: room.encryption_state().is_encrypted(),
79
        space: room.is_space(),
80
        direct: room.is_direct().await.unwrap_or(false),
81
        favourite: room.is_favourite(),
82
        low_priority: room.is_low_priority(),
83
        avatar: room.avatar_url().as_deref().map(MediaId::plain),
84
        latest_event_timestamp_ms: room
85
            .latest_event_timestamp()
86
            .map(|value| value.get().into()),
87
        latest_event: latest_event(room),
88
        notification_mode: effective_notification_mode.map(notification_mode),
89
        notification_mode_custom: custom_notification_mode.is_some(),
90
        topic: room.topic(),
91
        canonical_alias: room.canonical_alias(),
92
        joined_members: room.joined_members_count(),
93
        invited_members: room.invited_members_count(),
94
        heroes: room
95
            .heroes()
96
            .into_iter()
97
            .map(|hero| RoomHero {
98
                user_id: hero.user_id,
99
                display_name: hero.display_name,
100
                avatar: hero.avatar_url.as_deref().map(MediaId::plain),
101
            })
102
            .collect(),
103
        successor: room.successor_room().map(|successor| successor.room_id),
104
        inviter,
105
        parent_spaces: spaces
106
            .joined_parents_of_child(room.room_id())
107
            .await
108
            .into_iter()
109
            .map(|parent| parent.room_id)
110
            .collect(),
111
        children,
112
        space_order: if room.is_space() {
113
            stored_space_order(room).await
114
        } else {
115
            None
116
        },
117
        can_edit_packs: super::can_edit_packs(room).await,
118
        pinned_events: room.pinned_event_ids().unwrap_or_default(),
119
        can_pin: room.state() == RoomState::Joined
120
            && room.power_levels().await.is_ok_and(|levels| {
121
                levels.user_can_send_state(room.own_user_id(), StateEventType::RoomPinnedEvents)
122
            }),
123
    }
124
}
125
126
/// None when unset or when another client wrote something msc3230 doesn't allow
127
pub(crate) async fn stored_space_order(room: &Room) -> Option<String> {
128
    room.account_data(space_order::EVENT_TYPE.into())
129
        .await
130
        .ok()??
131
        .get_field::<Raw<JsonObject>>("content")
132
        .ok()??
133
        .get_field::<String>("order")
134
        .ok()?
135
        .filter(|order| space_order::valid(order))
136
}
137
138
const fn membership(state: RoomState) -> RoomMembership {
139
    match state {
140
        RoomState::Joined => RoomMembership::Joined,
141
        RoomState::Invited => RoomMembership::Invited,
142
        RoomState::Left => RoomMembership::Left,
143
        RoomState::Knocked => RoomMembership::Knocked,
144
        RoomState::Banned => RoomMembership::Banned,
145
    }
146
}
147
148
/// what the space itself lists, straight from local state. redacted children don't count
149
async fn space_children(room: &Room) -> Vec<OwnedRoomId> {
150
    let Ok(events) = room
151
        .get_state_events_static::<SpaceChildEventContent>()
152
        .await
153
    else {
154
        return Vec::new();
155
    };
156
    events
157
        .into_iter()
158
        .filter_map(|event| match event.deserialize().ok()? {
159
            SyncOrStrippedState::Sync(SyncStateEvent::Original(event)) => Some(event.state_key),
160
            SyncOrStrippedState::Stripped(event) => Some(event.state_key),
161
            SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_)) => None,
162
        })
163
        .collect()
164
}
165
166
fn latest_event(room: &Room) -> Option<LatestEvent> {
167
    let own = || Some(room.own_user_id().to_owned());
168
    let local = |local: &matrix_sdk::latest_events::LocalLatestEventValue| {
169
        let preview = match local.content.deserialize() {
170
            Ok(AnyMessageLikeEventContent::RoomMessage(content)) => preview(&content),
171
            _ => (None, None),
172
        };
173
        (preview, Some(local.timestamp))
174
    };
175
    let (sender, (kind, body), timestamp, send_state) = match room.latest_event() {
176
        LatestEventValue::None => return None,
177
        LatestEventValue::Remote(event) => {
178
            let preview = match event.raw().deserialize() {
179
                Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
180
                    message,
181
                ))) => message
182
                    .as_original()
183
                    .map_or((None, None), |message| preview(&message.content)),
184
                _ => (None, None),
185
            };
186
            (event.sender(), preview, event.timestamp(), SendState::Sent)
187
        }
188
        LatestEventValue::RemoteInvite {
189
            timestamp, inviter, ..
190
        } => (inviter, (None, None), Some(timestamp), SendState::Sent),
191
        LatestEventValue::LocalIsSending(value) => {
192
            let (preview, timestamp) = local(&value);
193
            (own(), preview, timestamp, SendState::Sending)
194
        }
195
        LatestEventValue::LocalHasBeenSent { value, .. } => {
196
            let (preview, timestamp) = local(&value);
197
            (own(), preview, timestamp, SendState::Sent)
198
        }
199
        // the sdk won't say whether it'll retry, so assume the user has to
200
        LatestEventValue::LocalCannotBeSent(value) => {
201
            let (preview, timestamp) = local(&value);
202
            (
203
                own(),
204
                preview,
205
                timestamp,
206
                SendState::Failed { will_retry: false },
207
            )
208
        }
209
    };
210
    Some(LatestEvent {
211
        sender,
212
        kind,
213
        body,
214
        timestamp_ms: timestamp.map(|timestamp| timestamp.get().into()),
215
        send_state,
216
    })
217
}
218
219
/// the latest event of an edited message is the edit, whose own body is the `* fallback`
220
fn preview(content: &RoomMessageEventContent) -> (Option<MessageKind>, Option<String>) {
221
    let msgtype = match &content.relates_to {
222
        Some(Relation::Replacement(replacement)) => &replacement.new_content.msgtype,
223
        _ => &content.msgtype,
224
    };
225
    (Some(message_kind(msgtype)), Some(msgtype.body().to_owned()))
226
}
227
228
pub(crate) fn space_child(room: SpaceRoom) -> SpaceChild {
229
    SpaceChild {
230
        name: room.display_name,
231
        room_id: room.room_id,
232
        topic: room.topic,
233
        avatar: room.avatar_url.as_deref().map(MediaId::plain),
234
        canonical_alias: room.canonical_alias,
235
        joined_members: room.num_joined_members,
236
        space: room.room_type == Some(ruma::room::RoomType::Space),
237
        children_count: room.children_count,
238
        membership: room.state.map(membership),
239
        via: room.via,
240
        suggested: room.suggested,
241
        world_readable: room.world_readable,
242
        guest_can_join: room.guest_can_join,
243
    }
244
}
245
246
pub(crate) const fn notification_mode(
247
    mode: matrix_sdk::notification_settings::RoomNotificationMode,
248
) -> RoomNotificationMode {
249
    match mode {
250
        matrix_sdk::notification_settings::RoomNotificationMode::AllMessages => {
251
            RoomNotificationMode::AllMessages
252
        }
253
        matrix_sdk::notification_settings::RoomNotificationMode::MentionsAndKeywordsOnly => {
254
            RoomNotificationMode::MentionsAndKeywordsOnly
255
        }
256
        matrix_sdk::notification_settings::RoomNotificationMode::Mute => RoomNotificationMode::Mute,
257
    }
258
}