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},
13};
14use throbber_widgets_tui::{Throbber, ThrobberState};
15use tokio::{
16 sync::{
17 mpsc::{UnboundedSender, unbounded_channel},
18 oneshot,
19 },
20 task::JoinHandle,
21};
22use tui_textarea::TextArea;
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("");
247 let client = self.client.clone();
248
249 let recovery_task = spawn(async move {
250 client.encryption().recovery().recover(recovery_key.trim()).await
251 });
252
253 self.mode =
254 Recovering { recovery_task, throbber_state: ThrobberState::default() };
255
256 No
257 }
258 _ => {
259 recovery_text_area.input(key);
260
261 No
262 }
263 }
264 }
265 Done { .. } => OnlySubScreen,
266 }
267 }
268}
269
270impl Widget for &mut RecoveringView {
271 fn render(self, area: Rect, buf: &mut Buffer)
272 where
273 Self: Sized,
274 {
275 use Mode::*;
276
277 self.update();
278
279 match &mut self.mode {
280 Recovering { throbber_state, .. } => {
281 let throbber = Throbber::default()
282 .label("Recovering")
283 .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
284 let centered_area = create_centered_throbber_area(area);
285 StatefulWidget::render(throbber, centered_area, buf, throbber_state);
286 }
287
288 Resetting { throbber_state, reset_state, .. } => match reset_state {
289 ResetState::InputtingMatrixAuthInfo { text_area, .. } => {
290 let [left, right] =
291 Layout::horizontal([Constraint::Length(14), Constraint::Length(50)])
292 .areas(area);
293
294 Paragraph::new("Password:").render(left, buf);
295 text_area.render(right, buf);
296 }
297 ResetState::ResettingOauth { approval_url } => {
298 let chunks = Layout::default()
299 .direction(Direction::Horizontal)
300 .margin(1)
301 .constraints([
302 Constraint::Fill(1),
303 Constraint::Length(65),
304 Constraint::Fill(1),
305 ])
306 .split(area);
307
308 let chunks = Layout::default()
309 .direction(Direction::Vertical)
310 .margin(1)
311 .constraints([
312 Constraint::Fill(1),
313 Constraint::Length(1),
314 Constraint::Fill(1),
315 ])
316 .split(chunks[1]);
317
318 let centered_area = chunks[1];
319
320 let [left, right] =
321 Layout::horizontal([Constraint::Length(38), Constraint::Length(22)])
322 .areas(centered_area);
323
324 let hyperlink = Hyperlink::new(
325 Text::from("account management URL").blue(),
326 approval_url.to_string(),
327 );
328
329 Text::from("To finish the reset approve it at the ").render(left, buf);
330 hyperlink.render(right, buf);
331 }
332 ResetState::Waiting { .. } | ResetState::ResettingMatrixAuth => {
333 let throbber = Throbber::default()
334 .label("Resetting your identity")
335 .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
336 let centered_area = create_centered_throbber_area(area);
337 StatefulWidget::render(throbber, centered_area, buf, throbber_state);
338 }
339 ResetState::Done => {
340 let constraints =
341 [Constraint::Fill(1), Constraint::Min(3), Constraint::Fill(1)];
342 let [_top, middle, _bottom] = Layout::vertical(constraints).areas(area);
343
344 Paragraph::new("Done resetting\n\nPress any key to continue")
345 .centered()
346 .render(middle, buf);
347 }
348 },
349
350 Inputting { recovery_text_area } => {
351 let [left, right] =
352 Layout::horizontal([Constraint::Length(14), Constraint::Length(50)])
353 .areas(area);
354
355 Paragraph::new("Recovery key: ").render(left, buf);
356 recovery_text_area.render(right, buf);
357 }
358
359 Done { result } => {
360 let constraints = [Constraint::Fill(1), Constraint::Min(3), Constraint::Fill(1)];
361 let [_top, middle, _bottom] = Layout::vertical(constraints).areas(area);
362
363 match result {
364 Ok(_) => {
365 Paragraph::new("Done recovering\n\nPress any key to continue")
366 .centered()
367 .render(middle, buf);
368 }
369 Err(error) => {
370 Paragraph::new(format!(
371 "Error recovering: {error:?}\n\nPress any key to continue"
372 ))
373 .centered()
374 .render(middle, buf);
375 }
376 }
377 }
378 }
379 }
380}