1 use crate::backend::{room, HandleError};
2 use crate::types::ExtraContent;
3 use comrak::{markdown_to_html, ComrakOptions};
4 use fractal_api::identifiers::{EventId, RoomId};
5 use fractal_api::r0::AccessToken;
6 use fractal_api::url::Url;
7 use gdk_pixbuf::Pixbuf;
8 use gio::prelude::FileExt;
9 use glib::clone;
10 use glib::source::Continue;
11 use gtk::prelude::*;
12 use lazy_static::lazy_static;
13 use log::error;
14 use rand::Rng;
15 use serde_json::json;
16 use serde_json::Value as JsonValue;
17 use std::env::temp_dir;
18 use std::fs;
19 use std::path::{Path, PathBuf};
20 use std::thread;
21
22 use crate::appop::room::Force;
23 use crate::appop::AppOp;
24 use crate::App;
25
26 use crate::uitypes::MessageContent;
27 use crate::uitypes::RowType;
28 use crate::widgets;
29
30 use crate::types::Message;
31
32 pub struct TmpMsg {
33 pub msg: Message,
34 pub widget: Option<gtk::Widget>,
35 }
36
37 impl AppOp {
get_message_by_id(&self, room_id: &RoomId, id: &EventId) -> Option<Message>38 pub fn get_message_by_id(&self, room_id: &RoomId, id: &EventId) -> Option<Message> {
39 let room = self.rooms.get(room_id)?;
40 let id = Some(id);
41 room.messages.iter().find(|m| m.id.as_ref() == id).cloned()
42 }
43
44 /// This function is used to mark as read the last message of a room when the focus comes in,
45 /// so we need to force the mark_as_read because the window isn't active yet
mark_active_room_messages(&mut self)46 pub fn mark_active_room_messages(&mut self) {
47 self.mark_last_message_as_read(Force(true));
48 }
49
add_room_message(&mut self, msg: &Message) -> Option<()>50 pub fn add_room_message(&mut self, msg: &Message) -> Option<()> {
51 if let Some(ui_msg) = self.create_new_room_message(msg) {
52 if let Some(ref mut history) = self.history {
53 history.add_new_message(
54 self.thread_pool.clone(),
55 self.user_info_cache.clone(),
56 ui_msg,
57 );
58 }
59 }
60 None
61 }
62
remove_room_message(&mut self, msg: &Message)63 pub fn remove_room_message(&mut self, msg: &Message) {
64 if let Some(ui_msg) = self.create_new_room_message(msg) {
65 if let Some(ref mut history) = self.history {
66 history.remove_message(
67 self.thread_pool.clone(),
68 self.user_info_cache.clone(),
69 ui_msg,
70 );
71 }
72 }
73 }
74
add_tmp_room_message(&mut self, msg: Message) -> Option<()>75 pub fn add_tmp_room_message(&mut self, msg: Message) -> Option<()> {
76 let login_data = self.login_data.clone()?;
77 let messages = self.history.as_ref()?.get_listbox();
78 if let Some(ui_msg) = self.create_new_room_message(&msg) {
79 let mb = widgets::MessageBox::new(login_data.server_url, login_data.access_token)
80 .tmpwidget(
81 self.thread_pool.clone(),
82 self.user_info_cache.clone(),
83 &ui_msg,
84 );
85 let m = mb.get_listbox_row();
86 messages.add(m);
87
88 if let Some(w) = messages.get_children().iter().last() {
89 self.msg_queue.insert(
90 0,
91 TmpMsg {
92 msg: msg.clone(),
93 widget: Some(w.clone()),
94 },
95 );
96 };
97 }
98 None
99 }
100
clear_tmp_msgs(&mut self)101 pub fn clear_tmp_msgs(&mut self) {
102 for t in self.msg_queue.iter_mut() {
103 if let Some(ref w) = t.widget {
104 w.destroy();
105 }
106 t.widget = None;
107 }
108 }
109
append_tmp_msgs(&mut self) -> Option<()>110 pub fn append_tmp_msgs(&mut self) -> Option<()> {
111 let login_data = self.login_data.clone()?;
112 let messages = self.history.as_ref()?.get_listbox();
113
114 let r = self.rooms.get(self.active_room.as_ref()?)?;
115 let mut widgets = vec![];
116 for t in self.msg_queue.iter().rev().filter(|m| m.msg.room == r.id) {
117 if let Some(ui_msg) = self.create_new_room_message(&t.msg) {
118 let mb = widgets::MessageBox::new(
119 login_data.server_url.clone(),
120 login_data.access_token.clone(),
121 )
122 .tmpwidget(
123 self.thread_pool.clone(),
124 self.user_info_cache.clone(),
125 &ui_msg,
126 );
127 let m = mb.get_listbox_row();
128 messages.add(m);
129
130 if let Some(w) = messages.get_children().iter().last() {
131 widgets.push(w.clone());
132 }
133 }
134 }
135
136 for (t, w) in self.msg_queue.iter_mut().rev().zip(widgets.iter()) {
137 t.widget = Some(w.clone());
138 }
139 None
140 }
141
mark_last_message_as_read(&mut self, Force(force): Force) -> Option<()>142 pub fn mark_last_message_as_read(&mut self, Force(force): Force) -> Option<()> {
143 let login_data = self.login_data.clone()?;
144 let window: gtk::Window = self
145 .ui
146 .builder
147 .get_object("main_window")
148 .expect("Can't find main_window in ui file.");
149 if window.is_active() || force {
150 /* Move the last viewed mark to the last message */
151 let active_room_id = self.active_room.as_ref()?;
152 let room = self.rooms.get_mut(active_room_id)?;
153 let uid = login_data.uid.clone();
154 room.messages.iter_mut().for_each(|msg| {
155 if msg.receipt.contains_key(&uid) {
156 msg.receipt.remove(&uid);
157 }
158 });
159 let last_message = room.messages.last_mut()?;
160 last_message.receipt.insert(uid, 0);
161
162 let room_id = last_message.room.clone();
163 let event_id = last_message.id.clone()?;
164 thread::spawn(move || {
165 match room::mark_as_read(
166 login_data.server_url,
167 login_data.access_token,
168 room_id,
169 event_id,
170 ) {
171 Ok((r, _)) => {
172 APPOP!(clear_room_notifications, (r));
173 }
174 Err(err) => {
175 err.handle_error();
176 }
177 }
178 });
179 }
180 None
181 }
182
msg_sent(&mut self, _txid: String, evid: Option<EventId>)183 pub fn msg_sent(&mut self, _txid: String, evid: Option<EventId>) {
184 if let Some(ref mut m) = self.msg_queue.pop() {
185 if let Some(ref w) = m.widget {
186 w.destroy();
187 }
188 m.widget = None;
189 m.msg.id = evid;
190 self.show_room_messages(vec![m.msg.clone()]);
191 }
192 self.force_dequeue_message();
193 }
194
retry_send(&mut self)195 pub fn retry_send(&mut self) {
196 gtk::timeout_add(5000, move || {
197 /* This will be removed once tmp messages are refactored */
198 APPOP!(force_dequeue_message);
199 Continue(false)
200 });
201 }
202
force_dequeue_message(&mut self)203 pub fn force_dequeue_message(&mut self) {
204 self.sending_message = false;
205 self.dequeue_message();
206 }
207
dequeue_message(&mut self) -> Option<()>208 pub fn dequeue_message(&mut self) -> Option<()> {
209 let login_data = self.login_data.clone()?;
210 if self.sending_message {
211 return None;
212 }
213
214 self.sending_message = true;
215 if let Some(next) = self.msg_queue.last() {
216 let msg = next.msg.clone();
217 match &next.msg.mtype[..] {
218 "m.image" | "m.file" | "m.audio" | "m.video" => {
219 thread::spawn(move || {
220 attach_file(login_data.server_url, login_data.access_token, msg)
221 });
222 }
223 _ => {
224 thread::spawn(move || {
225 match room::send_msg(login_data.server_url, login_data.access_token, msg) {
226 Ok((txid, evid)) => {
227 APPOP!(msg_sent, (txid, evid));
228 let initial = false;
229 let number_tries = 0;
230 APPOP!(sync, (initial, number_tries));
231 }
232 Err(err) => {
233 err.handle_error();
234 }
235 }
236 });
237 }
238 }
239 } else {
240 self.sending_message = false;
241 }
242 None
243 }
244
send_message(&mut self, msg: String)245 pub fn send_message(&mut self, msg: String) {
246 if msg.is_empty() {
247 // Not sending empty messages
248 return;
249 }
250
251 if let Some(room) = self.active_room.clone() {
252 if let Some(sender) = self.login_data.as_ref().map(|ld| ld.uid.clone()) {
253 let body = msg.clone();
254 let mtype = String::from("m.text");
255 let mut m = Message::new(room, sender, body, mtype, None);
256
257 if msg.starts_with("/me ") {
258 m.body = msg.trim_start_matches("/me ").to_owned();
259 m.mtype = String::from("m.emote");
260 }
261
262 // Riot does not properly show emotes with Markdown;
263 // Emotes with markdown have a newline after the username
264 if m.mtype != "m.emote" && self.md_enabled {
265 let mut md_options = ComrakOptions::default();
266 md_options.hardbreaks = true;
267 let mut md_parsed_msg = markdown_to_html(&msg, &md_options);
268
269 // Removing wrap tag: <p>..</p>\n
270 let limit = md_parsed_msg.len() - 5;
271 let trim = match (md_parsed_msg.get(0..3), md_parsed_msg.get(limit..)) {
272 (Some(open), Some(close)) if open == "<p>" && close == "</p>\n" => true,
273 _ => false,
274 };
275 if trim {
276 md_parsed_msg = md_parsed_msg
277 .get(3..limit)
278 .unwrap_or(&md_parsed_msg)
279 .to_string();
280 }
281
282 if md_parsed_msg != msg {
283 m.formatted_body = Some(md_parsed_msg);
284 m.format = Some(String::from("org.matrix.custom.html"));
285 }
286 }
287
288 self.add_tmp_room_message(m);
289 self.dequeue_message();
290 } else {
291 error!("Can't send message: No user is logged in");
292 }
293 } else {
294 error!("Can't send message: No active room");
295 }
296 }
297
attach_message(&mut self, path: PathBuf)298 pub fn attach_message(&mut self, path: PathBuf) {
299 if let Some(room) = self.active_room.clone() {
300 if let Some(sender) = self.login_data.as_ref().map(|ld| ld.uid.clone()) {
301 if let Ok(uri) = Url::from_file_path(&path) {
302 if let Ok(info) = gio::File::new_for_path(&path).query_info(
303 &gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
304 gio::FileQueryInfoFlags::NONE,
305 gio::NONE_CANCELLABLE,
306 ) {
307 // This should always return a type
308 let mime = info
309 .get_content_type()
310 .expect("Could not parse content type from file");
311 let mtype = match mime.as_ref() {
312 m if m.starts_with("image") => "m.image",
313 m if m.starts_with("audio") => "m.audio",
314 "application/x-riff" => "m.audio",
315 m if m.starts_with("video") => "m.video",
316 "application/x-mpegURL" => "m.video",
317 _ => "m.file",
318 };
319 let body = path
320 .file_name()
321 .and_then(|s| s.to_str())
322 .map(Into::into)
323 .unwrap_or_default();
324
325 let mut m = Message::new(room, sender, body, mtype.to_string(), None);
326 let info = match mtype {
327 "m.image" => get_image_media_info(&path, mime.as_ref()),
328 "m.audio" => get_audio_video_media_info(&uri, mime.as_ref()),
329 "m.video" => get_audio_video_media_info(&uri, mime.as_ref()),
330 "m.file" => get_file_media_info(&path, mime.as_ref()),
331 _ => None,
332 };
333
334 m.extra_content = info;
335 m.local_path = Some(path);
336
337 self.add_tmp_room_message(m);
338 self.dequeue_message();
339 } else {
340 error!("Can't send message: Could not query info");
341 }
342 } else {
343 error!("Can't send message: Path is not absolute")
344 }
345 } else {
346 error!("Can't send message: No user is logged in");
347 }
348 } else {
349 error!("Can't send message: No active room");
350 }
351 }
352
353 /// This method is called when a tmp message with an attach is sent correctly
354 /// to the matrix media server and we've the real url to use so we can
355 /// replace the tmp message with the same id with this new one
attached_file(&mut self, msg: Message)356 pub fn attached_file(&mut self, msg: Message) {
357 let p = self.msg_queue.iter().position(|m| m.msg == msg);
358 if let Some(i) = p {
359 let w = self.msg_queue.remove(i);
360 if let Some(w) = w.widget {
361 w.destroy()
362 }
363 }
364 self.add_tmp_room_message(msg);
365 }
366
367 /* TODO: find a better name for this function */
show_room_messages(&mut self, newmsgs: Vec<Message>) -> Option<()>368 pub fn show_room_messages(&mut self, newmsgs: Vec<Message>) -> Option<()> {
369 let mut msgs = vec![];
370
371 for msg in newmsgs.iter() {
372 if let Some(r) = self.rooms.get_mut(&msg.room) {
373 if !r.messages.contains(msg) {
374 r.messages.push(msg.clone());
375 msgs.push(msg.clone());
376 }
377 }
378 }
379
380 let mut msg_in_active = false;
381 let login_data = self.login_data.clone()?;
382 let uid = login_data.uid;
383 for msg in msgs.iter() {
384 if !msg.redacted && self.active_room.as_ref().map_or(false, |x| x == &msg.room) {
385 self.add_room_message(&msg);
386 msg_in_active = true;
387 }
388
389 if msg.replace != None {
390 /* No need to notify (and confuse the user) about edits. */
391 continue;
392 }
393
394 let should_notify = msg.sender != uid
395 && (msg.body.contains(&login_data.username.clone()?)
396 || self.rooms.get(&msg.room).map_or(false, |r| r.direct));
397
398 if should_notify {
399 let window: gtk::Window = self
400 .ui
401 .builder
402 .get_object("main_window")
403 .expect("Can't find main_window in ui file.");
404 if let (Some(app), Some(event_id)) = (window.get_application(), msg.id.as_ref()) {
405 self.notify(app, &msg.room, event_id);
406 }
407 }
408
409 self.roomlist.moveup(msg.room.clone());
410 self.roomlist.set_bold(msg.room.clone(), true);
411 }
412
413 if msg_in_active {
414 self.mark_last_message_as_read(Force(false));
415 }
416
417 None
418 }
419
420 /* TODO: find a better name for this function */
show_room_messages_top( &mut self, msgs: Vec<Message>, room_id: RoomId, prev_batch: Option<String>, )421 pub fn show_room_messages_top(
422 &mut self,
423 msgs: Vec<Message>,
424 room_id: RoomId,
425 prev_batch: Option<String>,
426 ) {
427 if let Some(r) = self.rooms.get_mut(&room_id) {
428 r.prev_batch = prev_batch;
429 }
430
431 let active_room = self.active_room.as_ref();
432 let mut list = vec![];
433 for item in msgs.iter().rev() {
434 /* create a list of new messages to load to the history */
435 if active_room.map_or(false, |a_room| item.room == *a_room) && !item.redacted {
436 if let Some(ui_msg) = self.create_new_room_message(item) {
437 list.push(ui_msg);
438 }
439 }
440
441 if let Some(r) = self.rooms.get_mut(&item.room) {
442 r.messages.insert(0, item.clone());
443 }
444 }
445
446 if let Some(ref mut history) = self.history {
447 history.add_old_messages_in_batch(
448 self.thread_pool.clone(),
449 self.user_info_cache.clone(),
450 list,
451 );
452 }
453 }
454
remove_message(&mut self, room_id: RoomId, id: EventId) -> Option<()>455 pub fn remove_message(&mut self, room_id: RoomId, id: EventId) -> Option<()> {
456 let message = self.get_message_by_id(&room_id, &id);
457
458 if let Some(msg) = message {
459 self.remove_room_message(&msg);
460 if let Some(ref mut room) = self.rooms.get_mut(&msg.room) {
461 if let Some(ref mut message) = room.messages.iter_mut().find(|e| e.id == msg.id) {
462 message.redacted = true;
463 }
464 }
465 }
466 None
467 }
468
469 /* parese a backend Message into a Message for the UI */
create_new_room_message(&self, msg: &Message) -> Option<MessageContent>470 pub fn create_new_room_message(&self, msg: &Message) -> Option<MessageContent> {
471 let login_data = self.login_data.clone()?;
472 let mut highlights = vec![];
473 lazy_static! {
474 static ref EMOJI_REGEX: regex::Regex = regex::Regex::new(r"(?x)
475 ^
476 [\p{White_Space}\p{Emoji}\p{Emoji_Presentation}\p{Emoji_Modifier}\p{Emoji_Modifier_Base}\p{Emoji_Component}]*
477 [\p{Emoji}]+
478 [\p{White_Space}\p{Emoji}\p{Emoji_Presentation}\p{Emoji_Modifier}\p{Emoji_Modifier_Base}\p{Emoji_Component}]*
479 $
480 # That string is made of at least one emoji, possibly more, possibly with modifiers, possibly with spaces, but nothing else
481 ").unwrap();
482 }
483
484 let t = match msg.mtype.as_ref() {
485 "m.emote" => RowType::Emote,
486 "m.image" => RowType::Image,
487 "m.sticker" => RowType::Sticker,
488 "m.audio" => RowType::Audio,
489 "m.video" => RowType::Video,
490 "m.file" => RowType::File,
491 _ => {
492 /* set message type to mention if the body contains the username, we should
493 * also match for MXID */
494 let is_mention = if let Some(user) = login_data.username.clone() {
495 msg.sender != login_data.uid && msg.body.contains(&user)
496 } else {
497 false
498 };
499
500 if is_mention {
501 if let Some(user) = login_data.username {
502 highlights.push(user);
503 }
504 highlights.push(login_data.uid.to_string());
505 highlights.push(String::from("message_menu"));
506
507 RowType::Mention
508 } else if EMOJI_REGEX.is_match(&msg.body) {
509 RowType::Emoji
510 } else {
511 RowType::Message
512 }
513 }
514 };
515
516 let room = self.rooms.get(&msg.room)?;
517 let name = if let Some(member) = room.members.get(&msg.sender) {
518 member.alias.clone()
519 } else {
520 None
521 };
522
523 let admin = room
524 .admins
525 .get(&login_data.uid)
526 .copied()
527 .unwrap_or_default();
528 let redactable = admin != 0 || login_data.uid == msg.sender;
529
530 let is_last_viewed = msg.receipt.contains_key(&login_data.uid);
531 Some(create_ui_message(
532 msg.clone(),
533 name,
534 t,
535 highlights,
536 redactable,
537 is_last_viewed,
538 ))
539 }
540 }
541
542 /* FIXME: don't convert msg to ui messages here, we should later get a ui message from storage */
create_ui_message( msg: Message, name: Option<String>, t: RowType, highlights: Vec<String>, redactable: bool, last_viewed: bool, ) -> MessageContent543 fn create_ui_message(
544 msg: Message,
545 name: Option<String>,
546 t: RowType,
547 highlights: Vec<String>,
548 redactable: bool,
549 last_viewed: bool,
550 ) -> MessageContent {
551 MessageContent {
552 msg: msg.clone(),
553 id: msg.id,
554 sender: msg.sender,
555 sender_name: name,
556 mtype: t,
557 body: msg.body,
558 date: msg.date,
559 replace_date: if msg.replace.is_some() {
560 Some(msg.date)
561 } else {
562 None
563 },
564 thumb: msg.thumb,
565 url: msg.url,
566 local_path: msg.local_path,
567 formatted_body: msg.formatted_body,
568 format: msg.format,
569 last_viewed,
570 highlights,
571 redactable,
572 widget: None,
573 }
574 }
575
576 /// This function opens the image, creates a thumbnail
577 /// and populates the info Json with the information it has
578
get_image_media_info(file: &Path, mimetype: &str) -> Option<JsonValue>579 fn get_image_media_info(file: &Path, mimetype: &str) -> Option<JsonValue> {
580 let (_, w, h) = Pixbuf::get_file_info(file)?;
581 let size = fs::metadata(file).ok()?.len();
582
583 // make thumbnail max 800x600
584 let thumb = Pixbuf::new_from_file_at_scale(&file, 800, 600, true).ok()?;
585 let mut rng = rand::thread_rng();
586 let x: u64 = rng.gen_range(1, 9_223_372_036_854_775_807);
587 let thumb_path = format!(
588 "{}/fractal_{}.png",
589 temp_dir().to_str().unwrap_or_default(),
590 x.to_string()
591 );
592 thumb.savev(&thumb_path, "png", &[]).ok()?;
593 let thumb_size = fs::metadata(&thumb_path).ok()?.len();
594
595 let info = json!({
596 "info": {
597 "thumbnail_url": thumb_path,
598 "thumbnail_info": {
599 "w": thumb.get_width(),
600 "h": thumb.get_height(),
601 "size": thumb_size,
602 "mimetype": "image/png"
603 },
604 "w": w,
605 "h": h,
606 "size": size,
607 "mimetype": mimetype,
608 "orientation": 0
609 }
610 });
611
612 Some(info)
613 }
614
get_audio_video_media_info(uri: &Url, mimetype: &str) -> Option<JsonValue>615 fn get_audio_video_media_info(uri: &Url, mimetype: &str) -> Option<JsonValue> {
616 let size = fs::metadata(uri.to_file_path().ok()?).ok()?.len();
617
618 if let Some(duration) = widgets::inline_player::get_media_duration(uri)
619 .ok()
620 .and_then(|d| d.mseconds())
621 {
622 Some(json!({
623 "info": {
624 "size": size,
625 "mimetype": mimetype,
626 "duration": duration,
627 }
628 }))
629 } else {
630 Some(json!({
631 "info": {
632 "size": size,
633 "mimetype": mimetype,
634 }
635 }))
636 }
637 }
638
get_file_media_info(file: &Path, mimetype: &str) -> Option<JsonValue>639 fn get_file_media_info(file: &Path, mimetype: &str) -> Option<JsonValue> {
640 let size = fs::metadata(file).ok()?.len();
641
642 let info = json!({
643 "info": {
644 "size": size,
645 "mimetype": mimetype,
646 }
647 });
648
649 Some(info)
650 }
651
652 struct NonMediaMsg;
653
attach_file(baseu: Url, tk: AccessToken, mut msg: Message) -> Result<(), NonMediaMsg>654 fn attach_file(baseu: Url, tk: AccessToken, mut msg: Message) -> Result<(), NonMediaMsg> {
655 let mut extra_content: Option<ExtraContent> = msg
656 .extra_content
657 .clone()
658 .and_then(|c| serde_json::from_value(c).ok());
659
660 let thumb_url = extra_content.clone().and_then(|c| c.info.thumbnail_url);
661
662 match (msg.url.clone(), msg.local_path.as_ref(), thumb_url) {
663 (Some(url), _, Some(thumb)) if url.scheme() == "mxc" && thumb.scheme() == "mxc" => {
664 send_msg_and_manage(baseu, tk, msg);
665
666 Ok(())
667 }
668 (_, Some(local_path), _) => {
669 if let Some(ref local_path_thumb) = msg.local_path_thumb {
670 let response = room::upload_file(baseu.clone(), tk.clone(), local_path_thumb)
671 .and_then(|response| Url::parse(&response.content_uri).map_err(Into::into));
672
673 match response {
674 Ok(thumb_uri) => {
675 msg.thumb = Some(thumb_uri.clone());
676 if let Some(ref mut xctx) = extra_content {
677 xctx.info.thumbnail_url = Some(thumb_uri);
678 }
679 msg.extra_content = serde_json::to_value(&extra_content).ok();
680 }
681 Err(err) => {
682 err.handle_error();
683 }
684 }
685
686 if let Err(_e) = std::fs::remove_file(local_path_thumb) {
687 error!("Can't remove thumbnail: {}", local_path_thumb.display());
688 }
689 }
690
691 let query =
692 room::upload_file(baseu.clone(), tk.clone(), local_path).and_then(|response| {
693 msg.url = Some(Url::parse(&response.content_uri)?);
694 thread::spawn(
695 clone!(@strong msg => move || send_msg_and_manage(baseu, tk, msg)),
696 );
697
698 Ok(msg)
699 });
700
701 match query {
702 Ok(msg) => {
703 APPOP!(attached_file, (msg));
704 }
705 Err(err) => {
706 err.handle_error();
707 }
708 };
709
710 Ok(())
711 }
712 _ => Err(NonMediaMsg),
713 }
714 }
715
send_msg_and_manage(baseu: Url, tk: AccessToken, msg: Message)716 fn send_msg_and_manage(baseu: Url, tk: AccessToken, msg: Message) {
717 match room::send_msg(baseu, tk, msg) {
718 Ok((txid, evid)) => {
719 APPOP!(msg_sent, (txid, evid));
720 let initial = false;
721 let number_tries = 0;
722 APPOP!(sync, (initial, number_tries));
723 }
724 Err(err) => {
725 err.handle_error();
726 }
727 };
728 }
729