atagen

macltok

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