atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git
labs/multiverse/src/main.rsRust992 lines35.8 KB
1
#![allow(clippy::large_enum_variant)]
2
3
use std::{
4
    collections::{HashMap, HashSet},
5
    io::{self, Write, stdout},
6
    path::{Path, PathBuf},
7
    sync::Arc,
8
    time::{Duration, Instant},
9
};
10
11
use clap::Parser;
12
use color_eyre::Result;
13
use crossterm::{
14
    event::{
15
        self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers,
16
    },
17
    execute,
18
};
19
use futures_util::{StreamExt as _, pin_mut};
20
use imbl::Vector;
21
use layout::Flex;
22
use matrix_sdk::{
23
    AuthSession, Client, SqliteCryptoStore, SqliteEventCacheStore, SqliteStateStore,
24
    ThreadingSupport,
25
    authentication::matrix::MatrixSession,
26
    config::StoreConfig,
27
    encryption::{BackupDownloadStrategy, EncryptionSettings},
28
    reqwest::Url,
29
    ruma::{OwnedRoomId, api::client::room::create_room::v3::Request as CreateRoomRequest},
30
    search_index::{SearchIndexGuard, SearchIndexStoreKind},
31
};
32
use matrix_sdk_base::{RoomStateFilter, event_cache::store::EventCacheStoreLockGuard};
33
use matrix_sdk_common::{cross_process_lock::CrossProcessLockConfig, locks::Mutex};
34
use matrix_sdk_ui::{
35
    Timeline as SdkTimeline,
36
    room_list_service::{self, State, filters::new_filter_non_left},
37
    sync_service::SyncService,
38
    timeline::{RoomExt as _, TimelineFocus, TimelineItem},
39
};
40
use ratatui::{DefaultTerminal, prelude::*, style::palette::tailwind, widgets::*};
41
use throbber_widgets_tui::{Throbber, ThrobberState};
42
use tokio::{
43
    spawn,
44
    sync::mpsc::{Receiver, Sender, channel, error::TryRecvError},
45
    task::JoinHandle,
46
    time::timeout,
47
};
48
use tracing::{debug, error, warn};
49
use tracing_subscriber::EnvFilter;
50
use widgets::{
51
    recovery::create_centered_throbber_area, room_view::RoomView, settings::SettingsView,
52
};
53
54
use crate::widgets::{
55
    create_room::CreateRoomView,
56
    help::HelpView,
57
    room_list::{ExtraRoomInfo, RoomInfos, RoomList, Rooms},
58
    search::{
59
        indexing::{IndexingMessage, IndexingView},
60
        searching::SearchingView,
61
    },
62
    status::Status,
63
};
64
65
mod widgets;
66
67
const HEADER_BG: Color = tailwind::BLUE.c950;
68
const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
69
const ALT_ROW_COLOR: Color = tailwind::SLATE.c900;
70
const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300;
71
const TEXT_COLOR: Color = tailwind::SLATE.c200;
72
73
type Timelines = Arc<Mutex<HashMap<OwnedRoomId, Timeline>>>;
74
75
#[derive(Debug, Parser)]
76
struct Cli {
77
    /// The homeserver the client should connect to.
78
    server_name: String,
79
80
    /// The path where session specific data should be stored.
81
    #[clap(default_value = "/tmp/")]
82
    session_path: PathBuf,
83
84
    /// Set the proxy that should be used for the connection.
85
    #[clap(short, long, env = "PROXY")]
86
    proxy: Option<Url>,
87
88
    /// Whether to *not* reload the `pos`ition sliding sync token from disk at
89
    /// start or not, for the room list sliding sync.
90
    ///
91
    /// Set to false by default (i.e. reload the position from disk).
92
    #[clap(short, long, default_value_t = false)]
93
    dont_share_pos: bool,
94
}
95
96
#[derive(Default)]
97
pub enum GlobalMode {
98
    /// The default mode, no popout screen is opened.
99
    #[default]
100
    Default,
101
    /// Mode where we have opened the help screen.
102
    Help,
103
    /// Mode where we have opened the settings screen.
104
    Settings { view: SettingsView },
105
    /// Mode where we are shutting our tasks down and exiting multiverse.
106
    Exiting { shutdown_task: JoinHandle<()> },
107
    /// Mode where we have opened the create room screen
108
    CreateRoom { view: CreateRoomView },
109
    /// Mode where we have opened the search screen
110
    Searching { view: SearchingView, is_global: bool },
111
    /// Mode where we have opened the indexing screen
112
    Indexing { view: IndexingView },
113
}
114
115
/// Helper function to create a centered rect using up certain percentage of the
116
/// available rect `r`
117
fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
118
    let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
