multiverse/widgets/recovery/
recovering.rs1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2use futures_util::FutureExt as _;
3use matrix_sdk::{
4 Client,
5 encryption::{CrossSigningResetAuthType, recovery::RecoveryError},
6 reqwest::Url,
7 ruma::api::client::uiaa::{AuthData, Password},
8};
9use matrix_sdk_common::executor::spawn;
10use ratatui::{
11 prelude::*,
12 widgets::{Block, Paragraph, WidgetRef as _},
13};
14use ratatui_textarea::TextArea;
15use throbber_widgets_tui::{Throbber, ThrobberState};
16use tokio::{
17 sync::{
18 mpsc::{UnboundedSender, unbounded_channel},
19 oneshot,
20 },
21 task::JoinHandle,
22};
23
24use super::ShouldExit;
25use crate::widgets::{Hyperlink, recovery::create_centered_throbber_area};
26
27#[derive(Debug)]
28enum ResetState {
29 Waiting { receiver: oneshot::Receiver<ResetMessage> },
30 ResettingOauth { approval_url: Url },
31 InputtingMatrixAuthInfo { sender: UnboundedSender<String>, text_area: TextArea<'static> },
32 ResettingMatrixAuth,
33 Done,
34}
35
36#[derive(Debug)]
37enum ResetMessage {
38 Oauth { approval_url: Url },
39 MatrixAuth { password_sender: UnboundedSender<String> },
40}
41
42#[derive(Debug)]
43pub struct RecoveringView {
44 client: Client,
45 mode: Mode,
46}
47
48#[derive(Debug)]
49enum Mode {
50 Recovering {
51 recovery_task: JoinHandle<Result<(), RecoveryError>>,
52 throbber_state: ThrobberState,
53 },
54 Resetting {
55 reset_state: ResetState,
56 reset_task: JoinHandle<Result<(), RecoveryError>>,
57 throbber_state: ThrobberState,
58 },
59 Inputting {
60 recovery_text_area: TextArea<'static>,
61 },
62 Done {
63 result: Result<(), RecoveryError>,
64 },
65}
66
67impl RecoveringView {
68 pub fn new(client: Client) -> Self {
69 let mut recovery_text_area = TextArea::default();
70
71 recovery_text_area.set_cursor_line_style(Style::default());
72 recovery_text_area.set_mask_char('\u{2022}'); recovery_text_area.set_placeholder_text("To enable recovery enter the recover key");
74
75 recovery_text_area.set_style(Style::default().fg(Color::LightGreen));
76 recovery_text_area.set_block(Block::default());
77
78 Self { client, mode: Mode::Inputting { recovery_text_area } }
79 }
80
81 fn update(&mut self) {
82 use Mode::*;
83
84 match &mut self.mode {
85 Recovering { recovery_task, .. } => {
86 if recovery_task.is_finished() {
87 let result = recovery_task
88 .now_or_never()
89 .expect("The task should have finished, we checked it")
90 .expect("The recovery enabling task should neve panic");
91 self.mode = Done { result };
92 }
93 }
94 Resetting { reset_state, reset_task, .. } => match reset_state {
95 ResetState::Waiting { receiver } => {
96 match receiver.try_recv() {
97 Ok(ResetMessage::Oauth { approval_url }) => {
98 *reset_state = ResetState::ResettingOauth { approval_url };
99 }
100 Ok(ResetMessage::MatrixAuth { password_sender }) => {
101 let mut text_area = TextArea::default();
102
103 text_area.set_cursor_line_style(Style::default());
104 text_area.set_mask_char('\u{2022}'); text_area
106 .set_placeholder_text("To reset your identity enter your password");
107
108 text_area.set_style(Style::default().fg(Color::LightGreen));
109 text_area.set_block(Block::default());
110
111 *reset_state = ResetState::InputtingMatrixAuthInfo {
112 sender: password_sender,
113 text_area,
114 };
115 }
116 _ => {}
117 }
118 }
119 ResetState::InputtingMatrixAuthInfo { .. } | ResetState::Done => {}
120 ResetState::ResettingOauth { .. } | ResetState::ResettingMatrixAuth => {
121 if reset_task.is_finished() {
122 *reset_state = ResetState::Done;
123 }
124 }
125 },
126 Inputting { .. } | Done { .. } => {}
127 }
128 }
129
130 pub fn on_tick(&mut self) {
131 use Mode::*;
132
133 match &mut self.mode {
134 Recovering { throbber_state, .. } => throbber_state.calc_next(),
135 Resetting { throbber_state, .. } => throbber_state.calc_next(),
136 Inputting { .. } | Done { .. } => {}
137 }
138 }
139
140 pub fn is_idle(&self) -> bool {
141 match self.mode {
142 Mode::Recovering { .. } | Mode::Resetting { .. } | Mode::Done { .. } => false,
143 Mode::Inputting { .. } => true,
144 }
145 }
146
147 fn handle_identity_reset(&mut self) {
148 let client = self.client.clone();
149 let (sender, receiver) = oneshot::channel();
150
151 let user_id = client
152 .user_id()
153 .expect("We should have access to our user ID if we're resetting our identity")
154 .to_owned();
155
156 let reset_task = spawn(async move {
157 let handle = client.encryption().recovery().reset_identity().await?;
158
159 if let Some(handle) = handle {
160 match handle.auth_type() {
161 CrossSigningResetAuthType::Uiaa(_) => {
162 let (password_sender, mut password_receiver) = unbounded_channel();
163 let _ = sender.send(ResetMessage::MatrixAuth { password_sender });
164
165 let password = password_receiver
166 .recv()
167 .await
168 .expect("The sender should not have been closed");
169
170 handle
171 .reset(AuthData::Password(Password::new(user_id.into(), password)))
172 .await
173 }
174 CrossSigningResetAuthType::OAuth(oauth_cross_signing_reset_info) => {
175 sender
176 .send(ResetMessage::Oauth {
177 approval_url: oauth_cross_signing_reset_info.approval_url.clone(),
178 })
179 .expect("");
180 handle.reset(oauth_cross_signing_reset_info.as_auth_data()).await
181 }
182 }
183 } else {
184 Ok(())
185 }
186 });
187
188 let reset_state = ResetState::Waiting { receiver };
189
190 self.mode =
191 Mode::Resetting { reset_state, reset_task, throbber_state: ThrobberState::default() };
192 }
193
194 pub fn handle_key(&mut self, key: KeyEvent) -> ShouldExit {
195 use KeyCode::*;
196 use Mode::*;
197 use ShouldExit::*;
198
199 match &mut self.mode {
200 Recovering { .. } => No,
201 Resetting { reset_state, .. } => match reset_state {
202 ResetState::Waiting { .. }
203 | ResetState::ResettingOauth { .. }
204 | ResetState::ResettingMatrixAuth => match (key.modifiers, key.code) {
205 (_, Esc) => {
206 *self = Self::new(self.client.clone());
207 No
208 }
209 _ => No,
210 },
211 ResetState::InputtingMatrixAuthInfo { sender, text_area } => {
212 match (key.modifiers, key.code) {
213 (_, Enter) => {
214 let password = text_area.lines().join("");
215 sender
216 .send(password)
217 .expect("The task should still wait for the password");
218
219 *reset_state = ResetState::ResettingMatrixAuth;
220
221 No
222 }
223 _ => {
224 text_area.input(key);
225 No
226 }
227 }
228 }
229
230 ResetState::Done => OnlySubScreen,
231 },
232
233 Inputting { recovery_text_area } => {
234 match (key.modifiers, key.code) {
235 (KeyModifiers::CONTROL, Char('r')) => {
236 self.handle_identity_reset();
237 No
238 }
239 (_, Esc) => Yes,
240 (_, Enter) => {
241 let recovery_key = recovery_text_area.lines().join("");
245 let client = self.client.clone();
246
247 let recovery_task = spawn(async move {
248 client.encryption().recovery().recover(recovery_key.trim()).await
249 });
250
251 self.mode =
252 Recovering { recovery_task, throbber_state: ThrobberState::default() };
253
254 No
255 }
256 _ => {
257 recovery_text_area.input(key);
258
259 No
260 }
261 }
262 }
263 Done { .. } => OnlySubScreen,
264 }
265 }
266}
267
268impl Widget for &mut RecoveringView {
269 fn render(self, area: Rect, buf: &mut Buffer)
270 where
271 Self: Sized,
272 {
273 use Mode::*;
274
275 self.update();
276
277 match &mut self.mode {
278 Recovering { throbber_state, .. } => {
279 let throbber = Throbber::default()
280 .label("Recovering")
281 .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
282 let centered_area = create_centered_throbber_area(area);
283 StatefulWidget::render(throbber, centered_area, buf, throbber_state);
284 }
285
286 Resetting { throbber_state, reset_state, .. } => match reset_state {
287 ResetState::InputtingMatrixAuthInfo { text_area, .. } => {
288 let [left, right] =
289 Layout::horizontal([Constraint::Length(14), Constraint::Length(50)])
290 .areas(area);
291
292 Paragraph::new("Password:").render(left, buf);
293 text_area.render(right, buf);
294 }
295 ResetState::ResettingOauth { approval_url } => {
296 let chunks = Layout::default()
297 .direction(Direction::Horizontal)
298 .margin(1)
299 .constraints([
300 Constraint::Fill(1),
301 Constraint::Length(65),
302 Constraint::Fill(1),
303 ])
304 .split(area);
305
306 let chunks = Layout::default()
307 .direction(Direction::Vertical)
308 .margin(1)
309 .constraints([
310 Constraint::Fill(1),
311 Constraint::Length(1),
312 Constraint::Fill(1),
313 ])
314 .split(chunks[1]);
315
316 let centered_area = chunks[1];
317
318 let [left, right] =
319 Layout::horizontal([Constraint::Length(38), Constraint::Length(22)])
320 .areas(centered_area);
321
322 let hyperlink = Hyperlink::new(
323 Text::from("account management URL").blue(),
324 approval_url.to_string(),
325 );
326
327 Text::from("To finish the reset approve it at the ").render(left, buf);
328 hyperlink.render_ref(right, buf);
329 }
330 ResetState::Waiting { .. } | ResetState::ResettingMatrixAuth => {
331 let throbber = Throbber::default()
332 .label("Resetting your identity")
333 .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
334 let centered_area = create_centered_throbber_area(area);
335 StatefulWidget::render(throbber, centered_area, buf, throbber_state);
336 }
337 ResetState::Done => {
338 let constraints =
339 [Constraint::Fill(1), Constraint::Min(3), Constraint::Fill(1)];
340 let [_top, middle, _bottom] = Layout::vertical(constraints).areas(area);
341
342 Paragraph::new("Done resetting\n\nPress any key to continue")
343 .centered()
344 .render(middle, buf);
345 }
346 },
347
348 Inputting { recovery_text_area } => {
349 let [left, right] =
350 Layout::horizontal([Constraint::Length(14), Constraint::Length(50)])
351 .areas(area);
352
353 Paragraph::new("Recovery key: ").render(left, buf);
354 recovery_text_area.render(right, buf);
355 }
356
357 Done { result } => {
358 let constraints = [Constraint::Fill(1), Constraint::Min(3), Constraint::Fill(1)];
359 let [_top, middle, _bottom] = Layout::vertical(constraints).areas(area);
360
361 match result {
362 Ok(_) => {
363 Paragraph::new("Done recovering\n\nPress any key to continue")
364 .centered()
365 .render(middle, buf);
366 }
367 Err(error) => {
368 Paragraph::new(format!(
369 "Error recovering: {error:?}\n\nPress any key to continue"
370 ))
371 .centered()
372 .render(middle, buf);
373 }
374 }
375 }
376 }
377 }
378}