atagen

macltok

public
Star0
Watch0
97fb5720d8812e018aee39923e4eea37fa7dd89b
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/commands.rsRust405 lines12.9 KB
1
use std::{future::Future, panic::AssertUnwindSafe};
2
3
use futures_util::FutureExt;
4
use matrix_sdk::encryption::verification::VerificationRequest;
5
use matrix_sdk::{
6
    Client,
7
    notification_settings::{IsEncrypted, IsOneToOne, RoomNotificationMode as SdkMode},
8
    ruma::{
9
        OwnedDeviceId, OwnedEventId, OwnedMxcUri, RoomId, TransactionId,
10
        api::client::{
11
            push::{get_pushrule, set_pushrule_actions},
12
            receipt::create_receipt::v3::ReceiptType,
13
        },
14
        events::receipt::ReceiptThread,
15
        push::{Action, HighlightTweakValue, RuleKind, Tweak},
16
    },
17
    send_queue::{LocalEchoContent, SendHandle},
18
};
19
use tokio::sync::mpsc;
20
21
use crate::{
22
    EncryptionCommand, Error, ErrorKind, InteractiveAuth, InteractiveAuthChallenge,
23
    NotificationCommand, RoomNotificationMode, TimelineId,
24
    encryption::VerificationController,
25
    transport::{EventSender, Responder},
26
};
27
28
pub(super) struct Job<T> {
29
    pub(super) command: T,
30
    pub(super) reply: Responder,
31
}
32
33
/// a panicking command answers with an error instead of taking its worker down with it
34
pub(super) async fn guarded<T>(
35
    label: &'static str,
36
    work: impl Future<Output = Result<T, Error>>,
37
) -> Result<T, Error> {
38
    AssertUnwindSafe(work)
39
        .catch_unwind()
40
        .await
41
        .unwrap_or_else(|_| Err(Error::new(ErrorKind::Other, format!("{label} panicked"))))
42
}
43
44
pub(super) async fn run_notifications(
45
    client: Client,
46
    mut jobs: mpsc::Receiver<Job<NotificationCommand>>,
47
) {
48
    while let Some(Job { command, reply }) = jobs.recv().await {
49
        reply.done(guarded("notification command", notification(&client, command)).await);
50
    }
51
}
52
53
pub(super) async fn run_encryption(
54
    client: Client,
55
    events: EventSender,
56
    mut jobs: mpsc::Receiver<Job<EncryptionCommand>>,
57
    mut incoming: mpsc::UnboundedReceiver<VerificationRequest>,
58
) {
59
    let mut verification = VerificationController::default();
60
    loop {
61
        tokio::select! {
62
            job = jobs.recv() => {
63
                let Some(Job { command, reply }) = job else { return };
64
                let work = encryption(&client, &events, &mut verification, command);
65
                reply.done(guarded("encryption command", work).await);
66
            }
67
            Some(request) = incoming.recv() => {
68
                verification.adopt_incoming(events.clone(), request).await;
69
            }
70
        }
71
    }
72
}
73
74
async fn encryption(
75
    client: &Client,
76
    events: &EventSender,
77
    verification: &mut VerificationController,
78
    command: EncryptionCommand,
79
) -> Result<(), Error> {
80
    let encryption = client.encryption();
81
    match command {
82
        EncryptionCommand::Recover(key) => encryption
83
            .recovery()
84
            .recover(&key)
85
            .await
86
            .map_err(|source| Error::from_recovery("account recovery failed", source)),
87
        EncryptionCommand::RecoverKeyBackup(key) => encryption
88
            .recovery()
89
            .recover_and_fix_backup(&key)
90
            .await
91
            .map_err(|source| Error::from_recovery("recovering key backup failed", source)),
92
        EncryptionCommand::CreateKeyBackup => encryption
93
            .backups()
94
            .create()
95
            .await
96
            .map_err(|source| Error::from_sdk("creating key backup failed", source)),
97
        EncryptionCommand::EnableKeyBackup => encryption
98
            .recovery()
99
            .enable_backup()
100
            .await
101
            .map_err(|source| Error::from_recovery("enabling key backup failed", source)),
102
        EncryptionCommand::DisableKeyBackup { delete_from_server } => {
103
            let backups = encryption.backups();
104
            if delete_from_server {
105
                backups.disable_and_delete().await
106
            } else {
107
                backups.disable().await
108
            }
109
            .map_err(|source| Error::from_sdk("disabling key backup failed", source))
110
        }
111
        EncryptionCommand::StartVerification(target) => {
112
            verification.start(client, events.clone(), target).await
113
        }
114
        EncryptionCommand::Verify(action) => verification.apply(events.clone(), action).await,
115
    }
116
}
117
118
async fn notification(client: &Client, command: NotificationCommand) -> Result<(), Error> {
119
    let settings = client.notification_settings().await;
120
    match command {
121
        NotificationCommand::SetRoomMode { room_id, mode } => {
122
            settings
123
                .set_room_notification_mode(&room_id, sdk_mode(mode))
124
                .await
125
        }
126
        NotificationCommand::ResetRoomMode(room_id) => {
127
            settings.delete_user_defined_room_rules(&room_id).await
128
        }
129
        NotificationCommand::SetDefaultMode {
130
            encrypted,
131
            one_to_one,
132
            mode,
133
        } => {
134
            settings
135
                .set_default_room_notification_mode(
136
                    IsEncrypted::from(encrypted),
137
                    IsOneToOne::from(one_to_one),
138
                    sdk_mode(mode),
139
                )
140
                .await
141
        }
142
        NotificationCommand::AddKeyword(keyword) => {
143
            match settings.add_keyword(keyword.clone()).await {
144
                Ok(()) => return highlight_keyword(client, &keyword).await,
145
                Err(error) => Err(error),
146
            }
147
        }
148
        NotificationCommand::RemoveKeyword(keyword) => settings.remove_keyword(&keyword).await,
149
    }
150
    .map_err(|source| {
151
        Error::with_source(
152
            ErrorKind::Server,
153
            "updating notification settings failed",
154
            source,
155
        )
156
    })
157
}
158
159
/// sdk keyword rules carry only notify and a sound, while Element's loud keywords also
160
/// highlight, which is what lets the inbox keep them as mentions. The sdk names a
161
/// keyword's content rule after the keyword itself, a rule under another id is left
162
/// alone.
163
async fn highlight_keyword(client: &Client, keyword: &str) -> Result<(), Error> {
164
    let failed = |source| Error::from_http("highlighting a notification keyword failed", source);
165
    let rule = match client
166
        .send(get_pushrule::v3::Request::new(
167
            RuleKind::Content,
168
            keyword.to_owned(),
169
        ))
170
        .await
171
    {
172
        Ok(response) => response.rule,
173
        Err(error)
174
            if error
175
                .as_client_api_error()
176
                .is_some_and(|error| error.status_code.as_u16() == 404) =>
177
        {
178
            return Ok(());
179
        }
180
        Err(error) => return Err(failed(error)),
181
    };
182
    if rule.actions.iter().any(Action::is_highlight) {
183
        return Ok(());
184
    }
185
    let mut actions: Vec<_> = rule
186
        .actions
187
        .into_iter()
188
        .filter(|action| !matches!(action, Action::SetTweak(Tweak::Highlight(_))))
189
        .collect();
190
    actions.push(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)));
