atagen

macltok

public
Star0
Watch0
9bd34e25bb861f0d2a6f19a39cbceeac12e36f93
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/mutations.rsRust645 lines22.5 KB
1
use std::{
2
    collections::{HashMap, HashSet, VecDeque},
3
    sync::Arc,
4
};
5
6
use matrix_sdk::{
7
    Client, Room,
8
    ruma::{
9
        Int,
10
        api::client::{
11
            profile::{AvatarUrl, DisplayName},
12
            room::{
13
                Visibility as SdkVisibility,
14
                create_room::v3::{Request as CreateRoomRequest, RoomPreset as SdkPreset},
15
            },
16
            state::get_state_event_for_key,
17
        },
18
        events::{
19
            AnyGlobalAccountDataEventContent, GlobalAccountDataEventType, MessageLikeEventType,
20
            StateEventType,
21
            ignored_user_list::{IgnoredUser, IgnoredUserListEventContent},
22
            room::power_levels::RoomPowerLevelsEventContent,
23
        },
24
        serde::{JsonObject, Raw},
25
    },
26
};
27
use ruma::{OwnedRoomId, OwnedUserId};
28
use serde_json::Value as JsonValue;
29
use tokio::sync::mpsc;
30
31
use super::commands::{Job, guarded};
32
use super::executor::TaskSet;
33
use crate::{
34
    CommandOutput, CreateRoomOptions, Error, ErrorKind, PowerLevelChanges, RoomCommand, RoomPreset,
35
    RoomVisibility, normalise::stored_space_order, space_order,
36
};
37
38
/// state changes that must not race each other. same room, same alias, same dm
39
/// partner or same account data type run in order, everything else runs at once
40
pub(super) enum Mutation {
41
    Room(RoomCommand),
42
    GlobalAccountData {
43
        event_type: GlobalAccountDataEventType,
44
        content: Raw<AnyGlobalAccountDataEventContent>,
45
    },
46
    Ignore {
47
        user_id: OwnedUserId,
48
        ignored: bool,
49
    },
50
    ReorderSpaces(Vec<OwnedRoomId>),
51
}
52
53
pub(super) type MutationJob = Job<Mutation>;
54
55
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
56
enum Key {
57
    Room(OwnedRoomId),
58
    Alias(String),
59
    Direct(OwnedUserId),
60
    AccountData(String),
61
    Independent(u64),
62
}
63
64
pub(super) async fn run_scheduler(
65
    client: Client,
66
    mut jobs: mpsc::Receiver<MutationJob>,
67
    queue_limit: usize,
68
) {
69
    let fresh = Fresh::default();
70
    let mut waiting = HashMap::<Key, VecDeque<MutationJob>>::new();
71
    let mut running = HashSet::<Key>::new();
72
    let mut tasks = TaskSet::<Key>::new();
73
    let mut queued = 0_usize;
74
    let mut independent = 0_u64;
75
    let mut receiving = true;
76
    while receiving || !tasks.is_empty() {
77
        tokio::select! {
78
            biased;
79
            Some(finished) = tasks.join_next(), if !tasks.is_empty() => {
80
                // a job's own panic is caught inside it, so this is only ever a key
81
                let Ok(key) = finished else { continue };
82
                let next = waiting.get_mut(&key).and_then(VecDeque::pop_front);
83
                if waiting.get(&key).is_some_and(VecDeque::is_empty) {
84
                    waiting.remove(&key);
85
                }
86
                match next {
87
                    Some(job) => {
88
                        queued -= 1;
89
                        spawn(&mut tasks, &client, &fresh, key, job);
90
                    }
91
                    None => {
92
                        running.remove(&key);
93
                    }
94
                }
95
            }
96
            job = jobs.recv(), if receiving => {
97
                let Some(job) = job else {
98
                    receiving = false;
99
                    continue;
100
                };
101
                let key = key(&job.command, client.user_id().map(ruma::UserId::server_name)).unwrap_or_else(|| {
102
                    independent += 1;
103
                    Key::Independent(independent)
104
                });
105
                if running.insert(key.clone()) {
106
                    spawn(&mut tasks, &client, &fresh, key, job);
107
                } else if queued < queue_limit {
108
                    waiting.entry(key).or_default().push_back(job);
109
                    queued += 1;
110
                } else {
111
                    job.reply.done(Err(Error::new(
112
                        ErrorKind::QueueFull,
113
                        "room command queue is full",
114
                    )));
115
                }
116
            }
117
        }
118
    }
119
}
120
121
/// `server` is ours, since a new alias lives there
122
fn key(mutation: &Mutation, server: Option<&ruma::ServerName>) -> Option<Key> {
123
    let room_id = match mutation {
124
        Mutation::GlobalAccountData { event_type, .. } => {
125
            return Some(Key::AccountData(event_type.to_string()));
126
        }
127
        Mutation::Ignore { .. } => {
128
            return Some(Key::AccountData(
129
                GlobalAccountDataEventType::IgnoredUserList.to_string(),
130
            ));
131
        }
132
        Mutation::ReorderSpaces(_) => {
133
            return Some(Key::AccountData(space_order::EVENT_TYPE.to_owned()));
134
        }
135
        Mutation::Room(RoomCommand::Join { room, .. }) => {
136
            return Some(match OwnedRoomId::try_from(room.clone()) {
137
                Ok(room_id) => Key::Room(room_id),
138
                Err(alias) => Key::Alias(alias.to_string()),
139
            });
140
        }
141
        Mutation::Room(RoomCommand::CreateDirect(user_id)) => {
142
            return Some(Key::Direct(user_id.clone()));
143
        }
144
        Mutation::Room(RoomCommand::Create(options)) => {
145
            return match (&options.alias_localpart, options.invite.as_slice()) {
146
                (Some(localpart), _) => Some(Key::Alias(format!("#{localpart}:{}", server?))),
147
                (None, [user_id]) if options.direct => Some(Key::Direct(user_id.clone())),
148
                _ => None,
149
            };
150
        }
151
        Mutation::Room(
152
            RoomCommand::Leave(room_id)
153
            | RoomCommand::Forget(room_id)
154
            | RoomCommand::AcceptInvite(room_id)
155
            | RoomCommand::RejectInvite(room_id)
156
            | RoomCommand::InviteUser { room_id, .. }
157
            | RoomCommand::KickUser { room_id, .. }
158
            | RoomCommand::BanUser { room_id, .. }
159
            | RoomCommand::UnbanUser { room_id, .. }
160
            | RoomCommand::SetPowerLevels { room_id, .. }
161
            | RoomCommand::SetUserPowerLevel { room_id, .. }
162
            | RoomCommand::MuteUser { room_id, .. }
163
            | RoomCommand::SetFavourite { room_id, .. }
164
            | RoomCommand::SetOwnDisplayName { room_id, .. }
165
            | RoomCommand::SetOwnAvatar { room_id, .. }
166
            | RoomCommand::SetAccountData { room_id, .. },
167
        ) => room_id,
168
        Mutation::Room(_) => return None,
169
    };
170
    Some(Key::Room(room_id.clone()))
171
}
172
173
/// dms made here that the next sync hasn't told the sdk about yet, since `get_dm_room`
174
/// only knows `m.direct` once it comes back round
175
type FreshDms = Arc<std::sync::Mutex<HashMap<OwnedUserId, OwnedRoomId>>>;
176
177
/// space orders written here that sync hasn't echoed into the store yet
178
type FreshOrders = Arc<std::sync::Mutex<HashMap<OwnedRoomId, String>>>;
179
180
#[derive(Clone, Default)]
181
struct Fresh {
182
    dms: FreshDms,
183
    orders: FreshOrders,
184
}
185
186
fn spawn(tasks: &mut TaskSet<Key>, client: &Client, fresh: &Fresh, key: Key, job: MutationJob) {
187
    let client = client.clone();
188
    let fresh = fresh.clone();
189
    tasks.spawn(async move {
190
        let Job { command, reply } = job;
191
        reply.send(guarded("room command", execute(&client, &fresh, command)).await);
192
        key
193
    });
194
}
195
196
async fn execute(
197
    client: &Client,
198
    fresh: &Fresh,
199
    mutation: Mutation,
200
) -> Result<CommandOutput, Error> {
201
    let command = match mutation {
202
        Mutation::GlobalAccountData {
203
            event_type,
204
            content,
205
        } => {
206
            client
207
                .account()
208
                .set_account_data_raw(event_type, content)
209
                .await
210
                .map_err(|source| Error::from_sdk("writing account data failed", source))?;
211
            return Ok(CommandOutput::Done);
212
        }
213
        Mutation::Ignore { user_id, ignored } => {
214
            return set_ignored(client, user_id, ignored).await.map(done);
215
        }
216
        Mutation::ReorderSpaces(order) => {
217
            return reorder_spaces(client, &fresh.orders, order).await.map(done);
218
        }
219
        Mutation::Room(command) => command,
220
    };
221
    let fresh_dms = &fresh.dms;
222
    let room = |room_id: &OwnedRoomId| client.get_room(room_id).ok_or_else(Error::room_unknown);
223
    let joined = |room: Room| CommandOutput::Room(room.room_id().to_owned());
224
    let sdk = |message: &'static str| move |source| Error::from_sdk(message, source);
225
    match command {
226
        RoomCommand::Join { room, via } => client
227
            .join_room_by_id_or_alias(&room, &via)
228
            .await
229
            .map(joined)
230
            .map_err(sdk("joining room failed")),
231
        RoomCommand::Leave(room_id) => room(&room_id)?
232
            .leave()
233
            .await
234
            .map(done)
235
            .map_err(sdk("leaving room failed")),
236
        RoomCommand::Forget(room_id) => room(&room_id)?
237
            .forget()
238
            .await
239
            .map(done)
240
            .map_err(sdk("forgetting room failed")),
241
        RoomCommand::AcceptInvite(room_id) => room(&room_id)?
242
            .join()
243
            .await
244
            .map(done)
245
            .map_err(sdk("accepting invite failed")),
246
        RoomCommand::RejectInvite(room_id) => room(&room_id)?
247
            .leave()
248
            .await
249
            .map(done)
250
            .map_err(sdk("rejecting invite failed")),
251
        RoomCommand::InviteUser { room_id, user_id } => room(&room_id)?
252
            .invite_user_by_id(&user_id)
253
            .await
254
            .map(done)
255
            .map_err(sdk("inviting user failed")),
256
        RoomCommand::KickUser {
257
            room_id,
258
            user_id,
259
            reason,
260
        } => room(&room_id)?
261
            .kick_user(&user_id, reason.as_deref())
262
            .await
263
            .map(done)
264
            .map_err(sdk("kicking user failed")),
265
        RoomCommand::BanUser {
266
            room_id,
267
            user_id,
268
            reason,
269
        } => room(&room_id)?
270
            .ban_user(&user_id, reason.as_deref())
271
            .await
272
            .map(done)
273
            .map_err(sdk("banning user failed")),
274
        RoomCommand::UnbanUser {
275
            room_id,
276
            user_id,
277
            reason,
278
        } => room(&room_id)?
279
            .unban_user(&user_id, reason.as_deref())
280
            .await
281
            .map(done)
282
            .map_err(sdk("unbanning user failed")),
283
        RoomCommand::SetPowerLevels { room_id, changes } => {
284
            apply_power_level_changes(&room(&room_id)?, changes)
285
                .await
286
                .map(done)
287
        }
288
        RoomCommand::SetUserPowerLevel {
289
            room_id,
290
            user_id,
291
            level,
292
        } => room(&room_id)?
293
            .update_power_levels(vec![(&user_id, power_level(level)?)])
294
            .await
295
            .map(done)
296
            .map_err(sdk("updating user power level failed")),
297
        RoomCommand::MuteUser {
298
            room_id,
299
            user_id,
300
            muted,
301
        } => mute_user(client, &room(&room_id)?, &user_id, muted)
302
            .await
303
            .map(done),
304
        RoomCommand::SetFavourite { room_id, favourite } => room(&room_id)?
305
            .set_is_favourite(favourite, None)
306
            .await
307
            .map(done)
308
            .map_err(sdk("setting favourite failed")),
309
        RoomCommand::SetOwnDisplayName { room_id, name } => {
310
            set_own_member_field(client, &room(&room_id)?, ProfileField::DisplayName, name)
311
                .await
312
                .map(done)
313
        }
314
        RoomCommand::SetOwnAvatar { room_id, avatar } => set_own_member_field(
315
            client,
316
            &room(&room_id)?,
317
            ProfileField::Avatar,
318
            avatar.map(String::from),
319
        )
320
        .await
321
        .map(done),
322
        RoomCommand::SetAccountData {
323
            room_id,
324
            event_type,
325
            content,
326
        } => room(&room_id)?
327
            .set_account_data_raw(event_type, content)
328
            .await
329
            .map(done)
330
            .map_err(sdk("writing room account data failed")),
331
        RoomCommand::Create(options) => client
332
            .create_room(create_room_request(options))
333
            .await
334
            .map(joined)
335
            .map_err(sdk("creating room failed")),
336
        RoomCommand::CreateDirect(user_id) => {
337
            if let Some(existing) = client.get_dm_room(&user_id) {
338
                return Ok(joined(existing));
339
            }
340
            let fresh = fresh_dms
341
                .lock()
342
                .expect("nothing panics holding this")
343
                .get(&user_id)
344
                .cloned();
345
            if let Some(room_id) = fresh.filter(|room_id| client.get_room(room_id).is_some()) {
346
                return Ok(CommandOutput::Room(room_id));
347
            }
348
            let room = client
349
                .create_dm(&user_id)
350
                .await
351
                .map_err(sdk("creating direct room failed"))?;
352
            fresh_dms
353
                .lock()
354
                .expect("nothing panics holding this")
355
                .insert(user_id, room.room_id().to_owned());
356
            Ok(joined(room))
357
        }
358
        _ => Err(Error::new(ErrorKind::Other, "room command was misrouted")),
359
    }
360
}
361
362
/// one state event carrying every requested change, since the sdk's own patcher is private
363
async fn apply_power_level_changes(room: &Room, changes: PowerLevelChanges) -> Result<(), Error> {
364
    let mut levels = room
365
        .power_levels()
366
        .await
367
        .map_err(|source| Error::from_sdk("reading power levels failed", source.into()))?;
368
    for (target, value) in [
369
        (&mut levels.ban, changes.ban),
370
        (&mut levels.invite, changes.invite),
371
        (&mut levels.kick, changes.kick),
372
        (&mut levels.redact, changes.redact),
373
        (&mut levels.events_default, changes.events_default),
374
        (&mut levels.state_default, changes.state_default),
375
        (&mut levels.users_default, changes.users_default),
376
        (&mut levels.notifications.room, changes.room_notification),
377
    ] {
378
        if let Some(value) = value {
379
            *target = power_level(value)?;
380
        }
381
    }
382
    for (event_type, value) in [
383
        (StateEventType::RoomName.into(), changes.room_name),
384
        (StateEventType::RoomAvatar.into(), changes.room_avatar),
385
        (StateEventType::RoomTopic.into(), changes.room_topic),
386
        (StateEventType::SpaceChild.into(), changes.space_child),
387
        (MessageLikeEventType::Beacon.into(), changes.beacon),
388
        (StateEventType::BeaconInfo.into(), changes.beacon_info),
389
    ] {
390
        if let Some(value) = value {
391
            levels.events.insert(event_type, power_level(value)?);
392
        }
393
    }
394
    let content = RoomPowerLevelsEventContent::try_from(levels).map_err(|source| {
395
        Error::with_source(
396
            ErrorKind::Invalid,
397
            "normalising power levels failed",
398
            source,
399
        )
400
    })?;
401
    room.send_state_event(content)
402
        .await
403
        .map(drop)
404
        .map_err(|source| Error::from_sdk("updating power levels failed", source))
405
}
406
407
/// reads the list from the server, since the store only catches up on the next sync
408
async fn set_ignored(client: &Client, user_id: OwnedUserId, ignored: bool) -> Result<(), Error> {
409
    if ignored && client.user_id() == Some(&*user_id) {
410
        return Err(Error::new(ErrorKind::Invalid, "you can't ignore yourself"));
411
    }
412
    let account = client.account();
413
    let mut list = account
414
        .fetch_account_data_static::<IgnoredUserListEventContent>()
415
        .await
416
        .map_err(|source| Error::from_sdk("reading the ignore list failed", source))?
417
        .map(|raw| raw.deserialize())
418
        .transpose()
419
        .map_err(|source| {
420
            Error::with_source(ErrorKind::Invalid, "the ignore list is malformed", source)
421
        })?
422
        .unwrap_or_default();
423
    let changed = if ignored {
424
        list.ignored_users
425
            .insert(user_id, IgnoredUser::new())
426
            .is_none()
427
    } else {
428
        list.ignored_users.remove(&user_id).is_some()
429
    };
430
    if !changed {
431
        return Ok(());
432
    }
433
    account
434
        .set_account_data(list)
435
        .await
436
        .map(drop)
437
        .map_err(|source| Error::from_sdk("writing the ignore list failed", source))
438
}
439
440
async fn reorder_spaces(
441
    client: &Client,
442
    fresh: &FreshOrders,
443
    order: Vec<OwnedRoomId>,
444
) -> Result<(), Error> {
445
    if order.iter().collect::<HashSet<_>>().len() != order.len() {
446
        return Err(Error::new(ErrorKind::Invalid, "a space is listed twice"));
447
    }
448
    let mut rooms = HashMap::new();
449
    let mut spaces = Vec::with_capacity(order.len());
450
    for room_id in order {
451
        let room = client.get_room(&room_id).ok_or_else(Error::room_unknown)?;
452
        let stored = stored_space_order(&room).await;
453
        let current = {
454
            let mut fresh = fresh.lock().expect("nothing panics holding this");
455
            match fresh.get(&room_id) {
456
                Some(written) if stored.as_ref() != Some(written) => Some(written.clone()),
457
                Some(_) => fresh.remove(&room_id),
458
                None => stored,
459
            }
460
        };
461
        rooms.insert(room_id.clone(), room);
462
        spaces.push((room_id, current));
463
    }
464
    for (room_id, order) in space_order::plan(&spaces) {
465
        let content = Raw::new(&serde_json::json!({ "order": order }))
466
            .expect("a string map always serialises")
467
            .cast_unchecked();
468
        rooms[&room_id]
469
            .set_account_data_raw(space_order::EVENT_TYPE.into(), content)
470
            .await
471
            .map_err(|source| Error::from_sdk("writing a space order failed", source))?;
472
        fresh
473
            .lock()
474
            .expect("nothing panics holding this")
475
            .insert(room_id, order);
476
    }
477
    Ok(())
478
}
479
480
/// below the cheapest of the levels a message could need, so neither plain nor encrypted
481
/// sends get through
482
async fn mute_user(
483
    client: &Client,
484
    room: &Room,
485
    user_id: &OwnedUserId,
486
    muted: bool,
487
) -> Result<(), Error> {
488
    let own_user_id = client
489
        .user_id()
490
        .ok_or_else(|| Error::new(ErrorKind::Unauthenticated, "the client is not logged in"))?;
491
    let mut levels = room
492
        .power_levels()
493
        .await
494
        .map_err(|source| Error::from_sdk("reading power levels failed", source.into()))?;
495
    if !levels.user_can_change_user_power_level(own_user_id, user_id) {
496
        return Err(Error::new(
497
            ErrorKind::Forbidden,
498
            "you can't change this user's power level",
499
        ));
500
    }
501
    let speaking = [
502
        levels.events_default,
503
        levels.for_message(MessageLikeEventType::RoomMessage),
504
        levels.for_message(MessageLikeEventType::RoomEncrypted),
505
    ]
506
    .into_iter()
507
    .min()
508
    .expect("the list isn't empty");
509
    let current = levels
510
        .users
511
        .get(user_id)
512
        .copied()
513
        .unwrap_or(levels.users_default);
514
    match (muted, current < speaking) {
515
        (true, true) | (false, false) => return Ok(()),
516
        (true, false) => {
517
            let level = speaking.checked_sub(Int::from(1)).ok_or_else(|| {
518
                Error::new(
519
                    ErrorKind::Invalid,
520
                    "there's no power level below this room's lowest",
521
                )
522
            })?;
523
            levels.users.insert(user_id.clone(), level);
524
        }
525
        (false, true) => {
526
            levels.users.remove(user_id);
527
        }
528
    }
529
    let content = RoomPowerLevelsEventContent::try_from(levels).map_err(|source| {
530
        Error::with_source(
531
            ErrorKind::Invalid,
532
            "normalising power levels failed",
533
            source,
534
        )
535
    })?;
536
    room.send_state_event(content)
537
        .await
538
        .map(drop)
539
        .map_err(|source| Error::from_sdk("updating power levels failed", source))
540
}
541
542
#[derive(Clone, Copy)]
543
enum ProfileField {
544
    DisplayName,
545
    Avatar,
546
}
547
548
/// rewrites your member event from the server's copy so membership and unknown keys survive,
549
/// and an earlier change this sync hasn't delivered yet isn't lost
550
async fn set_own_member_field(
551
    client: &Client,
552
    room: &Room,
553
    field: ProfileField,
554
    value: Option<String>,
555
) -> Result<(), Error> {
556
    let own_user_id = client
557
        .user_id()
558
        .ok_or_else(|| Error::new(ErrorKind::Unauthenticated, "the client is not logged in"))?
559
        .to_owned();
560
    let key = match field {
561
        ProfileField::DisplayName => "displayname",
562
        ProfileField::Avatar => "avatar_url",
563
    };
564
    let value = match value {
565
        Some(value) => Some(value),
566
        None => {
567
            let profile = client
568
                .account()
569
                .fetch_user_profile()
570
                .await
571
                .map_err(|source| Error::from_sdk("fetching the profile failed", source))?;
572
            let global = match field {
573
                ProfileField::DisplayName => profile.get_static::<DisplayName>(),
574
                ProfileField::Avatar => profile
575
                    .get_static::<AvatarUrl>()
576
                    .map(|avatar| avatar.map(String::from)),
577
            };
578
            global.map_err(|source| {
579
                Error::with_source(
580
                    ErrorKind::Server,
581
                    "the server sent a malformed profile",
582
                    source,
583
                )
584
            })?
585
        }
586
    };
587
    let request = get_state_event_for_key::v3::Request::new(
588
        room.room_id().to_owned(),
589
        StateEventType::RoomMember,
590
        own_user_id.to_string(),
591
    );
592
    let response = client
593
        .send(request)
594
        .await
595
        .map_err(|source| Error::from_http("reading your member event failed", source))?;
596
    let mut content: JsonObject =
597
        serde_json::from_str(response.event_or_content.get()).map_err(|source| {
598
            Error::with_source(ErrorKind::Invalid, "your member event is malformed", source)
599
        })?;
600
    match value {
601
        Some(value) => content.insert(key.to_owned(), JsonValue::String(value)),
602
        None => content.remove(key),
603
    };
604
    room.send_state_event_raw(
605
        "m.room.member",
606
        own_user_id.as_str(),
607
        JsonValue::Object(content),
608
    )
609
    .await
610
    .map(drop)
611
    .map_err(|source| Error::from_sdk("updating your member event failed", source))
612
}
613
614
fn done<T>(_: T) -> CommandOutput {
615
    CommandOutput::Done
616
}
617
618
fn power_level(value: i64) -> Result<Int, Error> {
619
    value.try_into().map_err(|source| {
620
        Error::with_source(
621
            ErrorKind::Invalid,
622
            "power level is outside the Matrix integer range",
623
            source,
624
        )
625
    })
626
}
627
628
fn create_room_request(options: CreateRoomOptions) -> CreateRoomRequest {
629
    let mut request = CreateRoomRequest::new();
630
    request.name = options.name;
631
    request.topic = options.topic;
632
    request.room_alias_name = options.alias_localpart;
633
    request.invite = options.invite;
634
    request.is_direct = options.direct;
635
    request.visibility = match options.visibility {
636
        RoomVisibility::Private => SdkVisibility::Private,
637
        RoomVisibility::Public => SdkVisibility::Public,
638
    };
639
    request.preset = options.preset.map(|preset| match preset {
640
        RoomPreset::PrivateChat => SdkPreset::PrivateChat,
641
        RoomPreset::TrustedPrivateChat => SdkPreset::TrustedPrivateChat,
642
        RoomPreset::PublicChat => SdkPreset::PublicChat,
643
    });
644
    request
645
}