atagen

macltok

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