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(Some(AuthData::Password(Password::new(
172 user_id.into(),
173 password,
174 ))))
175 .await
176 }
177 CrossSigningResetAuthType::OAuth(oauth_cross_signing_reset_info) => {
178 sender
179 .send(ResetMessage::Oauth {
180 approval_url: oauth_cross_signing_reset_info.approval_url.clone(),
181 })
182 .expect("");
183 handle.reset(None).await
184 }
185 }
186 } else {
187 Ok(())
188 }
189 });
190
191 let reset_state = ResetState::Waiting { receiver };
192
193 self.mode =
194 Mode::Resetting { reset_state, reset_task, throbber_state: ThrobberState::default() };
195 }
196
197 pub fn handle_key(&mut self, key: KeyEvent) -> ShouldExit {
198 use KeyCode::*;
199 use Mode::*;
200 use ShouldExit::*;
201
202 match &mut self.mode {
203 Recovering { .. } => No,
204 Resetting { reset_state, .. } => match reset_state {
205 ResetState::Waiting { .. }
206 | ResetState::ResettingOauth { .. }
207 | ResetState::ResettingMatrixAuth => match (key.modifiers, key.code) {
208 (_, Esc) => {
209 *self = Self::new(self.client.clone());
210 No
211 }
212 _ => No,
213 },
214 ResetState::InputtingMatrixAuthInfo { sender, text_area } => {
215 match (key.modifiers, key.code) {
216 (_, Enter) => {
217 let password = text_area.lines().join("");
218 sender
219 .send(password)
220 .expect("The task should still wait for the password");
221
222 *reset_state = ResetState::ResettingMatrixAuth;
223
224 No
225 }
226 _ => {
227 text_area.input(key);
228 No
229 }
230 }
231 }
232
233 ResetState::Done => OnlySubScreen,
234 },
235
236 Inputting { recovery_text_area } => {
237 match (key.modifiers, key.code) {
238 (KeyModifiers::CONTROL, Char('r')) => {
239 self.handle_identity_reset();
240 No
241 }
242 (_, Esc) => Yes,
243 (_, Enter) => {
244 let recovery_key = recovery_text_area.lines().join("");
248 let client = self.client.clone();
249
250 let recovery_task = spawn(async move {
251 client.encryption().recovery().recover(recovery_key.trim()).await
252 });
253
254 self.mode =
255 Recovering { recovery_task, throbber_state: ThrobberState::default() };
256
257 No
258 }
259 _ => {
260 recovery_text_area.input(key);
261
262 No
263 }
264 }
265 }
266 Done { .. } => OnlySubScreen,
267 }
268 }
269}
270
271impl Widget for &mut RecoveringView {
272 fn render(self, area: Rect, buf: &mut Buffer)
273 where
274 Self: Sized,
275 {
276 use Mode::*;
277
278 self.update();
279
280 match &mut self.mode {
281 Recovering { throbber_state, .. } => {
282 let throbber = Throbber::default()
283 .label("Recovering")
284 .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
285 let centered_area = create_centered_throbber_area(area);
286 StatefulWidget::render(throbber, centered_area, buf, throbber_state);
287 }
288
289 Resetting { throbber_state, reset_state, .. } => match reset_state {
290 ResetState::InputtingMatrixAuthInfo { text_area, .. } => {
291 let [left, right] =
292 Layout::horizontal([Constraint::Length(14), Constraint::Length(50)])
293 .areas(area);
294
295 Paragraph::new("Password:").render(left, buf);
296 text_area.render(right, buf);
297 }
298 ResetState::ResettingOauth { approval_url } => {
299 let chunks = Layout::default()
300 .direction(Direction::Horizontal)
301 .margin(1)
302 .constraints([
303 Constraint::Fill(1),
304 Constraint::Length(65),
305 Constraint::Fill(1),
306 ])
307 .split(area);
308
309 let chunks = Layout::default()
310 .direction(Direction::Vertical)
311 .margin(1)
312 .constraints([
313 Constraint::Fill(1),
314 Constraint::Length(1),
315 Constraint::Fill(1),
316 ])
317 .split(chunks[1]);
318
319 let centered_area = chunks[1];
320
321 let [left, right] =
322 Layout::horizontal([Constraint::Length(38), Constraint::Length(22)])
323 .areas(centered_area);
324
325 let hyperlink = Hyperlink::new(
326 Text::from("account management URL").blue(),
327 approval_url.to_string(),
328 );
329
330 Text::from("To finish the reset approve it at the ").render(left, buf);
331 hyperlink.render_ref(right, buf);
332 }
333 ResetState::Waiting { .. } | ResetState::ResettingMatrixAuth => {
334 let throbber = Throbber::default()
335 .label("Resetting your identity")
336 .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
337 let centered_area = create_centered_throbber_area(area);
338 StatefulWidget::render(throbber, centered_area, buf, throbber_state);
339 }
340 ResetState::Done => {
341 let constraints =
342 [Constraint::Fill(1), Constraint::Min(3), Constraint::Fill(1)];
343 let [_top, middle, _bottom] = Layout::vertical(constraints).areas(area);
344
345 Paragraph::new("Done resetting\n\nPress any key to continue")
346 .centered()
347 .render(middle, buf);
348 }
349 },
350
351 Inputting { recovery_text_area } => {
352 let [left, right] =
353 Layout::horizontal([Constraint::Length(14), Constraint::Length(50)])
354 .areas(area);
355
356 Paragraph::new("Recovery key: ").render(left, buf);
357 recovery_text_area.render(right, buf);
358 }
359
360 Done { result } => {
361 let constraints = [Constraint::Fill(1), Constraint::Min(3), Constraint::Fill(1)];
362 let [_top, middle, _bottom] = Layout::vertical(constraints).areas(area);
363
364 match result {
365 Ok(_) => {
366 Paragraph::new("Done recovering\n\nPress any key to continue")
367 .centered()
368 .render(middle, buf);
369 }
370 Err(error) => {
371 Paragraph::new(format!(
372 "Error recovering: {error:?}\n\nPress any key to continue"
373 ))
374 .centered()
375 .render(middle, buf);
376 }
377 }
378 }
379 }
380 }
381}