119
    let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
120
    let [area] = vertical.areas(area);
121
    let [area] = horizontal.areas(area);
122
    area
123
}
124
125
#[tokio::main]
126
async fn main() -> Result<()> {
127
    let cli = Cli::parse();
128
    let file_writer = tracing_appender::rolling::hourly(&cli.session_path, "logs-");
129
130
    tracing_subscriber::fmt()
131
        .with_env_filter(EnvFilter::from_default_env())
132
        .with_ansi(false)
133
        .with_writer(file_writer)
134
        .init();
135
136
    color_eyre::install()?;
137
138
    let share_pos = !cli.dont_share_pos;
139
    let client = configure_client(cli).await?;
140
141
    // Watch for errors in background jobs.
142
    spawn({
143
        let client = client.clone();
144
        async move {
145
            let mut recv = client.task_monitor().subscribe();
146
            while let Ok(report) = recv.recv().await {
147
                error!(name = report.task.name, ?report.reason, "A background task has crashed!");
148
            }
149
        }
150
    });
151
152
    let event_cache = client.event_cache();
153
    event_cache.config_mut().experimental_auto_backpagination = true;
154
    event_cache.subscribe()?;
155
156
    let terminal = ratatui::init();
157
    execute!(stdout(), EnableMouseCapture)?;
158
    let mut app = App::new(client, share_pos).await?;
159
160
    app.run(terminal).await
161
}
162
163
pub struct Timeline {
164
    timeline: Arc<SdkTimeline>,
165
    items: Arc<Mutex<Vector<Arc<TimelineItem>>>>,
166
    task: JoinHandle<()>,
167
}
168
169
#[derive(Default)]
170
pub struct AppState {
171
    /// What popup are we showing that is covering the majority of the screen,
172
    /// mainly used for help and settings screens.
173
    global_mode: GlobalMode,
174
175
    /// State for a global throbber.
176
    throbber_state: ThrobberState,
177
}
178
179
struct App {
180
    /// Reference to the main SDK client.
181
    client: Client,
182
183
    /// The sync service used for synchronizing events.
184
    sync_service: Arc<SyncService>,
185
186
    /// Timelines data structures for each room.
187
    timelines: Timelines,
188
189
    /// The room list widget on the left-hand side of the screen.
190
    room_list: RoomList,
191
192
    /// A view displaying the contents of the selected room, the widget on the
193
    /// right-hand side of the screen.
194
    room_view: RoomView,
195
196
    /// Task listening to room list service changes, and spawning timelines.
197
    listen_task: JoinHandle<()>,
198
199
    /// Task that is indexing events for search.
200
    indexing_task: JoinHandle<()>,
201
202
    /// Receiver that notifies when indexing is complete.
203
    indexing_receiver: Receiver<(bool, IndexingMessage)>,
204
205
    /// The status widget at the bottom of the screen.
206
    status: Status,
207
208
    state: AppState,
209
210
    last_tick: Instant,
211
}
212
213
impl App {
214
    const TICK_RATE: Duration = Duration::from_millis(250);
215
216
    async fn new(client: Client, share_pos: bool) -> Result<Self> {
217
        let sync_service =
218
            Arc::new(SyncService::builder(client.clone()).with_share_pos(share_pos).build().await?);
219
220
        let rooms = Rooms::default();
221
        let room_infos = RoomInfos::default();
222
        let timelines = Timelines::default();
223
224
        let room_list_service = sync_service.room_list_service();
225
        let all_rooms = room_list_service.all_rooms().await?;
226
227
        let listen_task = spawn(Self::listen_task(
228
            rooms.clone(),
229
            room_infos.clone(),
230
            timelines.clone(),
231
            all_rooms,
232
        ));
233
234
        // This will sync (with encryption) until an error happens or the program is
235
        // stopped.
236
        sync_service.start().await;
237
238
        let status = Status::new();
239
        let room_list =
240
            RoomList::new(client.clone(), rooms, room_infos, sync_service.clone(), status.handle());
241
242
        let room_view = RoomView::new(client.clone(), timelines.clone(), status.handle());
243
244
        let (indexing_sender, indexing_receiver) = channel::<(bool, IndexingMessage)>(1024);
245
        let indexing_task =
246
            spawn(App::indexing_task(client.clone(), indexing_sender, sync_service.clone()));
247
248
        let indexing_view = IndexingView::new();
249
250
        Ok(Self {
251
            sync_service,
252
            timelines,
253
            room_list,
254
            room_view,
255
            client,
256
            listen_task,
257
            indexing_task,
258
            indexing_receiver,
259
            status,
260
            state: AppState {
261
                global_mode: GlobalMode::Indexing { view: indexing_view },
262
                ..Default::default()
263
            },
264
            last_tick: Instant::now(),
265
        })
266
    }
267
268
    async fn listen_task(
269
        rooms: Rooms,
270
        room_infos: RoomInfos,
271
        timelines: Timelines,
272
        all_rooms: room_list_service::RoomList,
273
    ) {
274
        let (stream, entries_controller) = all_rooms.entries_with_dynamic_adapters(50_000);
275
        entries_controller.set_filter(Box::new(new_filter_non_left()));
276
277
        pin_mut!(stream);
278
279
        let mut previous_rooms = HashSet::new();
280
281
        while let Some(diffs) = stream.next().await {
282
            let all_rooms = {
283
                // Apply the diffs to the list of room entries.
284
                let mut rooms = rooms.lock();
285
286
                for diff in diffs {
287
                    diff.apply(&mut rooms);
288
                }
289
290
                // Collect rooms early to release the room entries list lock.
291
                (*rooms).clone()
292
            };
293
294
            let mut new_rooms = HashMap::new();
295
            let mut new_timelines = Vec::new();
296
297
            // Update all the room info for all rooms.
298
            for room in all_rooms.iter() {
299
                let raw_name = room.name();
300
                let display_name =
301
                    room.cached_display_name().map(|display_name| display_name.to_string());
302
                let is_dm = room
303
                    .is_direct()
304
                    .await
305
                    .map_err(|err| {
306
                        warn!("couldn't figure whether a room is a DM or not: {err}");
307
                    })
308
                    .ok();
309
                room_infos.lock().insert(
310
                    room.room_id().to_owned(),
311
                    ExtraRoomInfo { raw_name, display_name, is_dm },
312
                );
313
            }
314
315
            // Initialize all the new rooms.
316
            for room in
317
                all_rooms.into_iter().filter(|room| !previous_rooms.contains(room.room_id()))
318
            {
319
                // Initialize the timeline.
320
                let Ok(timeline) = room
321
                    .timeline_builder()
322
                    .with_focus(TimelineFocus::Live { hide_threaded_events: true })
323
                    .build()
324
                    .await
325
                else {
326
                    error!("error when creating default timeline");
327
                    continue;
328
                };
329
330
                // Save the timeline in the cache.
331
                let (items, stream) = timeline.subscribe().await;
332
                let items = Arc::new(Mutex::new(items));
333
334
                // Spawn a timeline task that will listen to all the timeline item changes.
335
                let i = items.clone();
336
                let timeline_task = spawn(async move {
337
                    pin_mut!(stream);
338
                    let items = i;
339
                    while let Some(diffs) = stream.next().await {
340
                        let mut items = items.lock();
341
342
                        for diff in diffs {
343
                            diff.apply(&mut items);
344
                        }
345
                    }
346
                });
347
348
                new_timelines.push((
349
                    room.room_id().to_owned(),
350
                    Timeline { timeline: Arc::new(timeline), items, task: timeline_task },
351
                ));
352
353
                // Save the room list service room in the cache.
354
                new_rooms.insert(room.room_id().to_owned(), room);
355
            }
356
357
            previous_rooms.extend(new_rooms.into_keys());
358
359
            timelines.lock().extend(new_timelines);
360
        }
361
    }
362
363
    async fn wait_for_room_sync(
364
        update_sender: &Sender<(bool, IndexingMessage)>,
365
        sync_service: Arc<SyncService>,
366
    ) {
367
        let mut sync_subscriber = sync_service.room_list_service().state();
368
369
        // Spin until there are rooms to index
370
        while let Some(state) = sync_subscriber.next().await {
371
            match state {
372
                State::Running => return,
373
                State::Terminated { from: _prev } => {
374
                    while let Err(e) =
375
                        update_sender.send((true, IndexingMessage::Progress(0))).await
376
                    {
377
                        debug!("Failed to send final message, trying again: {e:?}");
378
                    }
379
                    return;
380
                }
381
                _ => {
382
                    debug!("Sync service not running. Waiting to start indexing. {state:?}");
383
                }
384
            }
385
        }
386
    }
387
388
    async fn index_event_cache(
389
        client: &Client,
390
        update_sender: &Sender<(bool, IndexingMessage)>,
391
        store: &EventCacheStoreLockGuard,
392
        search_index_guard: &mut SearchIndexGuard<'_>,
393
        mut count: usize,
394
    ) -> Result<usize, ()> {
395
        for room in client.rooms_filtered(RoomStateFilter::JOINED.union(RoomStateFilter::LEFT)) {
396
            let room_id = room.room_id();
397
398
            let maybe_room_cache = room.event_cache().await;
399
            let Ok((room_cache, _drop_handles)) = maybe_room_cache else {
400
                warn!("Failed to get RoomEventCache: {maybe_room_cache:?}");
401
                continue;
402
            };
403
404
            let redaction_rules = room.clone_info().room_version_rules_or_default().redaction;
405
406
            let maybe_timeline_events = store.get_room_events(room_id, None, None).await;
407
            let Ok(timeline_events) = maybe_timeline_events else {
408
                warn!("Failed to get room's events: {maybe_timeline_events:?}");
409
                continue;
410
            };
411
412
            let no_of_events = timeline_events.len();
413
414
            if let Err(err) = search_index_guard
415
                .bulk_handle_timeline_event(
416
                    timeline_events.clone().into_iter(),
417
                    &room_cache,
418
                    room_id,
419
                    &redaction_rules,
420
                )
421
                .await
422
            {
423
                error!("Failed to handle event for indexing: {err}");
424
                let mut error = Some(err);
425
                while let Some(err) = error.take() {
426
                    if let Err(e) =
427
                        update_sender.send((true, IndexingMessage::Error(err.to_string()))).await
428
                    {
429
                        debug!("Failed to send final error message, trying again: {e:?}");
430
                    }
431
                }
432
                return Err(());
433
            }
434
435
            count += no_of_events;
436
            let _ = update_sender.send((false, IndexingMessage::Progress(count))).await;
437
        }
438
        Ok(count)
439
    }
440
441
    async fn index_from_server(
442
        client: &Client,
443
        update_sender: &Sender<(bool, IndexingMessage)>,
444
        search_index_guard: &mut SearchIndexGuard<'_>,
445
        mut count: usize,
446
    ) -> Result<usize, ()> {
447
        let batch_size = 25;
448
449
        let mut rooms = client.rooms_filtered(RoomStateFilter::JOINED);
450
        let mut idx = 0;
451
452
        while !rooms.is_empty() {
453
            let room = &rooms[idx];
454
455
            let room_id = room.room_id();
456
457
            let maybe_room_cache = room.event_cache().await;
458
            let Ok((room_cache, _drop_handles)) = maybe_room_cache else {
459
                warn!("Failed to get RoomEventCache: {maybe_room_cache:?}");
460
                idx = (idx + 1) % rooms.len();
461
                continue;
462
            };
463
464
            let redaction_rules = room.clone_info().room_version_rules_or_default().redaction;
465
466
            let Ok(pagination) = room_cache.pagination().run_backwards_until(batch_size).await
467
            else {
468
                error!("Failed to backpaginate {room_id}");
469
                idx = (idx + 1) % rooms.len();
470
                continue;
471
            };
472
473
            let no_of_events = pagination.events.len();
474
475
            if let Err(err) = search_index_guard
476
                .bulk_handle_timeline_event(
477
                    pagination.events.clone().into_iter(),
478
                    &room_cache,
479
                    room_id,
480
                    &redaction_rules,
481
                )
482
                .await
483
            {
484
                warn!("Failed to handle event for indexing: {err}");
485
                let mut error = Some(err);
486
                while let Some(err) = error.take() {
487
                    if let Err(e) =
488
                        update_sender.send((true, IndexingMessage::Error(err.to_string()))).await
489
                    {
490
                        debug!("Failed to send final error message, trying again: {e:?}");
491
                    }
492
                }
493
                return Err(());
494
            }
495
496
            count += no_of_events;
497
            let _ = update_sender.send((false, IndexingMessage::Progress(count))).await;
498
499
            if pagination.reached_start {
500
                rooms.remove(idx);
501
                let len = rooms.len();
502
                if len > 0 {
503
                    idx %= len;
504
                }
505
            } else {
506
                idx = (idx + 1) % rooms.len();
507
            }
508
        }
509
        Ok(count)
510
    }
511
512
    /// The sender sends (progress, done?, error?).
513
    async fn indexing_task(
514
        client: Client,
515
        update_sender: Sender<(bool, IndexingMessage)>,
516
        sync_service: Arc<SyncService>,
517
    ) {
518
        if timeout(Duration::from_secs(30), App::wait_for_room_sync(&update_sender, sync_service))
519
            .await
520
            .is_err()
521
        {
522
            debug!("Waiting for sync to run timed out. Quitting indexing task.");
523
            return;
524
        }
525
526
        let Ok(store) = client.event_cache_store().lock().await else {
527
            error!("Failed to get EventCacheStore");
528
            return;
529
        };
530
531
        let mut search_index_guard = client.search_index().lock().await;
532
        let count = 0;
533
534
        debug!("Start indexing from the event cache.");
535
536
        // First index everything in the cache
537
        let Ok(count) = App::index_event_cache(
538
            &client,
539
            &update_sender,
540
            store.as_clean().expect("Only one process should access the event cache store"),
541
            &mut search_index_guard,
542
            count,
543
        )
544
        .await
545
        else {
546
            debug!("Quitting index task.");
547
            return;
548
        };
549
550
        // Now index from the server
551
        debug!("Start indexing from the server.");
552
553
        let Ok(count) =
554
            App::index_from_server(&client, &update_sender, &mut search_index_guard, count).await
555
        else {
556
            debug!("Quitting index task.");
557
            return;
558
        };
559
560
        while let Err(err) = update_sender.send((true, IndexingMessage::Progress(count))).await {
561
            debug!("couldn't send final update {err}, trying again.");
562
        }
563
    }
564
565
    fn set_global_mode(&mut self, mode: GlobalMode) {
566
        self.state.global_mode = mode;
567
    }
568
569
    async fn handle_global_event(&mut self, event: Event) -> Result<bool> {
570
        use KeyCode::*;
571
572
        match event {
573
            Event::Key(KeyEvent { code: F(1), modifiers: KeyModifiers::NONE, .. }) => {
574
                self.set_global_mode(GlobalMode::Help)
575
            }
576
577
            Event::Key(KeyEvent { code: F(10), modifiers: KeyModifiers::NONE, .. }) => self
578
                .set_global_mode(GlobalMode::Settings {
579
                    view: SettingsView::new(self.client.clone(), self.sync_service.clone()),
580
                }),
581
582
            Event::Key(KeyEvent {
583
                code: Char('j') | Down,
584
                modifiers: KeyModifiers::CONTROL,
585
                ..
586
            }) => {
587
                self.room_list.next_room().await;
588
                let room_id = self.room_list.get_selected_room_id();
589
                self.room_view.set_selected_room(room_id);
590
            }
591
592
            Event::Key(KeyEvent {
593
                code: Char('k') | Up, modifiers: KeyModifiers::CONTROL, ..
594
            }) => {
595
                self.room_list.previous_room().await;
596
                let room_id = self.room_list.get_selected_room_id();
597
                self.room_view.set_selected_room(room_id);
598
            }
599
600
            Event::Key(KeyEvent { code: Char('m'), modifiers: KeyModifiers::ALT, .. }) => {
601
                self.room_view.mark_as_read().await
602
            }
603
604
            Event::Key(KeyEvent { code: Char('q'), modifiers: KeyModifiers::CONTROL, .. }) => {
605
                if !matches!(self.state.global_mode, GlobalMode::Default) {
606
                    self.set_global_mode(GlobalMode::Default);
607
                } else {
608
                    return Ok(true);
609
                }
610
            }
611
612
            Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('r'), .. }) => {
613
                self.set_global_mode(GlobalMode::CreateRoom { view: CreateRoomView::new() })
614
            }
615
616
            Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('s'), .. }) => self
