atagen

macltok

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