191
    client
192
        .send(set_pushrule_actions::v3::Request::new(
193
            RuleKind::Content,
194
            keyword.to_owned(),
195
            actions,
196
        ))
197
        .await
198
        .map_err(failed)?;
199
    Ok(())
200
}
201
202
const fn sdk_mode(mode: RoomNotificationMode) -> SdkMode {
203
    match mode {
204
        RoomNotificationMode::AllMessages => SdkMode::AllMessages,
205
        RoomNotificationMode::MentionsAndKeywordsOnly => SdkMode::MentionsAndKeywordsOnly,
206
        RoomNotificationMode::Mute => SdkMode::Mute,
207
    }
208
}
209
210
fn room(client: &Client, room_id: &RoomId) -> Result<matrix_sdk::Room, Error> {
211
    client.get_room(room_id).ok_or_else(Error::room_unknown)
212
}
213
214
/// the closed-room path. without a timeline nothing stops the marker going backwards
215
pub(super) async fn mark_read(
216
    client: &Client,
217
    timeline_id: &TimelineId,
218
    event_id: OwnedEventId,
219
) -> Result<(), Error> {
220
    let room = room(client, timeline_id.room_id())?;
221
    let receipt_failed = |source| Error::from_sdk("sending read receipt failed", source);
222
    let thread = match timeline_id.thread_root() {
223
        None => ReceiptThread::Main,
224
        Some(root) => ReceiptThread::Thread(root.clone()),
225
    };
226
    room.send_single_receipt(ReceiptType::Read, thread, event_id.clone())
227
        .await
228
        .map_err(receipt_failed)?;
229
    if timeline_id.thread_root().is_none() {
230
        room.send_single_receipt(ReceiptType::FullyRead, ReceiptThread::Unthreaded, event_id)
231
            .await
232
            .map_err(receipt_failed)?;
233
    }
234
    Ok(())
235
}
236
237
pub(super) async fn set_typing(
238
    client: &Client,
239
    room_id: &RoomId,
240
    typing: bool,
241
) -> Result<(), Error> {
242
    room(client, room_id)?
243
        .typing_notice(typing)
244
        .await
245
        .map_err(|source| Error::from_sdk("typing update failed", source))
246
}
247
248
async fn send_handle(
249
    client: &Client,
250
    room_id: &RoomId,
251
    transaction_id: &TransactionId,
252
) -> Result<SendHandle, Error> {
253
    let (echoes, _) = room(client, room_id)?
254
        .send_queue()
255
        .subscribe()
256
        .await
257
        .map_err(|source| {
258
            Error::with_source(ErrorKind::Store, "reading send queue failed", source)
259
        })?;
260
    echoes
261
        .into_iter()
262
        .find_map(|echo| match echo.content {
263
            LocalEchoContent::Event { send_handle, .. }
264
                if echo.transaction_id.as_str() == transaction_id.as_str() =>
265
            {
266
                Some(send_handle)
267
            }
268
            _ => None,
269
        })
270
        .ok_or_else(|| {
271
            Error::new(
272
                ErrorKind::Invalid,
273
                "send is no longer pending or is not a message",
274
            )
275
        })
276
}
277
278
pub(super) async fn cancel_send(
279
    client: &Client,
280
    room_id: &RoomId,
281
    transaction_id: &TransactionId,
282
) -> Result<(), Error> {
283
    match send_handle(client, room_id, transaction_id)
284
        .await?
285
        .abort()
286
        .await
287
    {
288
        Ok(true) => Ok(()),
289
        Ok(false) => Err(Error::new(
290
            ErrorKind::Invalid,
291
            "send could not be cancelled because it has already been sent",
292
        )),
293
        Err(source) => Err(Error::with_source(
294
            ErrorKind::Store,
295
            "cancelling send failed",
296
            source,
297
        )),
298
    }
299
}
300
301
pub(super) async fn retry_send(
302
    client: &Client,
303
    room_id: &RoomId,
304
    transaction_id: &TransactionId,
305
) -> Result<(), Error> {
306
    send_handle(client, room_id, transaction_id)
307
        .await?
308
        .unwedge()
309
        .await
310
        .map_err(|source| Error::with_source(ErrorKind::Store, "retrying send failed", source))?;
311
    room(client, room_id)?.send_queue().set_enabled(true);
312
    Ok(())
313
}
314
315
pub(super) async fn upload_avatar(
316
    client: &Client,
317
    mimetype: &str,
318
    bytes: Vec<u8>,
319
    max_bytes: usize,
320
) -> Result<(), Error> {
321
    if bytes.len() > max_bytes {
322
        return Err(Error::new(
323
            ErrorKind::MediaTooLarge,
324
            "avatar exceeds the configured byte limit",
325
        ));
326
    }
327
    let mimetype: mime::Mime = mimetype
328
        .parse()
329
        .map_err(|_| Error::new(ErrorKind::Invalid, "avatar MIME type is invalid"))?;
330
    if mimetype.type_() != mime::IMAGE {
331
        return Err(Error::new(
332
            ErrorKind::Invalid,
333
            "an avatar has to be an image",
334
        ));
335
    }
336
    client
337
        .account()
338
        .upload_avatar(&mimetype, bytes)
339
        .await
340
        .map(drop)
341
        .map_err(|source| Error::from_sdk("uploading avatar failed", source))
342
}
343
344
pub(super) async fn upload_media(
345
    client: &Client,
346
    mimetype: &str,
347
    bytes: Vec<u8>,
348
    max_bytes: usize,
349
) -> Result<OwnedMxcUri, Error> {
350
    if bytes.len() > max_bytes {
351
        return Err(Error::new(
352
            ErrorKind::MediaTooLarge,
353
            "media exceeds the configured byte limit",
354
        ));
355
    }
356
    let mimetype: mime::Mime = mimetype
357
        .parse()
358
        .map_err(|_| Error::new(ErrorKind::Invalid, "media MIME type is invalid"))?;
359
    client
360
        .media()
361
        .upload(&mimetype, bytes, None)
362
        .await
363
        .map(|response| response.content_uri)
364
        .map_err(|source| Error::from_sdk("uploading media failed", source))
365
}
366
367
pub(super) async fn rename_device(
368
    client: &Client,
369
    device_id: &OwnedDeviceId,
370
    display_name: &str,
371
) -> Result<(), Error> {
372
    client
373
        .rename_device(device_id, display_name)
374
        .await
375
        .map(drop)
376
        .map_err(|source| Error::from_http("renaming device failed", source))
377
}
378
379
pub(super) async fn delete_devices(
380
    client: &Client,
381
    device_ids: &[OwnedDeviceId],
382
    auth: Option<InteractiveAuth>,
383
) -> Result<(), Error> {
384
    let user_id = client
385
        .user_id()
386
        .ok_or_else(|| Error::new(ErrorKind::Unauthenticated, "the client is not logged in"))?;
387
    let auth = auth
388
        .map(|auth| auth.into_sdk(user_id))
389
        .transpose()
390
        .map_err(|message| {
391
            Error::new(
392
                ErrorKind::Invalid,
393
                format!("interactive authentication data is invalid, {message}"),
394
            )
395
        })?;
396
    match client.delete_devices(device_ids, auth).await {
397
        Ok(_) => Ok(()),
398
        Err(source) => match source.as_uiaa_response() {
399
            Some(info) => Err(Error::interactive_auth(InteractiveAuthChallenge::from(
400
                info,
401
            ))),
402
            None => Err(Error::from_http("deleting devices failed", source)),
403
        },
404
    }
405
}