617
                .set_global_mode(GlobalMode::Searching {
618
                    view: SearchingView::new(false),
619
                    is_global: false,
620
                }),
621
622
            Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('g'), .. }) => self
623
                .set_global_mode(GlobalMode::Searching {
624
                    view: SearchingView::new(true),
625
                    is_global: true,
626
                }),
627
628
            _ => self.room_view.handle_event(event).await,
629
        }
630
631
        Ok(false)
632
    }
633
634
    fn on_tick(&mut self) {
635
        self.state.throbber_state.calc_next();
636
637
        match &mut self.state.global_mode {
638
            GlobalMode::Help
639
            | GlobalMode::Default
640
            | GlobalMode::CreateRoom { .. }
641
            | GlobalMode::Searching { .. }
642
            | GlobalMode::Exiting { .. } => {}
643
            GlobalMode::Settings { view } => {
644
                view.on_tick();
645
            }
646
            GlobalMode::Indexing { view } => {
647
                view.on_tick();
648
            }
649
        }
650
    }
651
652
    async fn render_loop(&mut self, mut terminal: DefaultTerminal) -> Result<()> {
653
        use KeyCode::*;
654
655
        let mut check_channel = true;
656
657
        loop {
658
            if check_channel {
659
                match self.indexing_receiver.try_recv() {
660
                    Ok((done, message)) => {
661
                        if !matches!(message, IndexingMessage::Error(_)) && done {
662
                            self.set_global_mode(GlobalMode::Default);
663
                        } else if let GlobalMode::Indexing { view } = &mut self.state.global_mode {
664
                            view.set_message(message);
665
                        }
666
                    }
667
                    Err(TryRecvError::Disconnected) => check_channel = false,
668
                    Err(TryRecvError::Empty) => {}
669
                }
670
            }
671
672
            terminal.draw(|f| f.render_widget(&mut *self, f.area()))?;
673
674
            if event::poll(Duration::from_millis(100))? {
675
                let event = event::read()?;
676
677
                match &mut self.state.global_mode {
678
                    GlobalMode::Default => {
679
                        if self.handle_global_event(event).await? {
680
                            let sync_service = self.sync_service.clone();
681
                            let timelines = self.timelines.clone();
682
                            let listen_task = self.listen_task.abort_handle();
683
                            let indexing_task = self.indexing_task.abort_handle();
684
685
                            let shutdown_task = spawn(async move {
686
                                sync_service.stop().await;
687
688
                                listen_task.abort();
689
                                indexing_task.abort();
690
691
                                for timeline in timelines.lock().values() {
692
                                    timeline.task.abort();
693
                                }
694
                            });
695
696
                            self.set_global_mode(GlobalMode::Exiting { shutdown_task });
697
                        }
698
                    }
699
                    GlobalMode::Help => {
700
                        if let Event::Key(key) = event
701
                            && let KeyModifiers::NONE = key.modifiers
702
                            && let Char('q') | Esc = key.code
703
                        {
704
                            self.set_global_mode(GlobalMode::Default)
705
                        }
706
                    }
707
                    GlobalMode::Settings { view } => {
708
                        if let Event::Key(key) = event
709
                            && view.handle_key_press(key).await
710
                        {
711
                            self.set_global_mode(GlobalMode::Default);
712
                        }
713
                    }
714
                    GlobalMode::CreateRoom { view } => {
715
                        if let Event::Key(key) = event
716
                            && let KeyModifiers::NONE = key.modifiers
717
                        {
718
                            match key.code {
719
                                Enter => {
720
                                    if let Some(room_name) = view.get_text() {
721
                                        let mut request = CreateRoomRequest::new();
722
                                        request.name = Some(room_name);
723
                                        if let Err(err) = self
724
                                            .sync_service
725
                                            .room_list_service()
726
                                            .client()
727
                                            .create_room(request)
728
                                            .await
729
                                        {
730
                                            error!("error while creating room: {err:?}");
731
                                        }
732
                                    }
733
                                    self.set_global_mode(GlobalMode::Default);
734
                                }
735
                                Esc => self.set_global_mode(GlobalMode::Default),
736
                                _ => view.handle_key_press(key),
737
                            }
738
                        }
739
                    }
740
                    GlobalMode::Searching { view, is_global } => {
741
                        if let Event::Key(key) = event {
742
                            match key.code {
743
                                Enter => {
744
                                    if let Some(query) = view.get_text() {
745
                                        if *is_global {
746
                                            let mut search =
747
                                                self.client.search_messages(query, 5).build();
748
749
                                            let mut all_results = HashMap::new();
750
                                            loop {
751
                                                let Ok(results) = search.next_events().await else {
752
                                                    continue;
753
                                                };
754
                                                let Some(results) = results else {
755
                                                    break;
756
                                                };
757
                                                for (room_id, event_id) in results {
758
                                                    all_results
759
                                                        .entry(room_id)
760
                                                        .or_insert_with(Vec::new)
761
                                                        .push(event_id);
762
                                                }
763
                                            }
764
765
                                            view.set_results(
766
                                                all_results
767
                                                    .into_iter()
768
                                                    .map(|(room_id, events)| {
769
                                                        (Some(room_id), events)
770
                                                    })
771
                                                    .collect(),
772
                                            );
773
                                        } else if let Some((query, room)) =
774
                                            view.get_text().zip(self.room_view.room())
775
                                        {
776
                                            let mut room_search = room.search_messages(query, 5);
777
778
                                            let mut all_results = Vec::new();
779
                                            while let Some(results) =
780
                                                room_search.next_events().await?
781
                                            {
782
                                                all_results.extend(results);
783
                                            }
784
                                            view.set_results(vec![(None, all_results)]);
785
                                        }
786
                                    }
787
                                }
788
789
                                Esc => self.set_global_mode(GlobalMode::Default),
790
791
                                Up => view.list_state.previous(),
792
793
                                Down => view.list_state.next(),
794
795
                                _ => view.handle_key_press(key),
796
                            }
797
                        }
798
                    }
799
                    GlobalMode::Indexing { .. } => {
800
                        if let Event::Key(key) = event
801
                            && let KeyModifiers::NONE = key.modifiers
802
                            && let Esc = key.code
803
                        {
804
                            self.indexing_task.abort();
805
                            self.set_global_mode(GlobalMode::Default);
806
                        }
807
                    }
808
                    GlobalMode::Exiting { .. } => {}
809
                }
810
            }
811
812
            match &self.state.global_mode {
813
                GlobalMode::Default
814
                | GlobalMode::Help
815
                | GlobalMode::CreateRoom { .. }
816
                | GlobalMode::Searching { .. }
817
                | GlobalMode::Indexing { .. }
818
                | GlobalMode::Settings { .. } => {}
819
                GlobalMode::Exiting { shutdown_task } => {
820
                    if shutdown_task.is_finished() {
821
                        break;
822
                    }
823
                }
824
            }
825
826
            if self.last_tick.elapsed() >= Self::TICK_RATE {
827
                self.on_tick();
828
                self.last_tick = Instant::now();
829
            }
830
        }
831
832
        Ok(())
833
    }
