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