834
835
    async fn run(&mut self, terminal: DefaultTerminal) -> Result<()> {
836
        self.render_loop(terminal).await?;
837
838
        // At this point the user has exited the loop, so shut down the application.
839
        ratatui::restore();
840
        execute!(stdout(), DisableMouseCapture)?;
841
842
        Ok(())
843
    }
844
}
845
846
impl Widget for &mut App {
847
    /// Render the whole app.
848
    fn render(self, area: Rect, buf: &mut Buffer) {
849
        // Create a space for header, room list and timeline and the footer.
850
        let vertical =
851
            Layout::vertical([Constraint::Length(2), Constraint::Min(0), Constraint::Length(1)]);
852
        let [header_area, rest_area, status_area] = vertical.areas(area);
853
854
        // Create two chunks with equal horizontal screen space. One for the list and
855
        // the other for the info block.
856
        let horizontal =
857
            Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(75)]);
858
        let [room_list_area, room_view_area] = horizontal.areas(rest_area);
859
860
        self.render_title(header_area, buf);
861
        self.room_list.render(room_list_area, buf);
862
        self.room_view.render(room_view_area, buf);
863
        self.status.render(status_area, buf, &mut self.state);
864
865
        match &mut self.state.global_mode {
866
            GlobalMode::Default => {}
867
            GlobalMode::Exiting { .. } => {
868
                Clear.render(rest_area, buf);
869
                let centered = create_centered_throbber_area(area);
870
                let throbber = Throbber::default()
871
                    .label("Exiting")
872
                    .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
873
                StatefulWidget::render(throbber, centered, buf, &mut self.state.throbber_state);
874
            }
875
            GlobalMode::Settings { view } => {
876
                view.render(area, buf);
877
            }
878
            GlobalMode::Help => {
879
                let mut help_view = HelpView::new();
880
                help_view.render(area, buf);
881
            }
882
            GlobalMode::CreateRoom { view } => {
883
                view.render(area, buf);
884
            }
885
            GlobalMode::Searching { view, .. } => {
886
                view.render(room_view_area, buf);
887
            }
888
            GlobalMode::Indexing { view } => {
889
                view.render(area, buf);
890
            }
891
        }
892
    }
893
}
894
895
impl App {
896
    /// Render the top square (title of the program).
897
    fn render_title(&self, area: Rect, buf: &mut Buffer) {
898
        Paragraph::new("Multiverse").bold().centered().render(area, buf);
899
    }
900
}
901
902
/// Configure the client so it's ready for sync'ing.
903
///
904
/// Will log in or reuse a previous session.
905
async fn configure_client(cli: Cli) -> Result<Client> {
906
    let Cli { server_name, session_path, proxy, dont_share_pos: _ } = cli;
907
908
    let mut client_builder = Client::builder()
909
        .store_config(
910
            StoreConfig::new(CrossProcessLockConfig::multi_process("multiverse"))
911
                .crypto_store(SqliteCryptoStore::open(session_path.join("crypto"), None).await?)
912
                .state_store(SqliteStateStore::open(session_path.join("state"), None).await?)
913
                .event_cache_store(
914
                    SqliteEventCacheStore::open(session_path.join("cache"), None).await?,
915
                ),
916
        )
917
        .server_name_or_homeserver_url(&server_name)
918
        .with_encryption_settings(EncryptionSettings {
919
            auto_enable_cross_signing: true,
920
            backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
921
            auto_enable_backups: true,
922
        })
923
        .with_enable_share_history_on_invite(true)
924
        .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
925
        .search_index_store(SearchIndexStoreKind::UnencryptedDirectory(
926
            session_path.join("indexData"),
927
        ));
928
929
    if let Some(proxy_url) = proxy {
930
        client_builder = client_builder.proxy(proxy_url).disable_ssl_verification();
931
    }
932
933
    let client = client_builder.build().await?;
934
935
    // Try reading a session, otherwise create a new one.
936
    log_in_or_restore_session(&client, &session_path).await?;
937
938
    Ok(client)
939
}
940
941
async fn log_in_or_restore_session(client: &Client, session_path: &Path) -> Result<()> {
942
    let session_path = session_path.join("session.json");
943
944
    if let Ok(serialized) = std::fs::read_to_string(&session_path) {
945
        let session: MatrixSession = serde_json::from_str(&serialized)?;
946
        client.restore_session(session).await?;
947
    } else {
948
        login_with_password(client).await?;
949
950
        // Immediately save the session to disk.
951
        if let Some(session) = client.session() {
952
            let AuthSession::Matrix(session) = session else {
953
                panic!("unexpected OAuth 2.0 session")
954
            };
955
            let serialized = serde_json::to_string(&session)?;
956
            std::fs::write(session_path, serialized)?;
957
958
            println!("saved session");
959
        }
960
    }
961
962
    Ok(())
963
}
964
965
/// Asks the user of a username and password, and try to login using the matrix
966
/// auth with those.
967
async fn login_with_password(client: &Client) -> Result<()> {
968
    println!("Logging in with username and password…");
969
970
    loop {
971
        print!("\nUsername: ");
972
        stdout().flush().expect("Unable to write to stdout");
973
        let mut username = String::new();
974
        io::stdin().read_line(&mut username).expect("Unable to read user input");
975
        username = username.trim().to_owned();
976
977
        let password = rpassword::prompt_password("Password.")?;
978
979
        match client.matrix_auth().login_username(&username, password.trim()).await {
980
            Ok(_) => {
981
                println!("Logged in as {username}");
982
                break;
983
            }
984
            Err(error) => {
985
                println!("Error logging in: {error}");
986
                println!("Please try again\n");
987
            }
988
        }
989
    }
990
991
    Ok(())
992
}