From 41ba9e9c7c4879a75d7f9e8d87347b2d2e925850 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 18 Dec 2024 11:23:56 +0100 Subject: [PATCH 01/16] introduce Token type from room token and ui to only talk to backend Token type declared in nc_request. NCTalk got a mark_current_room_as_read method. App now uses said method. Tests Fixed --- src/backend/nc_request/mod.rs | 26 +-- src/backend/nc_request/nc_req_data_room.rs | 4 +- src/backend/nc_room.rs | 97 ++++----- src/backend/nc_talk.rs | 219 +++++++++++++-------- src/main.rs | 7 +- src/ui/app.rs | 4 +- src/ui/chat_selector.rs | 22 +-- 7 files changed, 215 insertions(+), 164 deletions(-) diff --git a/src/backend/nc_request/mod.rs b/src/backend/nc_request/mod.rs index cc01dd6..45273e7 100644 --- a/src/backend/nc_request/mod.rs +++ b/src/backend/nc_request/mod.rs @@ -27,12 +27,14 @@ use std::{collections::HashMap, error::Error}; #[cfg(test)] use mockall::{mock, predicate::*}; +pub type Token = String; + #[async_trait] pub trait NCRequestInterface: Debug + Send + Clone + Default + Send + Sync { async fn send_message( &self, message: String, - token: &str, + token: &Token, ) -> Result>; async fn fetch_autocomplete_users( &self, @@ -40,7 +42,7 @@ pub trait NCRequestInterface: Debug + Send + Clone + Default + Send + Sync { ) -> Result, Box>; async fn fetch_participants( &self, - token: &str, + token: &Token, ) -> Result, Box>; async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box>; async fn fetch_rooms_update( @@ -49,12 +51,12 @@ pub trait NCRequestInterface: Debug + Send + Clone + Default + Send + Sync { ) -> Result<(Vec, i64), Box>; async fn fetch_chat_initial( &self, - token: &str, + token: &Token, maxMessage: i32, ) -> Result, Box>; async fn fetch_chat_update( &self, - token: &str, + token: &Token, maxMessage: i32, last_message: i32, ) -> Result, Box>; @@ -242,7 +244,7 @@ impl NCRequestInterface for NCRequest { async fn send_message( &self, message: String, - token: &str, + token: &Token, ) -> Result> { let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token; let params = HashMap::from([("message", message)]); @@ -296,7 +298,7 @@ impl NCRequestInterface for NCRequest { async fn fetch_participants( &self, - token: &str, + token: &Token, ) -> Result, Box> { let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v4/room/" @@ -340,7 +342,7 @@ impl NCRequestInterface for NCRequest { async fn fetch_chat_initial( &self, - token: &str, + token: &Token, maxMessage: i32, ) -> Result, Box> { let response_result = self.request_chat(token, maxMessage, None).await; @@ -357,7 +359,7 @@ impl NCRequestInterface for NCRequest { async fn fetch_chat_update( &self, - token: &str, + token: &Token, maxMessage: i32, last_message: i32, ) -> Result, Box> { @@ -399,7 +401,7 @@ mock! { async fn send_message( &self, message: String, - token: &str, + token: &Token, ) -> Result>; async fn fetch_autocomplete_users( &self, @@ -407,7 +409,7 @@ mock! { ) -> Result, Box>; async fn fetch_participants( &self, - token: &str, + token: &Token, ) -> Result, Box>; async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box>; async fn fetch_rooms_update( @@ -416,12 +418,12 @@ mock! { ) -> Result<(Vec, i64), Box>; async fn fetch_chat_initial( &self, - token: &str, + token: &Token, maxMessage: i32, ) -> Result, Box>; async fn fetch_chat_update( &self, - token: &str, + token: &Token, maxMessage: i32, last_message: i32, ) -> Result, Box>; diff --git a/src/backend/nc_request/nc_req_data_room.rs b/src/backend/nc_request/nc_req_data_room.rs index 9b1c0ca..142f5fa 100644 --- a/src/backend/nc_request/nc_req_data_room.rs +++ b/src/backend/nc_request/nc_req_data_room.rs @@ -1,11 +1,11 @@ -use super::NCReqDataMessage; +use super::{NCReqDataMessage, Token}; use serde::{Deserialize, Deserializer, Serialize}; #[derive(Serialize, Deserialize, Debug, Default, Clone)] #[allow(clippy::struct_excessive_bools)] pub struct NCReqDataRoom { pub id: i32, - pub token: String, + pub token: Token, #[serde(rename = "type")] pub roomtype: i32, pub name: String, diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 518c2c5..ad73a11 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -1,7 +1,9 @@ use super::{ nc_message::NCMessage, nc_notify::NCNotify, - nc_request::{NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCRequestInterface}, + nc_request::{ + NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCRequestInterface, Token, + }, }; use async_trait::async_trait; use log; @@ -42,23 +44,31 @@ pub trait NCRoomInterface: Debug + Send + Display + Ord + Default { fn to_json(&self) -> String; fn to_data(&self) -> NCReqDataRoom; fn write_to_log(&mut self) -> Result<(), std::io::Error>; - fn to_token(&self) -> String; - async fn update_if_id_is_newer( + fn to_token(&self) -> Token; + async fn update_if_id_is_newer( &mut self, message_id: i32, data_option: Option, + requester: &Requester, ) -> Result<(), Box>; - async fn send(&self, message: String) -> Result>; - async fn update( + async fn send( + &self, + message: String, + requester: &Requester, + ) -> Result>; + async fn update( &mut self, data_option: Option, + requester: &Requester, + ) -> Result<(), Box>; + async fn mark_as_read( + &self, + requester: &Requester, ) -> Result<(), Box>; - async fn mark_as_read(&self) -> Result<(), Box>; } #[derive(Debug, Default)] -pub struct NCRoom { - requester: Requester, +pub struct NCRoom { notifier: NCNotify, pub messages: Vec, room_data: NCReqDataRoom, @@ -67,13 +77,13 @@ pub struct NCRoom { participants: Vec, } -impl NCRoom { - pub async fn new( +impl NCRoom { + pub async fn new( room_data: NCReqDataRoom, requester: Requester, notifier: NCNotify, path_to_log: std::path::PathBuf, - ) -> Option> { + ) -> Option { let mut tmp_path_buf = path_to_log.clone(); tmp_path_buf.push(room_data.token.as_str()); let path = tmp_path_buf.as_path(); @@ -90,13 +100,13 @@ impl NCRoom::fetch_messages(&requester, &room_data.token, &mut messages) + NCRoom::fetch_messages::(&requester, &room_data.token, &mut messages) .await .ok(); } } else { log::debug!("No Log File found for room {}", room_data.displayName); - NCRoom::::fetch_messages(&requester, &room_data.token, &mut messages) + NCRoom::fetch_messages::(&requester, &room_data.token, &mut messages) .await .ok(); } @@ -106,7 +116,6 @@ impl NCRoom NCRoom( requester: &Requester, - token: &str, + token: &Token, messages: &mut Vec, ) -> Result<(), Box> { let response = requester.fetch_chat_initial(token, 200).await?; @@ -129,9 +138,7 @@ impl NCRoom NCRoomInterface - for NCRoom -{ +impl NCRoomInterface for NCRoom { // the room endpoint doesnt tell you about reactions... fn get_last_room_level_message_id(&self) -> Option { self.messages @@ -229,33 +236,34 @@ impl NCRoomInterfac } } - fn to_token(&self) -> String { + fn to_token(&self) -> Token { self.room_data.token.clone() } - async fn send(&self, message: String) -> Result> { + async fn send( + &self, + message: String, + requester: &Requester, + ) -> Result> { log::debug!("Send Message {}", &message); - let response = self - .requester - .send_message(message, self.room_data.token.as_str()) - .await; + let response = requester.send_message(message, &self.room_data.token).await; match response { Ok(v) => Ok(v.message), Err(v) => Err(v), } } - async fn update( + async fn update( &mut self, data_option: Option, + requester: &Requester, ) -> Result<(), Box> { if let Some(data) = data_option { self.room_data = data.clone(); } - let response = self - .requester + let response = requester .fetch_chat_update( - self.room_data.token.as_str(), + &self.room_data.token, 200, self.messages.last().unwrap().get_id(), ) @@ -275,17 +283,19 @@ impl NCRoomInterfac for message in response { self.messages.push(message.into()); } - self.participants = self - .requester + self.participants = requester .fetch_participants(&self.room_data.token) .await .expect("Failed to fetch room participants"); Ok(()) } - async fn mark_as_read(&self) -> Result<(), Box> { + async fn mark_as_read( + &self, + requester: &Requester, + ) -> Result<(), Box> { if !self.messages.is_empty() { - self.requester + requester .mark_chat_read( &self.room_data.token, self.messages.last().ok_or("No last message")?.get_id(), @@ -294,10 +304,11 @@ impl NCRoomInterfac } Ok(()) } - async fn update_if_id_is_newer( + async fn update_if_id_is_newer( &mut self, message_id: i32, data_option: Option, + requester: &Requester, ) -> Result<(), Box> { use std::cmp::Ordering; @@ -310,7 +321,7 @@ impl NCRoomInterfac last_internal_id, message_id ); - self.update(data_option).await?; + self.update(data_option, requester).await?; } Ordering::Less => { log::warn!( @@ -327,37 +338,33 @@ impl NCRoomInterfac } } -impl Ord for NCRoom { +impl Ord for NCRoom { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.to_string().cmp(other) } } -impl PartialOrd for NCRoom { +impl PartialOrd for NCRoom { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialEq for NCRoom { +impl PartialEq for NCRoom { fn eq(&self, other: &Self) -> bool { self.as_str() == other.as_str() } } -impl Eq for NCRoom {} +impl Eq for NCRoom {} -impl std::fmt::Display - for NCRoom -{ +impl std::fmt::Display for NCRoom { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } -impl std::ops::Deref - for NCRoom -{ +impl std::ops::Deref for NCRoom { type Target = String; fn deref(&self) -> &Self::Target { diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 2f9f53f..e85d57a 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -15,34 +15,38 @@ use std::{ path::{Path, PathBuf}, }; -use super::nc_room::{NCRoom, NCRoomTypes}; +use super::{ + nc_request::Token, + nc_room::{NCRoom, NCRoomTypes}, +}; #[async_trait] pub trait NCBackend: Debug + Send + Default { type Room: NCRoomInterface; fn write_to_log(&mut self) -> Result<(), std::io::Error>; - fn get_current_room_token(&self) -> &str; - fn get_room(&self, token: &str) -> &Self::Room; + fn get_current_room_token(&self) -> &Token; + fn get_room(&self, token: &Token) -> &Self::Room; fn get_current_room(&self) -> &Self::Room; - fn get_unread_rooms(&self) -> Vec; - fn get_room_by_displayname(&self, name: &str) -> String; - fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)>; - fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)>; - fn get_room_keys(&self) -> Vec<&'_ String>; + fn get_unread_rooms(&self) -> Vec; + fn get_room_by_displayname(&self, name: &str) -> Token; + fn get_dm_keys_display_name_mapping(&self) -> Vec<(Token, String)>; + fn get_group_keys_display_name_mapping(&self) -> Vec<(Token, String)>; + fn get_room_keys(&self) -> Vec<&'_ Token>; async fn send_message(&mut self, message: String) -> Result<(), Box>; - async fn select_room(&mut self, token: String) -> Result<(), Box>; + async fn select_room(&mut self, token: Token) -> Result<(), Box>; async fn update_rooms(&mut self, force_update: bool) -> Result<(), Box>; + async fn mark_current_room_as_read(&self) -> Result<(), Box>; fn add_room(&mut self, room_option: Option); } #[derive(Debug, Default)] pub struct NCTalk { - rooms: HashMap>, + rooms: HashMap, chat_data_path: PathBuf, last_requested: i64, requester: Requester, notifier: NCNotify, - pub current_room_token: String, + pub current_room_token: Token, } impl NCTalk { @@ -50,7 +54,7 @@ impl NCTalk, requester: Requester, notifier: NCNotify, - rooms: &mut HashMap>, + rooms: &mut HashMap, chat_log_path: PathBuf, ) { let v = response.into_iter().map(|child| { @@ -71,18 +75,18 @@ impl NCTalk, + mut data: HashMap, requester: &Requester, notify: &NCNotify, chat_log_path: &Path, - initial_message_ids: &mut HashMap, - rooms: &mut HashMap>, + initial_message_ids: &mut HashMap, + rooms: &mut HashMap, ) -> Result<(), Box> { let mut handles = HashMap::new(); for (token, room) in &mut data { handles.insert( token.clone(), - tokio::spawn(NCRoom::::new( + tokio::spawn(NCRoom::new::( room.clone(), requester.clone(), notify.clone(), @@ -93,12 +97,13 @@ impl NCTalk(token) { + if initial_message_ids.contains_key::(token) { let message_id = initial_message_ids.get(token).unwrap().lastMessage.id; json_room - .update_if_id_is_newer( + .update_if_id_is_newer::( message_id, Some((*initial_message_ids.get(token).unwrap()).clone()), + requester, ) .await?; rooms.insert(token.clone(), json_room); @@ -116,10 +121,10 @@ impl NCTalk (String, Option>) { + ) -> (Token, Option) { ( packaged_child.token.clone(), - NCRoom::::new(packaged_child, requester_box, notifier, chat_log_path).await, + NCRoom::new::(packaged_child, requester_box, notifier, chat_log_path).await, ) } pub async fn new( @@ -132,22 +137,30 @@ impl NCTalk = response + let mut initial_message_ids: HashMap = response .iter() .map(|room| (room.token.clone(), room)) - .collect::>(); + .collect::>(); + + let mut rooms = HashMap::::new(); - let mut rooms = HashMap::>::new(); + log::debug!("Trying to read from disk."); if path.exists() { if let Ok(data) = serde_json::from_str::>( std::fs::read_to_string(path).unwrap().as_str(), ) { + let token_data = data + .iter() + .map(|(key, room_data)| (Token::from(key), room_data.clone())) + .collect(); NCTalk::parse_files( - data, + token_data, &requester, ¬ify, chat_log_path.as_path(), @@ -202,16 +215,13 @@ impl NCTalk NCTalk NCBackend for NCTalk { - type Room = NCRoom; + type Room = NCRoom; fn write_to_log(&mut self) -> Result<(), std::io::Error> { use std::io::Write; - let mut data = HashMap::::new(); + let mut data = HashMap::::new(); let mut tmp_path_buf = self.chat_data_path.clone(); tmp_path_buf.push("Talk.json"); let path = tmp_path_buf.as_path(); @@ -265,28 +275,28 @@ impl NCBackend for } } - fn get_current_room_token(&self) -> &str { - self.current_room_token.as_str() + fn get_current_room_token(&self) -> &Token { + &self.current_room_token } fn get_current_room(&self) -> &Self::Room { &self.rooms[&self.current_room_token] } - fn get_room(&self, token: &str) -> &Self::Room { + fn get_room(&self, token: &Token) -> &Self::Room { &self.rooms[token] } - fn get_unread_rooms(&self) -> Vec { + fn get_unread_rooms(&self) -> Vec { self.rooms .values() .filter(|room| room.has_unread() && self.current_room_token != room.to_token()) .sorted_by(std::cmp::Ord::cmp) .map(NCRoomInterface::to_token) - .collect::>() + .collect::>() } - fn get_room_by_displayname(&self, name: &str) -> String { + fn get_room_by_displayname(&self, name: &str) -> Token { for room in self.rooms.values() { if room.to_string() == *name { return room.to_token(); @@ -295,7 +305,7 @@ impl NCBackend for panic!("room doesnt exist {}", name); } - fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)> { + fn get_dm_keys_display_name_mapping(&self) -> Vec<(Token, String)> { self.rooms .iter() .filter(|(_, room)| { @@ -311,8 +321,8 @@ impl NCBackend for .collect_vec() } - fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)> { - let mut mapping: Vec<(String, String)> = Vec::new(); + fn get_group_keys_display_name_mapping(&self) -> Vec<(Token, String)> { + let mut mapping: Vec<(Token, String)> = Vec::new(); for (key, room) in &self.rooms { match room.get_room_type() { NCRoomTypes::Group | NCRoomTypes::Public => { @@ -325,30 +335,30 @@ impl NCBackend for mapping } - fn get_room_keys(&self) -> Vec<&String> { - self.rooms.keys().collect::>() + fn get_room_keys(&self) -> Vec<&Token> { + self.rooms.keys().collect::>() } async fn send_message(&mut self, message: String) -> Result<(), Box> { self.rooms .get(&self.current_room_token) .ok_or("Room not found when it should be there")? - .send(message) + .send::(message, &self.requester) .await?; self.rooms .get_mut(&self.current_room_token) .ok_or("Room not found when it should be there")? - .update(None) + .update::(None, &self.requester) .await } - async fn select_room(&mut self, token: String) -> Result<(), Box> { + async fn select_room(&mut self, token: Token) -> Result<(), Box> { self.current_room_token.clone_from(&token); log::debug!("key {}", token); self.rooms .get_mut(&self.current_room_token) .ok_or_else(|| format!("Failed to get Room ref for room selection: {token}."))? - .update(None) + .update::(None, &self.requester) .await } @@ -362,16 +372,22 @@ impl NCBackend for }; self.last_requested = timestamp; for room in response { - if self.rooms.contains_key(room.token.as_str()) { + if self.rooms.contains_key(&room.token) { let room_ref = self .rooms - .get_mut(room.token.as_str()) + .get_mut(&room.token) .ok_or("Failed to get Room ref for update.")?; if force_update { - room_ref.update(Some(room)).await?; + room_ref + .update::(Some(room), &self.requester) + .await?; } else { room_ref - .update_if_id_is_newer(room.lastMessage.id, Some(room)) + .update_if_id_is_newer::( + room.lastMessage.id, + Some(room), + &self.requester, + ) .await?; } } else { @@ -390,6 +406,10 @@ impl NCBackend for Ok(()) } + async fn mark_current_room_as_read(&self) -> Result<(), Box> { + self.get_current_room().mark_as_read(&self.requester).await + } + fn add_room(&mut self, room_option: Option) { if let Some(room) = room_option { self.rooms.insert(room.to_token(), room); @@ -411,17 +431,18 @@ mock! { impl NCBackend for NCTalk{ type Room = MockNCRoomInterface; fn write_to_log(&mut self) -> Result<(), std::io::Error>; - fn get_current_room_token(&self) -> &str; - fn get_room(&self, token: &str) -> &::Room; + fn get_current_room_token(&self) -> &Token; + fn get_room(&self, token: &Token) -> &::Room; fn get_current_room(&self) -> &::Room; - fn get_unread_rooms(&self) -> Vec; - fn get_room_by_displayname(&self, name: &str) -> String; - fn get_dm_keys_display_name_mapping(&self) -> Vec<(String, String)>; - fn get_group_keys_display_name_mapping(&self) -> Vec<(String, String)>; - fn get_room_keys<'a>(&'a self) -> Vec<&'a String>; + fn get_unread_rooms(&self) -> Vec; + fn get_room_by_displayname(&self, name: &str) -> Token; + fn get_dm_keys_display_name_mapping(&self) -> Vec<(Token, String)>; + fn get_group_keys_display_name_mapping(&self) -> Vec<(Token, String)>; + fn get_room_keys<'a>(&'a self) -> Vec<&'a Token>; async fn send_message(& mut self, message: String) -> Result<(), Box>; - async fn select_room(&mut self, token: String) -> Result<(), Box>; + async fn select_room(&mut self, token: Token) -> Result<(), Box>; async fn update_rooms(& mut self, force_update: bool) -> Result<(), Box>; + async fn mark_current_room_as_read(&self) -> Result<(), Box>; fn add_room(&mut self, room_option: Option<::Room>); } } @@ -444,7 +465,9 @@ mod tests { use super::*; use crate::{ - backend::nc_request::{NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom}, + backend::nc_request::{ + MockNCRequest, NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, + }, config::init, }; @@ -485,13 +508,18 @@ mod tests { .return_once_st(move |_, _| Ok(vec![default_message.clone()])); mock_requester_fetch .expect_fetch_participants() - .times(2) + .times(1) .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); - mock_requester_fetch + mock_requester .expect_fetch_chat_update() .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); + mock_requester + .expect_fetch_participants() + .times(1) + .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); + mock_requester_file .expect_clone() .return_once_st(|| mock_requester_fetch); @@ -512,17 +540,20 @@ mod tests { #[tokio::test] async fn room_handling() { - let config = init("./test/").unwrap(); + let init = init("./test/").unwrap(); + let config = init; let mut mock_requester = crate::backend::nc_request::MockNCRequest::new(); let mut mock_requester_file = crate::backend::nc_request::MockNCRequest::new(); let mut mock_requester_fetch = crate::backend::nc_request::MockNCRequest::new(); let mock_requester_room = crate::backend::nc_request::MockNCRequest::new(); + let default_token = Token::from("123"); + let default_room = NCReqDataRoom { displayName: "General".to_string(), roomtype: 2, // Group Chat - token: "123".to_string(), + token: default_token.clone(), ..Default::default() }; @@ -559,31 +590,34 @@ mod tests { .return_once_st(move |_, _| Ok(vec![default_message.clone()])); mock_requester_fetch .expect_fetch_participants() - .times(4) + .times(1) .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); - - mock_requester_fetch + mock_requester + .expect_fetch_participants() + .times(3) + .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); + mock_requester .expect_fetch_chat_update() - .with(eq("123"), eq(200), eq(1)) + .with(eq(default_token.clone()), eq(200), eq(1)) .once() .in_sequence(&mut seq) .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); - mock_requester_fetch + mock_requester .expect_send_message() .once() .in_sequence(&mut seq) - .withf(|message: &String, token: &str| message == "Test" && token == "123") + .withf(|message: &String, token: &Token| message == "Test" && *token == "123") .return_once_st(|_, _| Ok(NCReqDataMessage::default())); - mock_requester_fetch + mock_requester .expect_fetch_chat_update() .once() - .with(eq("123"), eq(200), eq(2)) + .with(eq(Token::from("123")), eq(200), eq(2)) .in_sequence(&mut seq) .return_once_st(move |_, _, _| Ok(vec![post_send_message.clone()])); - mock_requester_fetch + mock_requester .expect_fetch_chat_update() - .with(eq("123"), eq(200), eq(3)) + .with(eq(Token::from("123")), eq(200), eq(3)) .once() .in_sequence(&mut seq) .return_once_st(move |_, _, _| Ok(vec![post_room_switch_message.clone()])); @@ -600,27 +634,37 @@ mod tests { .expect_clone() .return_once_st(|| mock_requester_room); - let mut backend = NCTalk::new(mock_requester, &config) + let backend = NCTalk::new(mock_requester, &config) .await .expect("Failed to create Backend"); + check_results(backend).await; + } + + async fn check_results(mut backend: NCTalk) { assert!(backend.send_message("Test".to_owned()).await.is_ok()); - assert!(backend.select_room("123".to_owned()).await.is_ok()); + assert!(backend.select_room("123".into()).await.is_ok()); assert!(backend.update_rooms(false).await.is_ok()); - assert_eq!(backend.get_current_room_token(), "123"); - assert_eq!(backend.get_room("123").to_token(), "123"); - assert_eq!(backend.get_current_room().to_token(), "123"); + assert_eq!(backend.get_current_room_token(), &Token::from("123")); + assert_eq!( + backend.get_room(&"123".into()).to_token(), + Token::from("123") + ); + assert_eq!(backend.get_current_room().to_token(), Token::from("123")); assert_eq!(backend.get_unread_rooms().len(), 0); - assert_eq!(backend.get_room_by_displayname("General"), "123"); + assert_eq!( + backend.get_room_by_displayname("General"), + Token::from("123") + ); assert_eq!(backend.get_dm_keys_display_name_mapping(), vec![]); assert_eq!( backend.get_group_keys_display_name_mapping(), - vec![("123".to_string(), "General".to_string())] + vec![("123".into(), "General".to_string())] ); - assert_eq!(backend.get_room_keys(), vec!["123"]); + assert_eq!(backend.get_room_keys(), vec![&Token::from("123")]); } #[tokio::test] @@ -639,7 +683,7 @@ mod tests { let default_room = NCReqDataRoom { displayName: "General".to_string(), - token: "a123".to_string(), + token: "a123".into(), roomtype: 2, // Group Chat ..Default::default() }; @@ -664,10 +708,13 @@ mod tests { .return_once_st(move |_, _| Ok(vec![default_message.clone()])); mock_requester_fetch .expect_fetch_participants() - .times(2) + .times(1) .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); - - mock_requester_fetch + mock_requester + .expect_fetch_participants() + .times(1) + .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); + mock_requester .expect_fetch_chat_update() .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); diff --git a/src/main.rs b/src/main.rs index 7bdbc24..bd81007 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,12 +28,7 @@ async fn main() -> Result<(), Box> { let requester = backend::nc_request::NCRequest::new(&config).expect("cannot create NCRequest"); - let backend = match backend::nc_talk::NCTalk::new(requester, &config).await { - Ok(backend) => backend, - Err(why) => { - panic!("Failed to create backend because: {}", why); - } - }; + let backend = backend::nc_talk::NCTalk::new(requester, &config).await?; let mut ui: ui::app::App<'_, _> = ui::app::App::new(backend, &config); ui.run(&config).await diff --git a/src/ui/app.rs b/src/ui/app.rs index ae1bd57..3625f6b 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -139,7 +139,7 @@ impl App<'_, Backend> { } pub async fn mark_current_as_read(&mut self) -> Result<(), Box> { - self.backend.get_current_room().mark_as_read().await?; + self.backend.mark_current_room_as_read().await?; self.backend.update_rooms(true).await?; self.update_ui()?; Ok(()) @@ -178,7 +178,7 @@ impl App<'_, Backend> { .selected() .last() .expect("no selection available") - .clone(), + .into(), ) .await?; self.current_screen = CurrentScreen::Reading; diff --git a/src/ui/chat_selector.rs b/src/ui/chat_selector.rs index 12f6cd9..030bc78 100644 --- a/src/ui/chat_selector.rs +++ b/src/ui/chat_selector.rs @@ -33,7 +33,7 @@ impl ChatSelector<'_> { .iter() .map(|token| { TreeItem::new_leaf::( - token.clone(), + token.to_string(), backend.get_room(token).get_display_name().into(), ) }) @@ -47,7 +47,7 @@ impl ChatSelector<'_> { .get_dm_keys_display_name_mapping() .iter() .map(|(token, display_name)| { - TreeItem::new_leaf::(token.clone(), display_name.clone()) + TreeItem::new_leaf::(token.to_string(), display_name.clone()) }) .collect_vec(), ) @@ -59,7 +59,7 @@ impl ChatSelector<'_> { .get_group_keys_display_name_mapping() .iter() .map(|(token, display_name)| { - TreeItem::new_leaf::(token.clone(), display_name.clone()) + TreeItem::new_leaf::(token.to_string(), display_name.clone()) }) .collect_vec(), ) @@ -80,7 +80,7 @@ impl ChatSelector<'_> { .iter() .map(|token| { TreeItem::new_leaf::( - token.clone(), + token.to_string(), backend.get_room(token).get_display_name().into(), ) }) @@ -93,7 +93,7 @@ impl ChatSelector<'_> { .get_dm_keys_display_name_mapping() .iter() .map(|(token, display_name)| { - TreeItem::new_leaf::(token.clone(), display_name.clone()) + TreeItem::new_leaf::(token.to_string(), display_name.clone()) }) .collect_vec(), )?, @@ -104,7 +104,7 @@ impl ChatSelector<'_> { .get_group_keys_display_name_mapping() .iter() .map(|(token, display_name)| { - TreeItem::new_leaf::(token.clone(), display_name.clone()) + TreeItem::new_leaf::(token.to_string(), display_name.clone()) }) .collect_vec(), )?, @@ -132,7 +132,7 @@ impl ChatSelector<'_> { #[cfg(test)] mod tests { - use crate::backend::nc_request::NCReqDataParticipants; + use crate::backend::nc_request::{NCReqDataParticipants, Token}; use crate::backend::nc_room::MockNCRoomInterface; use crate::backend::nc_talk::MockNCTalk; use crate::config::init; @@ -177,7 +177,7 @@ mod tests { .expect_get_unread_rooms() .once() .in_sequence(&mut seq) - .return_const(vec!["0".to_string()]); + .return_const(vec![Token::from("0")]); mock_room .expect_get_display_name() @@ -186,7 +186,7 @@ mod tests { mock_nc_backend .expect_get_room() - .with(eq("0".to_string())) + .with(eq(Token::from("0"))) .once() .in_sequence(&mut seq) .return_const(mock_room); @@ -195,13 +195,13 @@ mod tests { .expect_get_dm_keys_display_name_mapping() .once() .in_sequence(&mut seq) - .return_const(vec![("Butz".to_string(), "1".to_string())]); + .return_const(vec![(Token::from("Butz"), "1".to_string())]); mock_nc_backend .expect_get_group_keys_display_name_mapping() .once() .in_sequence(&mut seq) - .return_const(vec![("Bert".to_string(), "2".to_string())]); + .return_const(vec![(Token::from("Bert"), "2".to_string())]); let mut chat_selector_box = ChatSelector::new(&mock_nc_backend, &config); From 5c2a830e9c62440fd77e9ab9018c014c06a1ceff Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 18 Dec 2024 11:35:25 +0100 Subject: [PATCH 02/16] drop useless add_room method --- src/backend/nc_talk.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index e85d57a..5626881 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -25,8 +25,8 @@ pub trait NCBackend: Debug + Send + Default { type Room: NCRoomInterface; fn write_to_log(&mut self) -> Result<(), std::io::Error>; fn get_current_room_token(&self) -> &Token; - fn get_room(&self, token: &Token) -> &Self::Room; fn get_current_room(&self) -> &Self::Room; + fn get_room(&self, token: &Token) -> &Self::Room; fn get_unread_rooms(&self) -> Vec; fn get_room_by_displayname(&self, name: &str) -> Token; fn get_dm_keys_display_name_mapping(&self) -> Vec<(Token, String)>; @@ -36,7 +36,6 @@ pub trait NCBackend: Debug + Send + Default { async fn select_room(&mut self, token: Token) -> Result<(), Box>; async fn update_rooms(&mut self, force_update: bool) -> Result<(), Box>; async fn mark_current_room_as_read(&self) -> Result<(), Box>; - fn add_room(&mut self, room_option: Option); } #[derive(Debug, Default)] @@ -283,10 +282,6 @@ impl NCBackend for &self.rooms[&self.current_room_token] } - fn get_room(&self, token: &Token) -> &Self::Room { - &self.rooms[token] - } - fn get_unread_rooms(&self) -> Vec { self.rooms .values() @@ -392,14 +387,16 @@ impl NCBackend for } } else { self.notifier.new_room(&room.displayName)?; - self.add_room( + self.rooms.insert( + room.token.clone(), NCRoom::new( room, self.requester.clone(), self.notifier.clone(), self.chat_data_path.clone(), ) - .await, + .await + .expect("Could not Create Room."), ); } } @@ -409,11 +406,8 @@ impl NCBackend for async fn mark_current_room_as_read(&self) -> Result<(), Box> { self.get_current_room().mark_as_read(&self.requester).await } - - fn add_room(&mut self, room_option: Option) { - if let Some(room) = room_option { - self.rooms.insert(room.to_token(), room); - } + fn get_room(&self, token: &Token) -> &Self::Room { + &self.rooms[token] } } @@ -443,7 +437,6 @@ mock! { async fn select_room(&mut self, token: Token) -> Result<(), Box>; async fn update_rooms(& mut self, force_update: bool) -> Result<(), Box>; async fn mark_current_room_as_read(&self) -> Result<(), Box>; - fn add_room(&mut self, room_option: Option<::Room>); } } From a9e345edf713c92adfdf5db6de629bd3a175a5fd Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 18 Dec 2024 13:15:01 +0100 Subject: [PATCH 03/16] move current room handling into App --- src/backend/nc_talk.rs | 73 +++++++++++++++--------------------------- src/ui/app.rs | 68 ++++++++++++++++++++++----------------- src/ui/chat_box.rs | 22 ++++++++----- src/ui/title_bar.rs | 16 ++++++--- src/ui/users.rs | 10 +++--- 5 files changed, 95 insertions(+), 94 deletions(-) diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 5626881..7b08a68 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -24,18 +24,19 @@ use super::{ pub trait NCBackend: Debug + Send + Default { type Room: NCRoomInterface; fn write_to_log(&mut self) -> Result<(), std::io::Error>; - fn get_current_room_token(&self) -> &Token; - fn get_current_room(&self) -> &Self::Room; fn get_room(&self, token: &Token) -> &Self::Room; fn get_unread_rooms(&self) -> Vec; fn get_room_by_displayname(&self, name: &str) -> Token; fn get_dm_keys_display_name_mapping(&self) -> Vec<(Token, String)>; fn get_group_keys_display_name_mapping(&self) -> Vec<(Token, String)>; fn get_room_keys(&self) -> Vec<&'_ Token>; - async fn send_message(&mut self, message: String) -> Result<(), Box>; - async fn select_room(&mut self, token: Token) -> Result<(), Box>; + async fn send_message(&mut self, message: String, token: &Token) -> Result<(), Box>; + async fn select_room(&mut self, token: &Token) -> Result<(), Box>; async fn update_rooms(&mut self, force_update: bool) -> Result<(), Box>; - async fn mark_current_room_as_read(&self) -> Result<(), Box>; + async fn mark_current_room_as_read( + &self, + token: &Token, + ) -> Result<(), Box>; } #[derive(Debug, Default)] @@ -45,7 +46,6 @@ pub struct NCTalk { last_requested: i64, requester: Requester, notifier: NCNotify, - pub current_room_token: Token, } impl NCTalk { @@ -214,12 +214,11 @@ impl NCTalk NCBackend for } } - fn get_current_room_token(&self) -> &Token { - &self.current_room_token - } - - fn get_current_room(&self) -> &Self::Room { - &self.rooms[&self.current_room_token] - } - fn get_unread_rooms(&self) -> Vec { self.rooms .values() - .filter(|room| room.has_unread() && self.current_room_token != room.to_token()) + .filter(|room| room.has_unread()) .sorted_by(std::cmp::Ord::cmp) .map(NCRoomInterface::to_token) .collect::>() @@ -334,24 +325,23 @@ impl NCBackend for self.rooms.keys().collect::>() } - async fn send_message(&mut self, message: String) -> Result<(), Box> { + async fn send_message(&mut self, message: String, token: &Token) -> Result<(), Box> { self.rooms - .get(&self.current_room_token) + .get(token) .ok_or("Room not found when it should be there")? .send::(message, &self.requester) .await?; self.rooms - .get_mut(&self.current_room_token) + .get_mut(token) .ok_or("Room not found when it should be there")? .update::(None, &self.requester) .await } - async fn select_room(&mut self, token: Token) -> Result<(), Box> { - self.current_room_token.clone_from(&token); + async fn select_room(&mut self, token: &Token) -> Result<(), Box> { log::debug!("key {}", token); self.rooms - .get_mut(&self.current_room_token) + .get_mut(token) .ok_or_else(|| format!("Failed to get Room ref for room selection: {token}."))? .update::(None, &self.requester) .await @@ -403,8 +393,11 @@ impl NCBackend for Ok(()) } - async fn mark_current_room_as_read(&self) -> Result<(), Box> { - self.get_current_room().mark_as_read(&self.requester).await + async fn mark_current_room_as_read( + &self, + token: &Token, + ) -> Result<(), Box> { + self.rooms[token].mark_as_read(&self.requester).await } fn get_room(&self, token: &Token) -> &Self::Room { &self.rooms[token] @@ -425,18 +418,16 @@ mock! { impl NCBackend for NCTalk{ type Room = MockNCRoomInterface; fn write_to_log(&mut self) -> Result<(), std::io::Error>; - fn get_current_room_token(&self) -> &Token; fn get_room(&self, token: &Token) -> &::Room; - fn get_current_room(&self) -> &::Room; fn get_unread_rooms(&self) -> Vec; fn get_room_by_displayname(&self, name: &str) -> Token; fn get_dm_keys_display_name_mapping(&self) -> Vec<(Token, String)>; fn get_group_keys_display_name_mapping(&self) -> Vec<(Token, String)>; fn get_room_keys<'a>(&'a self) -> Vec<&'a Token>; - async fn send_message(& mut self, message: String) -> Result<(), Box>; - async fn select_room(&mut self, token: Token) -> Result<(), Box>; + async fn send_message(& mut self, message: String, token: &Token) -> Result<(), Box>; + async fn select_room(&mut self, token: &Token) -> Result<(), Box>; async fn update_rooms(& mut self, force_update: bool) -> Result<(), Box>; - async fn mark_current_room_as_read(&self) -> Result<(), Box>; + async fn mark_current_room_as_read(&self, token: &Token) -> Result<(), Box>; } } @@ -566,11 +557,6 @@ mod tests { id: 3, ..Default::default() }; - let post_room_switch_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 3, - ..Default::default() - }; let mut seq = Sequence::new(); @@ -587,7 +573,7 @@ mod tests { .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); mock_requester .expect_fetch_participants() - .times(3) + .times(2) .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); mock_requester .expect_fetch_chat_update() @@ -608,12 +594,6 @@ mod tests { .with(eq(Token::from("123")), eq(200), eq(2)) .in_sequence(&mut seq) .return_once_st(move |_, _, _| Ok(vec![post_send_message.clone()])); - mock_requester - .expect_fetch_chat_update() - .with(eq(Token::from("123")), eq(200), eq(3)) - .once() - .in_sequence(&mut seq) - .return_once_st(move |_, _, _| Ok(vec![post_room_switch_message.clone()])); mock_requester_file .expect_clone() @@ -635,18 +615,17 @@ mod tests { } async fn check_results(mut backend: NCTalk) { - assert!(backend.send_message("Test".to_owned()).await.is_ok()); - - assert!(backend.select_room("123".into()).await.is_ok()); + assert!(backend + .send_message("Test".to_owned(), &Token::from("123")) + .await + .is_ok()); assert!(backend.update_rooms(false).await.is_ok()); - assert_eq!(backend.get_current_room_token(), &Token::from("123")); assert_eq!( backend.get_room(&"123".into()).to_token(), Token::from("123") ); - assert_eq!(backend.get_current_room().to_token(), Token::from("123")); assert_eq!(backend.get_unread_rooms().len(), 0); assert_eq!( backend.get_room_by_displayname("General"), diff --git a/src/ui/app.rs b/src/ui/app.rs index 3625f6b..4dad170 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,5 +1,5 @@ use crate::{ - backend::{nc_room::NCRoomInterface, nc_talk::NCBackend}, + backend::{nc_request::Token, nc_room::NCRoomInterface, nc_talk::NCBackend}, config::Config, ui::{ chat_box::ChatBox, @@ -51,34 +51,33 @@ pub struct App<'a, Backend: NCBackend> { users: Users<'a>, user_sidebar_visible: bool, default_style: Style, + current_room_token: Token, } impl App<'_, Backend> { pub fn new(backend: Backend, config: &Config) -> Self { + let init_room = backend.get_room_by_displayname(config.data.ui.default_room.as_str()); Self { current_screen: CurrentScreen::Reading, - title: TitleBar::new( - CurrentScreen::Reading, - backend.get_current_room().to_string(), - config, - ), + title: TitleBar::new(CurrentScreen::Reading, init_room.clone(), config), selector: ChatSelector::new(&backend, config), input: InputBox::new("", config), chat: { let mut chat = ChatBox::new(config); - chat.update_messages(&backend); + chat.update_messages(&backend, &init_room); chat.select_last_message(); chat }, users: { let mut users = Users::new(config); - users.update(&backend); + users.update(&backend, &init_room); users }, backend, help: HelpBox::new(config), user_sidebar_visible: config.data.ui.user_sidebar_default, default_style: config.theme.default_style(), + current_room_token: init_room, } } @@ -117,39 +116,52 @@ impl App<'_, Backend> { .constraints([Constraint::Min(4), Constraint::Length(3)]) .split(base_layout[1]); - if self.user_sidebar_visible && self.backend.get_current_room().is_group() { + if self.user_sidebar_visible + && self.backend.get_room(&self.current_room_token).is_group() + { let chat_layout = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(80), Constraint::Percentage(20)]) .split(main_layout[0]); - self.chat - .set_width_and_update_if_change(chat_layout[0].width, &self.backend); + self.chat.set_width_and_update_if_change( + chat_layout[0].width, + &self.backend, + &self.current_room_token, + ); self.chat.render_area(f, chat_layout[0]); self.users.render_area(f, chat_layout[1]); } else { - self.chat - .set_width_and_update_if_change(main_layout[0].width, &self.backend); + self.chat.set_width_and_update_if_change( + main_layout[0].width, + &self.backend, + &self.current_room_token, + ); self.chat.render_area(f, main_layout[0]); }; self.input.render_area(f, main_layout[1]); } - self.title.update(self.current_screen, &self.backend); + self.title + .update(self.current_screen, &self.backend, &self.current_room_token); self.title.render_area(f, base_layout[0]); } pub async fn mark_current_as_read(&mut self) -> Result<(), Box> { - self.backend.mark_current_room_as_read().await?; + self.backend + .mark_current_room_as_read(&self.current_room_token) + .await?; self.backend.update_rooms(true).await?; self.update_ui()?; Ok(()) } fn update_ui(&mut self) -> Result<(), Box> { - self.title.update(self.current_screen, &self.backend); + self.title + .update(self.current_screen, &self.backend, &self.current_room_token); self.selector.update(&self.backend)?; - self.chat.update_messages(&self.backend); - self.users.update(&self.backend); + self.chat + .update_messages(&self.backend, &self.current_room_token); + self.users.update(&self.backend, &self.current_room_token); Ok(()) } @@ -158,7 +170,7 @@ impl App<'_, Backend> { Ok(()) } else { self.backend - .send_message(self.input.lines().join("\n")) + .send_message(self.input.lines().join("\n"), &self.current_room_token) .await?; self.input.select_all(); self.input.cut(); @@ -171,16 +183,14 @@ impl App<'_, Backend> { pub async fn select_room(&mut self) -> Result<(), Box> { if self.selector.state.selected().len() == 2 { - self.backend - .select_room( - self.selector - .state - .selected() - .last() - .expect("no selection available") - .into(), - ) - .await?; + self.current_room_token.clone_from( + self.selector + .state + .selected() + .last() + .expect("no selection available"), + ); + self.backend.select_room(&self.current_room_token).await?; self.current_screen = CurrentScreen::Reading; self.update_ui()?; self.chat.select_last_message(); diff --git a/src/ui/chat_box.rs b/src/ui/chat_box.rs index 813a9e0..d030a84 100644 --- a/src/ui/chat_box.rs +++ b/src/ui/chat_box.rs @@ -1,3 +1,4 @@ +use crate::backend::nc_request::Token; use crate::backend::{nc_room::NCRoomInterface, nc_talk::NCBackend}; use crate::config::Config; use ratatui::{ @@ -39,21 +40,26 @@ impl ChatBox<'_> { } } - pub fn set_width_and_update_if_change(&mut self, width: u16, backend: &impl NCBackend) { + pub fn set_width_and_update_if_change( + &mut self, + width: u16, + backend: &impl NCBackend, + current_room: &Token, + ) { let new_width = (width - TIME_WIDTH - 2 - NAME_WIDTH).max(10); if self.width != new_width { self.width = new_width; - self.update_messages(backend); + self.update_messages(backend, current_room); } } - pub fn update_messages(&mut self, backend: &impl NCBackend) { + pub fn update_messages(&mut self, backend: &impl NCBackend, current_room: &Token) { use itertools::Itertools; use std::convert::TryInto; self.messages.clear(); for message_data in backend - .get_current_room() + .get_room(current_room) .get_messages() .iter() .filter(|mes| !mes.is_reaction() && !mes.is_edit_note() && !mes.is_comment_deleted()) @@ -100,8 +106,8 @@ impl ChatBox<'_> { ]; self.messages.push(Row::new(reaction)); } - if backend.get_current_room().has_unread() - && backend.get_current_room().get_last_read() == message_data.get_id() + if backend.get_room(current_room).has_unread() + && backend.get_room(current_room).get_last_read() == message_data.get_id() { let unread_marker: Vec = vec![ "".into(), @@ -230,7 +236,7 @@ mod tests { .return_const(vec![mock_message_1, mock_message_2]); mock_room.expect_has_unread().times(2).return_const(false); mock_nc_backend - .expect_get_current_room() + .expect_get_room() .times(3) .return_const(mock_room); @@ -256,7 +262,7 @@ mod tests { terminal.backend().assert_buffer(&expected); - chat_box.update_messages(&mock_nc_backend); + chat_box.update_messages(&mock_nc_backend, &"123".to_string()); terminal .draw(|frame| chat_box.render_area(frame, Rect::new(0, 0, 40, 10))) diff --git a/src/ui/title_bar.rs b/src/ui/title_bar.rs index b984fac..185e5d7 100644 --- a/src/ui/title_bar.rs +++ b/src/ui/title_bar.rs @@ -1,3 +1,4 @@ +use crate::backend::nc_request::Token; use crate::backend::nc_room::NCRoomInterface; use crate::config::Config; use crate::{backend::nc_talk::NCBackend, ui::app::CurrentScreen}; @@ -32,10 +33,15 @@ impl TitleBar<'_> { } } - pub fn update(&mut self, screen: CurrentScreen, backend: &impl NCBackend) { + pub fn update( + &mut self, + screen: CurrentScreen, + backend: &impl NCBackend, + current_room: &Token, + ) { self.mode = screen.to_string(); - self.room = backend.get_current_room().to_string(); - self.unread = backend.get_current_room().get_unread(); + self.room = backend.get_room(current_room).to_string(); + self.unread = backend.get_room(current_room).get_unread(); let unread_array: Vec = backend .get_unread_rooms() .iter() @@ -137,10 +143,10 @@ mod tests { .once() .return_const(vec![]); mock_nc_backend - .expect_get_current_room() + .expect_get_room() .times(2) .return_const(mock_room); - bar.update(CurrentScreen::Reading, &mock_nc_backend); + bar.update(CurrentScreen::Reading, &mock_nc_backend, &"123".to_string()); terminal .draw(|frame| bar.render_area(frame, Rect::new(0, 0, 30, 3))) diff --git a/src/ui/users.rs b/src/ui/users.rs index 6901525..f992bbe 100644 --- a/src/ui/users.rs +++ b/src/ui/users.rs @@ -6,7 +6,7 @@ use ratatui::{ }; use style::Styled; -use crate::backend::{nc_room::NCRoomInterface, nc_talk::NCBackend}; +use crate::backend::{nc_request::Token, nc_room::NCRoomInterface, nc_talk::NCBackend}; use crate::config::Config; pub struct Users<'a> { @@ -36,9 +36,9 @@ impl Users<'_> { pub fn render_area(&self, frame: &mut Frame, area: Rect) { frame.render_stateful_widget(self, area, &mut self.state.clone()); } - pub fn update(&mut self, backend: &impl NCBackend) { + pub fn update(&mut self, backend: &impl NCBackend, current_room: &Token) { self.user_list = backend - .get_current_room() + .get_room(current_room) .get_users() .iter() .sorted_by(|user1, user2| user1.displayName.cmp(&user2.displayName)) @@ -114,10 +114,10 @@ mod tests { dummy_user.displayName = "Butz".to_string(); mock_room.expect_get_users().return_const(vec![dummy_user]); mock_nc_backend - .expect_get_current_room() + .expect_get_room() .once() .return_const(mock_room); - users.update(&mock_nc_backend); + users.update(&mock_nc_backend, &"123".to_string()); terminal .draw(|frame| users.render_area(frame, Rect::new(0, 0, 8, 8))) From f84f2c97a5fccfaea2f5cd599599e97de6c3e072 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Wed, 18 Dec 2024 13:56:31 +0100 Subject: [PATCH 04/16] move notifications from backend to frontend --- src/backend/mod.rs | 1 - src/backend/nc_room.rs | 25 ++++---- src/backend/nc_talk.rs | 64 +++++++++---------- src/ui/app.rs | 21 ++++-- src/ui/mod.rs | 1 + .../nc_notify.rs => ui/notifications.rs} | 26 +++++++- 6 files changed, 82 insertions(+), 56 deletions(-) rename src/{backend/nc_notify.rs => ui/notifications.rs} (79%) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 00c9a57..9959e55 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,5 +1,4 @@ pub mod nc_message; -pub mod nc_notify; pub mod nc_request; pub mod nc_room; pub mod nc_talk; diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index ad73a11..c8288db 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -1,6 +1,5 @@ use super::{ nc_message::NCMessage, - nc_notify::NCNotify, nc_request::{ NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCRequestInterface, Token, }, @@ -60,7 +59,7 @@ pub trait NCRoomInterface: Debug + Send + Display + Ord + Default { &mut self, data_option: Option, requester: &Requester, - ) -> Result<(), Box>; + ) -> Result, Box>; async fn mark_as_read( &self, requester: &Requester, @@ -69,7 +68,6 @@ pub trait NCRoomInterface: Debug + Send + Display + Ord + Default { #[derive(Debug, Default)] pub struct NCRoom { - notifier: NCNotify, pub messages: Vec, room_data: NCReqDataRoom, path_to_log: std::path::PathBuf, @@ -81,7 +79,6 @@ impl NCRoom { pub async fn new( room_data: NCReqDataRoom, requester: Requester, - notifier: NCNotify, path_to_log: std::path::PathBuf, ) -> Option { let mut tmp_path_buf = path_to_log.clone(); @@ -116,7 +113,6 @@ impl NCRoom { .expect("Failed to fetch room participants"); Some(NCRoom { - notifier, messages, path_to_log: tmp_path_buf, room_type: FromPrimitive::from_i32(room_data.roomtype).unwrap(), @@ -257,7 +253,7 @@ impl NCRoomInterface for NCRoom { &mut self, data_option: Option, requester: &Requester, - ) -> Result<(), Box> { + ) -> Result, Box> { if let Some(data) = data_option { self.room_data = data.clone(); } @@ -269,11 +265,11 @@ impl NCRoomInterface for NCRoom { ) .await .unwrap(); - if self.has_unread() && !response.is_empty() { - self.notifier - .unread_message(&self.room_data.displayName, response.len())?; - } - if !response.is_empty() { + + let is_empty = response.is_empty(); + let update_info = Some((self.room_data.displayName.clone(), response.len())); + + if !is_empty { log::debug!( "Updating {} adding {} new Messages", self.to_string(), @@ -287,8 +283,11 @@ impl NCRoomInterface for NCRoom { .fetch_participants(&self.room_data.token) .await .expect("Failed to fetch room participants"); - - Ok(()) + if self.has_unread() && !is_empty { + Ok(update_info) + } else { + Ok(None) + } } async fn mark_as_read( &self, diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 7b08a68..f93b2e3 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -1,6 +1,5 @@ use crate::{ backend::{ - nc_notify::NCNotify, nc_request::{NCReqDataRoom, NCRequestInterface}, nc_room::NCRoomInterface, }, @@ -30,9 +29,16 @@ pub trait NCBackend: Debug + Send + Default { fn get_dm_keys_display_name_mapping(&self) -> Vec<(Token, String)>; fn get_group_keys_display_name_mapping(&self) -> Vec<(Token, String)>; fn get_room_keys(&self) -> Vec<&'_ Token>; - async fn send_message(&mut self, message: String, token: &Token) -> Result<(), Box>; - async fn select_room(&mut self, token: &Token) -> Result<(), Box>; - async fn update_rooms(&mut self, force_update: bool) -> Result<(), Box>; + async fn send_message( + &mut self, + message: String, + token: &Token, + ) -> Result, Box>; + async fn select_room( + &mut self, + token: &Token, + ) -> Result, Box>; + async fn update_rooms(&mut self, force_update: bool) -> Result, Box>; async fn mark_current_room_as_read( &self, token: &Token, @@ -45,14 +51,12 @@ pub struct NCTalk { chat_data_path: PathBuf, last_requested: i64, requester: Requester, - notifier: NCNotify, } impl NCTalk { async fn parse_response( response: Vec, requester: Requester, - notifier: NCNotify, rooms: &mut HashMap, chat_log_path: PathBuf, ) { @@ -60,7 +64,6 @@ impl NCTalk::new_room( child, requester.clone(), - notifier.clone(), chat_log_path.clone(), )) }); @@ -76,7 +79,6 @@ impl NCTalk, requester: &Requester, - notify: &NCNotify, chat_log_path: &Path, initial_message_ids: &mut HashMap, rooms: &mut HashMap, @@ -88,7 +90,6 @@ impl NCTalk( room.clone(), requester.clone(), - notify.clone(), chat_log_path.to_path_buf(), )), ); @@ -118,20 +119,17 @@ impl NCTalk (Token, Option) { ( packaged_child.token.clone(), - NCRoom::new::(packaged_child, requester_box, notifier, chat_log_path).await, + NCRoom::new::(packaged_child, requester_box, chat_log_path).await, ) } pub async fn new( requester: Requester, config: &Config, ) -> Result, Box> { - let notify = NCNotify::new(config); - let chat_log_path = config.get_server_data_dir(); let mut tmp_path_buf = chat_log_path.clone(); tmp_path_buf.push("Talk.json"); @@ -161,7 +159,6 @@ impl NCTalk NCTalk::parse_response( remaining_room_data, requester.clone(), - notify.clone(), &mut rooms, chat_log_path.clone(), ) @@ -192,7 +188,6 @@ impl NCTalk::parse_response( response, requester.clone(), - notify.clone(), &mut rooms, chat_log_path.clone(), ) @@ -203,7 +198,6 @@ impl NCTalk::parse_response( response, requester.clone(), - notify.clone(), &mut rooms, chat_log_path.clone(), ) @@ -215,7 +209,6 @@ impl NCTalk NCBackend for self.rooms.keys().collect::>() } - async fn send_message(&mut self, message: String, token: &Token) -> Result<(), Box> { + async fn send_message( + &mut self, + message: String, + token: &Token, + ) -> Result, Box> { self.rooms .get(token) .ok_or("Room not found when it should be there")? @@ -338,7 +335,10 @@ impl NCBackend for .await } - async fn select_room(&mut self, token: &Token) -> Result<(), Box> { + async fn select_room( + &mut self, + token: &Token, + ) -> Result, Box> { log::debug!("key {}", token); self.rooms .get_mut(token) @@ -347,7 +347,7 @@ impl NCBackend for .await } - async fn update_rooms(&mut self, force_update: bool) -> Result<(), Box> { + async fn update_rooms(&mut self, force_update: bool) -> Result, Box> { let (response, timestamp) = if force_update { self.requester .fetch_rooms_update(self.last_requested) @@ -356,6 +356,7 @@ impl NCBackend for self.requester.fetch_rooms_initial().await? }; self.last_requested = timestamp; + let mut new_room_token: Vec = vec![]; for room in response { if self.rooms.contains_key(&room.token) { let room_ref = self @@ -376,21 +377,16 @@ impl NCBackend for .await?; } } else { - self.notifier.new_room(&room.displayName)?; + new_room_token.push(room.displayName.clone()); self.rooms.insert( room.token.clone(), - NCRoom::new( - room, - self.requester.clone(), - self.notifier.clone(), - self.chat_data_path.clone(), - ) - .await - .expect("Could not Create Room."), + NCRoom::new(room, self.requester.clone(), self.chat_data_path.clone()) + .await + .expect("Could not Create Room."), ); } } - Ok(()) + Ok(new_room_token) } async fn mark_current_room_as_read( @@ -424,9 +420,9 @@ mock! { fn get_dm_keys_display_name_mapping(&self) -> Vec<(Token, String)>; fn get_group_keys_display_name_mapping(&self) -> Vec<(Token, String)>; fn get_room_keys<'a>(&'a self) -> Vec<&'a Token>; - async fn send_message(& mut self, message: String, token: &Token) -> Result<(), Box>; - async fn select_room(&mut self, token: &Token) -> Result<(), Box>; - async fn update_rooms(& mut self, force_update: bool) -> Result<(), Box>; + async fn send_message(& mut self, message: String, token: &Token) -> Result, Box>; + async fn select_room(&mut self, token: &Token) -> Result, Box>; + async fn update_rooms(& mut self, force_update: bool) -> Result, Box>; async fn mark_current_room_as_read(&self, token: &Token) -> Result<(), Box>; } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 4dad170..2fa5774 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -26,6 +26,8 @@ use crossterm::event::{ }; use tui_textarea::Key; +use super::notifications::NotifyWrapper; + enum ProcessEventResult { Continue, Exit, @@ -52,11 +54,14 @@ pub struct App<'a, Backend: NCBackend> { user_sidebar_visible: bool, default_style: Style, current_room_token: Token, + notify: NotifyWrapper, } impl App<'_, Backend> { pub fn new(backend: Backend, config: &Config) -> Self { let init_room = backend.get_room_by_displayname(config.data.ui.default_room.as_str()); + let notify = NotifyWrapper::new(config); + Self { current_screen: CurrentScreen::Reading, title: TitleBar::new(CurrentScreen::Reading, init_room.clone(), config), @@ -78,6 +83,7 @@ impl App<'_, Backend> { user_sidebar_visible: config.data.ui.user_sidebar_default, default_style: config.theme.default_style(), current_room_token: init_room, + notify, } } @@ -150,7 +156,8 @@ impl App<'_, Backend> { self.backend .mark_current_room_as_read(&self.current_room_token) .await?; - self.backend.update_rooms(true).await?; + self.notify + .maybe_notify_new_rooms(self.backend.update_rooms(true).await?)?; self.update_ui()?; Ok(()) } @@ -169,9 +176,11 @@ impl App<'_, Backend> { if self.input.is_empty() { Ok(()) } else { - self.backend - .send_message(self.input.lines().join("\n"), &self.current_room_token) - .await?; + self.notify.maybe_notify_new_message( + self.backend + .send_message(self.input.lines().join("\n"), &self.current_room_token) + .await?, + )?; self.input.select_all(); self.input.cut(); self.input.select_all(); @@ -190,7 +199,9 @@ impl App<'_, Backend> { .last() .expect("no selection available"), ); - self.backend.select_room(&self.current_room_token).await?; + self.notify.maybe_notify_new_message( + self.backend.select_room(&self.current_room_token).await?, + )?; self.current_screen = CurrentScreen::Reading; self.update_ui()?; self.chat.select_last_message(); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 93590e3..f413c22 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,6 +3,7 @@ pub mod chat_box; pub mod chat_selector; pub mod help_box; pub mod input_box; +pub mod notifications; pub mod terminal_helpers; pub mod title_bar; pub mod users; diff --git a/src/backend/nc_notify.rs b/src/ui/notifications.rs similarity index 79% rename from src/backend/nc_notify.rs rename to src/ui/notifications.rs index aa5a190..bdefcad 100644 --- a/src/backend/nc_notify.rs +++ b/src/ui/notifications.rs @@ -2,15 +2,15 @@ use crate::config::Config; use notify_rust::{Hint, Notification, Timeout}; #[derive(Debug, Clone, Default)] -pub struct NCNotify { +pub struct NotifyWrapper { app_name: String, timeout: Timeout, silent: bool, } -impl NCNotify { +impl NotifyWrapper { pub fn new(config: &Config) -> Self { - NCNotify { + NotifyWrapper { app_name: config.data.general.chat_server_name.clone(), timeout: if config.data.notifications.persistent { Timeout::Never @@ -66,4 +66,24 @@ impl NCNotify { pub fn is_persistent(&self) -> bool { self.timeout == Timeout::Never } + + pub fn maybe_notify_new_message( + &self, + input: Option<(String, usize)>, + ) -> Result<(), Box> { + if let Some((displayname, size)) = input { + self.unread_message(&displayname, size)?; + } + Ok(()) + } + + pub fn maybe_notify_new_rooms( + &self, + input: Vec, + ) -> Result<(), Box> { + for displayname in input { + self.new_room(&displayname)?; + } + Ok(()) + } } From 433372259b4e11704467a1f21f4c38a18b80688f Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Thu, 19 Dec 2024 09:29:34 +0100 Subject: [PATCH 05/16] refactor API requester to be multithreaded --- src/backend/nc_request/mod.rs | 440 +----------------- src/backend/nc_request/nc_req_data_message.rs | 9 +- src/backend/nc_request/nc_req_worker.rs | 356 ++++++++++++++ src/backend/nc_request/nc_requester.rs | 298 ++++++++++++ src/backend/nc_room.rs | 5 +- src/backend/nc_talk.rs | 4 +- src/main.rs | 3 +- 7 files changed, 672 insertions(+), 443 deletions(-) create mode 100644 src/backend/nc_request/nc_req_worker.rs create mode 100644 src/backend/nc_request/nc_requester.rs diff --git a/src/backend/nc_request/mod.rs b/src/backend/nc_request/mod.rs index 45273e7..5635bbb 100644 --- a/src/backend/nc_request/mod.rs +++ b/src/backend/nc_request/mod.rs @@ -5,449 +5,13 @@ mod nc_req_data_message; mod nc_req_data_room; mod nc_req_data_user; +mod nc_req_worker; mod nc_request_ocs_wrapper; +pub mod nc_requester; pub use nc_req_data_message::*; pub use nc_req_data_room::*; pub use nc_req_data_user::*; pub use nc_request_ocs_wrapper::*; -use crate::config::Config; -use async_trait::async_trait; -use base64::{prelude::BASE64_STANDARD, write::EncoderWriter}; -use jzon; -use reqwest::{ - header::{HeaderMap, HeaderValue, AUTHORIZATION}, - Client, Response, Url, -}; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use std::{collections::HashMap, error::Error}; - -#[cfg(test)] -use mockall::{mock, predicate::*}; - pub type Token = String; - -#[async_trait] -pub trait NCRequestInterface: Debug + Send + Clone + Default + Send + Sync { - async fn send_message( - &self, - message: String, - token: &Token, - ) -> Result>; - async fn fetch_autocomplete_users( - &self, - name: &str, - ) -> Result, Box>; - async fn fetch_participants( - &self, - token: &Token, - ) -> Result, Box>; - async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box>; - async fn fetch_rooms_update( - &self, - last_timestamp: i64, - ) -> Result<(Vec, i64), Box>; - async fn fetch_chat_initial( - &self, - token: &Token, - maxMessage: i32, - ) -> Result, Box>; - async fn fetch_chat_update( - &self, - token: &Token, - maxMessage: i32, - last_message: i32, - ) -> Result, Box>; - async fn mark_chat_read(&self, token: &str, last_message: i32) -> Result<(), Box>; -} - -#[derive(Debug, Clone, Default)] -pub struct NCRequest { - base_url: String, - client: Client, - base_headers: HeaderMap, - json_dump_path: Option, -} - -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct NCReqDataMessageParameter { - #[serde(rename = "type")] - param_type: String, - id: String, - name: String, -} - -impl NCRequest { - pub fn new(config: &Config) -> Result> { - use std::io::Write; - - let general = &config.data.general; - - let username = general.user.clone(); - let password = Some(general.app_pw.clone()); - let base_url = general.url.clone(); - - let json_dump_path = config.get_http_dump_dir(); - let mut headers = HeaderMap::new(); - headers.insert("OCS-APIRequest", HeaderValue::from_static("true")); - headers.insert("Accept", HeaderValue::from_static("application/json")); - - let mut buf = b"Basic ".to_vec(); - { - let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD); - write!(encoder, "{username}:").expect("i/o error"); - if let Some(password) = password { - write!(encoder, "{password}").expect("i/o error"); - } - } - let mut auth_value = - HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); - auth_value.set_sensitive(true); - headers.insert(AUTHORIZATION, auth_value); - - // get a client builder - let client = reqwest::Client::builder() - .default_headers(headers.clone()) - .build()?; - - Ok(NCRequest { - base_url: base_url.to_string(), - client, - base_headers: headers, - json_dump_path, - }) - } - - async fn request_rooms( - &self, - last_timestamp: Option, - ) -> Result<(Vec, i64), Box> { - let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v4/room"; - let params = if let Some(timestamp) = last_timestamp { - HashMap::from([("modifiedSince", timestamp.to_string())]) - } else { - HashMap::new() - }; - let url = Url::parse_with_params(&url_string, ¶ms)?; - let response = self.request(url).await?; - match response.status() { - reqwest::StatusCode::OK => { - let timestamp = response - .headers() - .get("X-Nextcloud-Talk-Modified-Before") - .ok_or("Failed to get header")? - .to_str()? - .parse::()?; - let text = response.text().await?; - match serde_json::from_str::>>(&text) { - Ok(parser_response) => Ok((parser_response.ocs.data, timestamp)), - Err(why) => { - self.dump_json_to_log(&url_string, &text)?; - Err(Box::new(why)) - } - } - } - _ => Err(Box::new( - response - .error_for_status() - .err() - .ok_or("Failed to convert Err in reqwest")?, - )), - } - } - - async fn request_chat( - &self, - token: &str, - maxMessage: i32, - last_message: Option, - ) -> Result>, Box> { - let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token; - let params = if let Some(lastId) = last_message { - log::debug!("Last MessageID {}", lastId); - HashMap::from([ - ("limit", maxMessage.to_string()), - ("setReadMarker", "0".into()), - ("lookIntoFuture", "1".into()), - ("lastKnownMessageId", lastId.to_string()), - ("timeout", "0".into()), - ("includeLastKnown", "0".into()), - ]) - } else { - HashMap::from([ - ("limit", maxMessage.to_string()), - ("setReadMarker", "0".into()), - ("lookIntoFuture", "0".into()), - ]) - }; - let url = Url::parse_with_params(&url_string, ¶ms)?; - let response = self.request(url).await?; - match response.status() { - reqwest::StatusCode::OK => { - log::debug!("Got new Messages."); - let text = response.text().await?; - match serde_json::from_str::>>(&text) { - Ok(parser_response) => Ok(Some(parser_response.ocs.data)), - Err(why) => { - self.dump_json_to_log(&url_string, &text)?; - Err(Box::new(why)) - } - } - } - reqwest::StatusCode::NOT_MODIFIED => { - log::debug!("No new Messages."); - Ok(Some(Vec::new())) - } - reqwest::StatusCode::PRECONDITION_FAILED => Ok(None), - _ => { - log::debug!("{} got Err {:?}", token, response); - Err(Box::new( - response - .error_for_status() - .err() - .ok_or("Failed to convert Error")?, - )) - } - } - } - - async fn request_post(&self, url: Url) -> Result { - let builder = self.client.post(url); - builder.send().await - } - - async fn request(&self, url: Url) -> Result { - let builder = self.client.get(url); - builder.send().await - } - - fn dump_json_to_log(&self, url: &str, text: &str) -> Result<(), Box> { - use std::io::Write; - - if let Some(path) = &self.json_dump_path { - let name: String = url - .chars() - .map(|ch| if ch == '/' { '_' } else { ch }) - .collect(); - let mut file = std::fs::File::create(name)?; - let pretty_text = jzon::stringify_pretty(jzon::parse(text)?, 2); - file.write_all(pretty_text.as_bytes())?; - } - Ok(()) - } -} - -#[async_trait] -impl NCRequestInterface for NCRequest { - async fn send_message( - &self, - message: String, - token: &Token, - ) -> Result> { - let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token; - let params = HashMap::from([("message", message)]); - let url = Url::parse_with_params(&url_string, params)?; - let response = self.request_post(url).await?; - - match response.status() { - reqwest::StatusCode::CREATED => Ok(response - .json::>() - .await? - .ocs - .data), - _ => Err(Box::new( - response - .error_for_status() - .err() - .ok_or("Failed to convert Err in reqwest")?, - )), - } - } - - async fn fetch_autocomplete_users( - &self, - name: &str, - ) -> Result, Box> { - let url_string = self.base_url.clone() + "/ocs/v2.php/core/autocomplete/get"; - let params = HashMap::from([("limit", "200"), ("search", name)]); - let url = Url::parse_with_params(&url_string, params)?; - let response = self.request(url).await?; - - match response.status() { - reqwest::StatusCode::OK => { - let text = response.text().await?; - match serde_json::from_str::>>(&text) { - Ok(parser_response) => Ok(parser_response.ocs.data), - Err(why) => { - self.dump_json_to_log(&url_string, &text)?; - log::debug!("{} with {:?}", url_string, why); - Err(Box::new(why)) - } - } - } - _ => Err(Box::new( - response - .error_for_status() - .err() - .ok_or("Failed to convert Err in reqwest")?, - )), - } - } - - async fn fetch_participants( - &self, - token: &Token, - ) -> Result, Box> { - let url_string = self.base_url.clone() - + "/ocs/v2.php/apps/spreed/api/v4/room/" - + token - + "/participants"; - let params = HashMap::from([("includeStatus", "true")]); - let url = Url::parse_with_params(&url_string, params)?; - - let response = self.request(url).await?; - match response.status() { - reqwest::StatusCode::OK => { - let text = response.text().await?; - match serde_json::from_str::>>(&text) { - Ok(parser_response) => Ok(parser_response.ocs.data), - Err(why) => { - self.dump_json_to_log(&url_string, &text)?; - log::debug!("{} with {:?}", url_string, why); - Err(Box::new(why)) - } - } - } - _ => Err(Box::new( - response - .error_for_status() - .err() - .ok_or("Failed to convert Err in reqwest")?, - )), - } - } - - async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box> { - self.request_rooms(None).await - } - - async fn fetch_rooms_update( - &self, - last_timestamp: i64, - ) -> Result<(Vec, i64), Box> { - self.request_rooms(Some(last_timestamp)).await - } - - async fn fetch_chat_initial( - &self, - token: &Token, - maxMessage: i32, - ) -> Result, Box> { - let response_result = self.request_chat(token, maxMessage, None).await; - // Initial results come last to first. And we want the latest message always to be at the end. - match response_result { - Ok(Some(mut response)) => { - response.reverse(); - Ok(response) - } - Ok(None) => Err(String::from("Room disappeared, precondition not met error.").into()), - Err(why) => Err(why), - } - } - - async fn fetch_chat_update( - &self, - token: &Token, - maxMessage: i32, - last_message: i32, - ) -> Result, Box> { - let response_result = self - .request_chat(token, maxMessage, Some(last_message)) - .await; - match response_result { - Ok(Some(response)) => Ok(response), - Ok(None) => Err(String::from("Room disappeared, precondition not met error.").into()), - Err(why) => Err(why), - } - } - - async fn mark_chat_read(&self, token: &str, last_message: i32) -> Result<(), Box> { - let url_string = - self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token + "/read"; - let url = Url::parse(&url_string)?; - log::debug!("Marking {} as read", token); - let response = self.request_post(url).await?; - match response.status() { - reqwest::StatusCode::OK => Ok(()), - _ => Err(Box::new( - response - .error_for_status() - .err() - .ok_or("Failed to convert Error")?, - )), - } - } -} - -#[cfg(test)] -mock! { - #[derive(Debug, Default, Clone)] - pub NCRequest {} // Name of the mock struct, less the "Mock" prefix - - #[async_trait] - impl NCRequestInterface for NCRequest { - async fn send_message( - &self, - message: String, - token: &Token, - ) -> Result>; - async fn fetch_autocomplete_users( - &self, - name: &str, - ) -> Result, Box>; - async fn fetch_participants( - &self, - token: &Token, - ) -> Result, Box>; - async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box>; - async fn fetch_rooms_update( - &self, - last_timestamp: i64, - ) -> Result<(Vec, i64), Box>; - async fn fetch_chat_initial( - &self, - token: &Token, - maxMessage: i32, - ) -> Result, Box>; - async fn fetch_chat_update( - &self, - token: &Token, - maxMessage: i32, - last_message: i32, - ) -> Result, Box>; - async fn mark_chat_read(&self, token: &str, last_message: i32) -> Result<(), Box>; - } - impl Clone for NCRequest { // specification of the trait to mock - fn clone(&self) -> Self; - } -} - -#[cfg(test)] -mod tests { - use crate::config::init; - - use super::*; - - #[tokio::test] - async fn new_requester() { - let dir = tempfile::tempdir().unwrap(); - - std::env::set_var("HOME", dir.path().as_os_str()); - let config = init("./test/").unwrap(); - let result = NCRequest::new(&config); - assert!(result.is_ok()); - let requester = result.unwrap(); - } -} diff --git a/src/backend/nc_request/nc_req_data_message.rs b/src/backend/nc_request/nc_req_data_message.rs index 1b95248..61404c1 100644 --- a/src/backend/nc_request/nc_req_data_message.rs +++ b/src/backend/nc_request/nc_req_data_message.rs @@ -1,7 +1,14 @@ -use super::NCReqDataMessageParameter; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::HashMap; +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct NCReqDataMessageParameter { + #[serde(rename = "type")] + param_type: String, + id: String, + name: String, +} + #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct NCReqDataMessage { pub id: i32, diff --git a/src/backend/nc_request/nc_req_worker.rs b/src/backend/nc_request/nc_req_worker.rs new file mode 100644 index 0000000..f2dff1a --- /dev/null +++ b/src/backend/nc_request/nc_req_worker.rs @@ -0,0 +1,356 @@ +#![allow(non_snake_case)] +#![allow(unused_variables)] +#![allow(dead_code)] + +use crate::config::Config; +use base64::{prelude::BASE64_STANDARD, write::EncoderWriter}; +use jzon; +use reqwest::{ + header::{HeaderMap, HeaderValue, AUTHORIZATION}, + Client, Response, Url, +}; +use std::fmt::Debug; +use std::{collections::HashMap, error::Error}; + +use super::*; + +#[derive(Debug)] +pub struct NCRequestWorker { + base_url: String, + client: Client, + base_headers: HeaderMap, + json_dump_path: Option, +} + +impl NCRequestWorker { + pub fn new(config: &Config) -> Result> { + use std::io::Write; + + let general = &config.data.general; + + let username = general.user.clone(); + let password = Some(general.app_pw.clone()); + let base_url = general.url.clone(); + + let json_dump_path = config.get_http_dump_dir(); + let mut headers = HeaderMap::new(); + headers.insert("OCS-APIRequest", HeaderValue::from_static("true")); + headers.insert("Accept", HeaderValue::from_static("application/json")); + + let mut buf = b"Basic ".to_vec(); + { + let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD); + write!(encoder, "{username}:").expect("i/o error"); + if let Some(password) = password { + write!(encoder, "{password}").expect("i/o error"); + } + } + let mut auth_value = + HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); + auth_value.set_sensitive(true); + headers.insert(AUTHORIZATION, auth_value); + + // get a client builder + let client = reqwest::Client::builder() + .default_headers(headers.clone()) + .build()?; + + log::warn!("Worker Ready"); + + Ok(NCRequestWorker { + base_url: base_url.to_string(), + client, + base_headers: headers, + json_dump_path, + }) + } + + async fn request_rooms( + &self, + last_timestamp: Option, + ) -> Result<(Vec, i64), Box> { + let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v4/room"; + let params = if let Some(timestamp) = last_timestamp { + HashMap::from([("modifiedSince", timestamp.to_string())]) + } else { + HashMap::new() + }; + let url = Url::parse_with_params(&url_string, ¶ms)?; + let response = self.request(url).await?; + match response.status() { + reqwest::StatusCode::OK => { + let timestamp = response + .headers() + .get("X-Nextcloud-Talk-Modified-Before") + .ok_or("Failed to get header")? + .to_str()? + .parse::()?; + let text = response.text().await?; + match serde_json::from_str::>>(&text) { + Ok(parser_response) => Ok((parser_response.ocs.data, timestamp)), + Err(why) => { + self.dump_json_to_log(&url_string, &text)?; + Err(Box::new(why)) + } + } + } + _ => Err(Box::new( + response + .error_for_status() + .err() + .ok_or("Failed to convert Err in reqwest")?, + )), + } + } + + async fn request_chat( + &self, + token: &str, + maxMessage: i32, + last_message: Option, + ) -> Result>, Box> { + let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token; + let params = if let Some(lastId) = last_message { + log::debug!("Last MessageID {}", lastId); + HashMap::from([ + ("limit", maxMessage.to_string()), + ("setReadMarker", "0".into()), + ("lookIntoFuture", "1".into()), + ("lastKnownMessageId", lastId.to_string()), + ("timeout", "0".into()), + ("includeLastKnown", "0".into()), + ]) + } else { + HashMap::from([ + ("limit", maxMessage.to_string()), + ("setReadMarker", "0".into()), + ("lookIntoFuture", "0".into()), + ]) + }; + let url = Url::parse_with_params(&url_string, ¶ms)?; + let response = self.request(url).await?; + match response.status() { + reqwest::StatusCode::OK => { + log::debug!("Got new Messages."); + let text = response.text().await?; + match serde_json::from_str::>>(&text) { + Ok(parser_response) => Ok(Some(parser_response.ocs.data)), + Err(why) => { + self.dump_json_to_log(&url_string, &text)?; + Err(Box::new(why)) + } + } + } + reqwest::StatusCode::NOT_MODIFIED => { + log::debug!("No new Messages."); + Ok(Some(Vec::new())) + } + reqwest::StatusCode::PRECONDITION_FAILED => Ok(None), + _ => { + log::debug!("{} got Err {:?}", token, response); + Err(Box::new( + response + .error_for_status() + .err() + .ok_or("Failed to convert Error")?, + )) + } + } + } + + async fn request_post(&self, url: Url) -> Result { + let builder = self.client.post(url); + builder.send().await + } + + async fn request(&self, url: Url) -> Result { + let builder = self.client.get(url); + builder.send().await + } + + fn dump_json_to_log(&self, url: &str, text: &str) -> Result<(), Box> { + use std::io::Write; + + if let Some(path) = &self.json_dump_path { + let name: String = url + .chars() + .map(|ch| if ch == '/' { '_' } else { ch }) + .collect(); + let mut file = std::fs::File::create(name)?; + let pretty_text = jzon::stringify_pretty(jzon::parse(text)?, 2); + file.write_all(pretty_text.as_bytes())?; + } + Ok(()) + } + + pub async fn send_message( + &self, + message: String, + token: &Token, + ) -> Result> { + let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token; + let params = HashMap::from([("message", message)]); + let url = Url::parse_with_params(&url_string, params)?; + let response = self.request_post(url).await?; + + match response.status() { + reqwest::StatusCode::CREATED => Ok(response + .json::>() + .await? + .ocs + .data), + _ => Err(Box::new( + response + .error_for_status() + .err() + .ok_or("Failed to convert Err in reqwest")?, + )), + } + } + + pub async fn fetch_autocomplete_users( + &self, + name: &str, + ) -> Result, Box> { + let url_string = self.base_url.clone() + "/ocs/v2.php/core/autocomplete/get"; + let params = HashMap::from([("limit", "200"), ("search", name)]); + let url = Url::parse_with_params(&url_string, params)?; + let response = self.request(url).await?; + + match response.status() { + reqwest::StatusCode::OK => { + let text = response.text().await?; + match serde_json::from_str::>>(&text) { + Ok(parser_response) => Ok(parser_response.ocs.data), + Err(why) => { + self.dump_json_to_log(&url_string, &text)?; + log::debug!("{} with {:?}", url_string, why); + Err(Box::new(why)) + } + } + } + _ => Err(Box::new( + response + .error_for_status() + .err() + .ok_or("Failed to convert Err in reqwest")?, + )), + } + } + + pub async fn fetch_participants( + &self, + token: &Token, + ) -> Result, Box> { + let url_string = self.base_url.clone() + + "/ocs/v2.php/apps/spreed/api/v4/room/" + + token + + "/participants"; + let params = HashMap::from([("includeStatus", "true")]); + let url = Url::parse_with_params(&url_string, params)?; + + let response = self.request(url).await?; + match response.status() { + reqwest::StatusCode::OK => { + let text = response.text().await?; + match serde_json::from_str::>>(&text) { + Ok(parser_response) => Ok(parser_response.ocs.data), + Err(why) => { + self.dump_json_to_log(&url_string, &text)?; + log::debug!("{} with {:?}", url_string, why); + Err(Box::new(why)) + } + } + } + _ => Err(Box::new( + response + .error_for_status() + .err() + .ok_or("Failed to convert Err in reqwest")?, + )), + } + } + + pub async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box> { + self.request_rooms(None).await + } + + pub async fn fetch_rooms_update( + &self, + last_timestamp: i64, + ) -> Result<(Vec, i64), Box> { + self.request_rooms(Some(last_timestamp)).await + } + + pub async fn fetch_chat_initial( + &self, + token: &Token, + maxMessage: i32, + ) -> Result, Box> { + let response_result = self.request_chat(token, maxMessage, None).await; + // Initial results come last to first. And we want the latest message always to be at the end. + match response_result { + Ok(Some(mut response)) => { + response.reverse(); + Ok(response) + } + Ok(None) => Err(String::from("Room disappeared, precondition not met error.").into()), + Err(why) => Err(why), + } + } + + pub async fn fetch_chat_update( + &self, + token: &Token, + maxMessage: i32, + last_message: i32, + ) -> Result, Box> { + let response_result = self + .request_chat(token, maxMessage, Some(last_message)) + .await; + match response_result { + Ok(Some(response)) => Ok(response), + Ok(None) => Err(String::from("Room disappeared, precondition not met error.").into()), + Err(why) => Err(why), + } + } + + pub async fn mark_chat_read( + &self, + token: &str, + last_message: i32, + ) -> Result<(), Box> { + let url_string = + self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token + "/read"; + let url = Url::parse(&url_string)?; + log::debug!("Marking {} as read", token); + let response = self.request_post(url).await?; + match response.status() { + reqwest::StatusCode::OK => Ok(()), + _ => Err(Box::new( + response + .error_for_status() + .err() + .ok_or("Failed to convert Error")?, + )), + } + } +} + +#[cfg(test)] +mod tests { + use crate::config::init; + + use super::*; + + #[tokio::test] + async fn new_requester() { + let dir = tempfile::tempdir().unwrap(); + + std::env::set_var("HOME", dir.path().as_os_str()); + let config = init("./test/").unwrap(); + let result = NCRequestWorker::new(&config); + assert!(result.is_ok()); + let requester = result.unwrap(); + } +} diff --git a/src/backend/nc_request/nc_requester.rs b/src/backend/nc_request/nc_requester.rs new file mode 100644 index 0000000..d89bc06 --- /dev/null +++ b/src/backend/nc_request/nc_requester.rs @@ -0,0 +1,298 @@ +use tokio::{ + sync::mpsc::{self, Receiver, Sender}, + time::{sleep, Duration}, +}; + +use crate::config::Config; +use async_trait::async_trait; + +use std::error::Error; +use std::fmt::Debug; + +#[cfg(test)] +use mockall::{mock, predicate::*}; + +use super::{ + nc_req_worker::NCRequestWorker, NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, + NCReqDataUser, Token, +}; + +#[derive(Default)] +pub enum ApiRequests { + #[default] + None, + SendMessage(Token, String), + FetchRoomsInitial, + FetchRoomsUpdate(i64), + FetchParticipants(Token), + FetchChatInitial(Token, i32), + FetchChatUpdate(Token, i32, i32), + FetchAutocompleteUsers(String), + MarkChatRead(Token, i32), +} + +#[async_trait] +pub trait NCRequestInterface: Debug + Send + Send + Sync { + async fn fetch_send_message(&mut self) -> Option; + async fn fetch_autocomplete_users(&mut self) -> Option>; + async fn fetch_participants(&mut self) -> Option>; + async fn fetch_rooms_initial(&mut self) -> Option<(Vec, i64)>; + async fn fetch_rooms_update(&mut self) -> Option<(Vec, i64)>; + async fn fetch_chat_initial(&mut self) -> Option>; + async fn fetch_chat_update(&mut self) -> Option>; + async fn request_send_message( + &self, + message: String, + token: &Token, + ) -> Result<(), Box>; + async fn request_autocomplete_users(&self, name: &str) -> Result<(), Box>; + async fn request_participants(&self, token: &Token) -> Result<(), Box>; + async fn request_rooms_initial(&self) -> Result<(), Box>; + async fn request_rooms_update(&self, last_timestamp: i64) -> Result<(), Box>; + async fn request_chat_initial( + &self, + token: &Token, + maxMessage: i32, + ) -> Result<(), Box>; + async fn request_chat_update( + &self, + token: &Token, + maxMessage: i32, + last_message: i32, + ) -> Result<(), Box>; + async fn request_mark_chat_read( + &self, + token: &str, + last_message: i32, + ) -> Result<(), Box>; +} + +#[derive(Debug)] +pub struct NCRequest { + request_tx: Sender, + rx_fetch_room_initial: Receiver<(Vec, i64)>, + rx_fetch_room_update: Receiver<(Vec, i64)>, + rx_fetch_chat_initial: Receiver>, + rx_fetch_chat_update: Receiver>, + rx_fetch_send_message: Receiver, + rx_fetch_participants: Receiver>, + rx_fetch_autocomplete_users: Receiver>, +} + +impl NCRequest { + pub fn new(config: &Config) -> Result> { + let (tx, mut rx) = mpsc::channel::(50); + let (tx_fetch_room_initial, rx_fetch_room_initial) = + mpsc::channel::<(Vec, i64)>(10); + let (tx_fetch_room_update, rx_fetch_room_update) = + mpsc::channel::<(Vec, i64)>(10); + let (tx_fetch_chat_initial, rx_fetch_chat_initial) = + mpsc::channel::>(10); + let (tx_fetch_chat_update, rx_fetch_chat_update) = + mpsc::channel::>(10); + let (tx_fetch_send_message, rx_fetch_send_message) = mpsc::channel::(10); + let (tx_fetch_participants, rx_fetch_participants) = + mpsc::channel::>(10); + let (tx_fetch_autocomplete_users, rx_fetch_autocomplete_users) = + mpsc::channel::>(10); + + let worker = NCRequestWorker::new(config).unwrap(); + log::warn!("Spawn Now"); + + tokio::spawn(async move { + loop { + if let Some(req) = rx.recv().await { + match req { + ApiRequests::FetchChatInitial(token, maxMessage) => { + tx_fetch_chat_initial + .send(worker.fetch_chat_initial(&token, maxMessage).await.unwrap()) + .await; + } + ApiRequests::FetchChatUpdate(token, maxMessage, last_message) => { + tx_fetch_chat_update + .send( + worker + .fetch_chat_update(&token, maxMessage, last_message) + .await + .unwrap(), + ) + .await; + } + ApiRequests::FetchRoomsInitial => { + tx_fetch_room_initial + .send(worker.fetch_rooms_initial().await.unwrap()) + .await; + log::warn!("Send Room Fetch Request"); + } + _ => { + log::warn!("Unknown Request"); + } + } + }; + sleep(Duration::from_millis(100)).await; + } + }); + log::warn!("Spawn Done"); + + Ok(NCRequest { + request_tx: tx, + rx_fetch_room_initial, + rx_fetch_room_update, + rx_fetch_chat_initial, + rx_fetch_chat_update, + rx_fetch_send_message, + rx_fetch_participants, + rx_fetch_autocomplete_users, + }) + } +} + +#[async_trait] +impl NCRequestInterface for NCRequest { + async fn request_send_message( + &self, + message: String, + token: &Token, + ) -> Result<(), Box> { + self.request_tx + .send(ApiRequests::SendMessage(token.clone(), message)) + .await + .expect("Queing request for sending of message failed."); + Ok(()) + } + + async fn request_rooms_initial(&self) -> Result<(), Box> { + self.request_tx + .send(ApiRequests::FetchRoomsInitial) + .await + .expect("Queing request for sending of message failed."); + Ok(()) + } + async fn request_autocomplete_users(&self, name: &str) -> Result<(), Box> { + self.request_tx + .send(ApiRequests::FetchAutocompleteUsers(name.to_string())) + .await + .expect("Queing request for sending of message failed."); + Ok(()) + } + async fn request_participants(&self, token: &Token) -> Result<(), Box> { + self.request_tx + .send(ApiRequests::FetchParticipants(token.clone())) + .await + .expect("Queing request for sending of message failed."); + Ok(()) + } + async fn request_rooms_update(&self, last_timestamp: i64) -> Result<(), Box> { + self.request_tx + .send(ApiRequests::FetchRoomsUpdate(last_timestamp)) + .await + .expect("Queing request for sending of message failed."); + Ok(()) + } + async fn request_chat_initial( + &self, + token: &Token, + maxMessage: i32, + ) -> Result<(), Box> { + self.request_tx + .send(ApiRequests::FetchChatInitial(token.clone(), maxMessage)) + .await + .expect("Queing request for sending of message failed."); + Ok(()) + } + async fn request_chat_update( + &self, + token: &Token, + maxMessage: i32, + last_message: i32, + ) -> Result<(), Box> { + self.request_tx + .send(ApiRequests::FetchChatUpdate( + token.clone(), + maxMessage, + last_message, + )) + .await + .expect("Queing request for sending of message failed."); + Ok(()) + } + async fn request_mark_chat_read( + &self, + token: &str, + last_message: i32, + ) -> Result<(), Box> { + self.request_tx + .send(ApiRequests::MarkChatRead(token.to_string(), last_message)) + .await + .expect("Queing request for sending of message failed."); + Ok(()) + } + + async fn fetch_send_message(&mut self) -> Option { + self.rx_fetch_send_message.recv().await + } + async fn fetch_rooms_initial(&mut self) -> Option<(Vec, i64)> { + self.rx_fetch_room_initial.recv().await + } + + async fn fetch_autocomplete_users(&mut self) -> Option> { + self.rx_fetch_autocomplete_users.recv().await + } + async fn fetch_participants(&mut self) -> Option> { + self.rx_fetch_participants.recv().await + } + async fn fetch_rooms_update(&mut self) -> Option<(Vec, i64)> { + self.rx_fetch_room_update.recv().await + } + async fn fetch_chat_initial(&mut self) -> Option> { + self.rx_fetch_chat_initial.recv().await + } + async fn fetch_chat_update(&mut self) -> Option> { + self.rx_fetch_chat_update.recv().await + } +} + +#[cfg(test)] +mock! { + #[derive(Debug, Default, Clone)] + pub NCRequest {} // Name of the mock struct, less the "Mock" prefix + + #[async_trait] + impl NCRequestInterface for NCRequest { + async fn fetch_send_message(&mut self) -> Option; + async fn fetch_autocomplete_users(&mut self) -> Option>; + async fn fetch_participants(&mut self) -> Option>; + async fn fetch_rooms_initial(&mut self) -> Option<(Vec, i64)>; + async fn fetch_rooms_update(&mut self) -> Option<(Vec, i64)>; + async fn fetch_chat_initial(&mut self) -> Option>; + async fn fetch_chat_update(&mut self) -> Option>; + async fn request_send_message( + &self, + message: String, + token: &Token, + ) -> Result<(), Box>; + async fn request_autocomplete_users(&self, name: &str) -> Result<(), Box>; + async fn request_participants(&self, token: &Token) -> Result<(), Box>; + async fn request_rooms_initial(&self) -> Result<(), Box>; + async fn request_rooms_update(&self, last_timestamp: i64) -> Result<(), Box>; + async fn request_chat_initial( + &self, + token: &Token, + maxMessage: i32, + ) -> Result<(), Box>; + async fn request_chat_update( + &self, + token: &Token, + maxMessage: i32, + last_message: i32, + ) -> Result<(), Box>; + async fn request_mark_chat_read( + &self, + token: &str, + last_message: i32, + ) -> Result<(), Box>; + } + impl Clone for NCRequest { // specification of the trait to mock + fn clone(&self) -> Self; + } +} diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index c8288db..6ec1eff 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -1,7 +1,8 @@ use super::{ nc_message::NCMessage, nc_request::{ - NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCRequestInterface, Token, + nc_requester::NCRequestInterface, NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, + Token, }, }; use async_trait::async_trait; @@ -108,7 +109,7 @@ impl NCRoom { .ok(); } let participants = requester - .fetch_participants(&room_data.token) + .request_participants_participants(&room_data.token) .await .expect("Failed to fetch room participants"); diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index f93b2e3..e7f1f0f 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -1,6 +1,6 @@ use crate::{ backend::{ - nc_request::{NCReqDataRoom, NCRequestInterface}, + nc_request::{nc_requester::NCRequestInterface, NCReqDataRoom}, nc_room::NCRoomInterface, }, config::Config, @@ -9,9 +9,11 @@ use async_trait::async_trait; use itertools::Itertools; use std::{ collections::HashMap, + default, error::Error, fmt::Debug, path::{Path, PathBuf}, + sync::mpsc, }; use super::{ diff --git a/src/main.rs b/src/main.rs index bd81007..c9ba41f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,8 @@ async fn main() -> Result<(), Box> { log::warn!("Entering Sechat-rs, please be aware this is {pre} SW!"); } - let requester = backend::nc_request::NCRequest::new(&config).expect("cannot create NCRequest"); + let requester = backend::nc_request::nc_requester::NCRequest::new(&config) + .expect("cannot create NCRequest"); let backend = backend::nc_talk::NCTalk::new(requester, &config).await?; let mut ui: ui::app::App<'_, _> = ui::app::App::new(backend, &config); From 1f7b049174e8d8585a271105bbf766d29e7355b3 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Thu, 19 Dec 2024 11:37:41 +0100 Subject: [PATCH 06/16] refactor code to use the new threaded api --- Cargo.toml | 7 +- src/backend/nc_request/nc_req_worker.rs | 4 +- src/backend/nc_request/nc_requester.rs | 325 +++++++------ src/backend/nc_room.rs | 111 +++-- src/backend/nc_talk.rs | 618 +++++++++++++----------- src/main.rs | 3 +- 6 files changed, 591 insertions(+), 477 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cf4b8c4..ccf7637 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,13 @@ exclude = [ "*.log", "tags", ] +[[bin]] +name = "sechat" +path = "src/main.rs" - +# [[bin]] +# name = "sechat-test" +# path = "src/main2.rs" [build-dependencies] cargo-make = "0.37.23" diff --git a/src/backend/nc_request/nc_req_worker.rs b/src/backend/nc_request/nc_req_worker.rs index f2dff1a..0e5e323 100644 --- a/src/backend/nc_request/nc_req_worker.rs +++ b/src/backend/nc_request/nc_req_worker.rs @@ -12,7 +12,9 @@ use reqwest::{ use std::fmt::Debug; use std::{collections::HashMap, error::Error}; -use super::*; +use super::{ + NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCReqDataUser, NCReqOCSWrapper, Token, +}; #[derive(Debug)] pub struct NCRequestWorker { diff --git a/src/backend/nc_request/nc_requester.rs b/src/backend/nc_request/nc_requester.rs index d89bc06..cc485d8 100644 --- a/src/backend/nc_request/nc_requester.rs +++ b/src/backend/nc_request/nc_requester.rs @@ -1,5 +1,8 @@ use tokio::{ - sync::mpsc::{self, Receiver, Sender}, + sync::{ + mpsc::{self, Sender}, + oneshot, + }, time::{sleep, Duration}, }; @@ -21,80 +24,69 @@ use super::{ pub enum ApiRequests { #[default] None, - SendMessage(Token, String), - FetchRoomsInitial, - FetchRoomsUpdate(i64), - FetchParticipants(Token), - FetchChatInitial(Token, i32), - FetchChatUpdate(Token, i32, i32), - FetchAutocompleteUsers(String), - MarkChatRead(Token, i32), + SendMessage(Token, String, oneshot::Sender>), + FetchRoomsInitial(oneshot::Sender, i64)>>), + FetchRoomsUpdate(i64, oneshot::Sender, i64)>>), + FetchParticipants(Token, oneshot::Sender>>), + FetchChatInitial(Token, i32, oneshot::Sender>>), + FetchChatUpdate( + Token, + i32, + i32, + oneshot::Sender>>, + ), + FetchAutocompleteUsers(String, oneshot::Sender>>), + MarkChatRead(Token, i32, oneshot::Sender>), } #[async_trait] pub trait NCRequestInterface: Debug + Send + Send + Sync { - async fn fetch_send_message(&mut self) -> Option; - async fn fetch_autocomplete_users(&mut self) -> Option>; - async fn fetch_participants(&mut self) -> Option>; - async fn fetch_rooms_initial(&mut self) -> Option<(Vec, i64)>; - async fn fetch_rooms_update(&mut self) -> Option<(Vec, i64)>; - async fn fetch_chat_initial(&mut self) -> Option>; - async fn fetch_chat_update(&mut self) -> Option>; async fn request_send_message( &self, message: String, token: &Token, - ) -> Result<(), Box>; - async fn request_autocomplete_users(&self, name: &str) -> Result<(), Box>; - async fn request_participants(&self, token: &Token) -> Result<(), Box>; - async fn request_rooms_initial(&self) -> Result<(), Box>; - async fn request_rooms_update(&self, last_timestamp: i64) -> Result<(), Box>; + ) -> Result>, Box>; + async fn request_autocomplete_users( + &self, + name: &str, + ) -> Result>>, Box>; + async fn request_participants( + &self, + token: &Token, + ) -> Result>>, Box>; + async fn request_rooms_initial( + &self, + ) -> Result, i64)>>, Box>; + async fn request_rooms_update( + &self, + last_timestamp: i64, + ) -> Result, i64)>>, Box>; async fn request_chat_initial( &self, token: &Token, maxMessage: i32, - ) -> Result<(), Box>; + ) -> Result>>, Box>; async fn request_chat_update( &self, token: &Token, maxMessage: i32, last_message: i32, - ) -> Result<(), Box>; + ) -> Result>>, Box>; async fn request_mark_chat_read( &self, token: &str, last_message: i32, - ) -> Result<(), Box>; + ) -> Result>, Box>; } #[derive(Debug)] pub struct NCRequest { request_tx: Sender, - rx_fetch_room_initial: Receiver<(Vec, i64)>, - rx_fetch_room_update: Receiver<(Vec, i64)>, - rx_fetch_chat_initial: Receiver>, - rx_fetch_chat_update: Receiver>, - rx_fetch_send_message: Receiver, - rx_fetch_participants: Receiver>, - rx_fetch_autocomplete_users: Receiver>, } impl NCRequest { - pub fn new(config: &Config) -> Result> { + pub fn new(config: &Config) -> Self { let (tx, mut rx) = mpsc::channel::(50); - let (tx_fetch_room_initial, rx_fetch_room_initial) = - mpsc::channel::<(Vec, i64)>(10); - let (tx_fetch_room_update, rx_fetch_room_update) = - mpsc::channel::<(Vec, i64)>(10); - let (tx_fetch_chat_initial, rx_fetch_chat_initial) = - mpsc::channel::>(10); - let (tx_fetch_chat_update, rx_fetch_chat_update) = - mpsc::channel::>(10); - let (tx_fetch_send_message, rx_fetch_send_message) = mpsc::channel::(10); - let (tx_fetch_participants, rx_fetch_participants) = - mpsc::channel::>(10); - let (tx_fetch_autocomplete_users, rx_fetch_autocomplete_users) = - mpsc::channel::>(10); let worker = NCRequestWorker::new(config).unwrap(); log::warn!("Spawn Now"); @@ -103,28 +95,56 @@ impl NCRequest { loop { if let Some(req) = rx.recv().await { match req { - ApiRequests::FetchChatInitial(token, maxMessage) => { - tx_fetch_chat_initial - .send(worker.fetch_chat_initial(&token, maxMessage).await.unwrap()) - .await; + ApiRequests::FetchChatInitial(token, maxMessage, response) => { + response + .send(Some( + worker.fetch_chat_initial(&token, maxMessage).await.unwrap(), + )) + .expect("could not Send."); } - ApiRequests::FetchChatUpdate(token, maxMessage, last_message) => { - tx_fetch_chat_update - .send( + ApiRequests::FetchChatUpdate(token, maxMessage, last_message, response) => { + response + .send(Some( worker .fetch_chat_update(&token, maxMessage, last_message) .await .unwrap(), - ) - .await; + )) + .expect("could not Send."); } - ApiRequests::FetchRoomsInitial => { - tx_fetch_room_initial - .send(worker.fetch_rooms_initial().await.unwrap()) - .await; + ApiRequests::FetchRoomsInitial(response) => { + response + .send(Some(worker.fetch_rooms_initial().await.unwrap())) + .expect("could not Send."); log::warn!("Send Room Fetch Request"); } - _ => { + ApiRequests::FetchRoomsUpdate(last_timestamp, response) => { + response + .send(Some( + worker.fetch_rooms_update(last_timestamp).await.unwrap(), + )) + .expect("could not Send."); + } + ApiRequests::SendMessage(token, message, response) => { + response + .send(Some(worker.send_message(message, &token).await.unwrap())) + .expect("could not Send."); + } + ApiRequests::FetchAutocompleteUsers(name, response) => { + response + .send(Some(worker.fetch_autocomplete_users(&name).await.unwrap())) + .expect("could not Send."); + } + ApiRequests::FetchParticipants(token, response) => { + response + .send(Some(worker.fetch_participants(&token).await.unwrap())) + .expect("could not Send."); + } + ApiRequests::MarkChatRead(token, last_message, response) => { + worker.mark_chat_read(&token, last_message).await.unwrap(); + response.send(Some(())).expect("could not Send."); + } + ApiRequests::None => { log::warn!("Unknown Request"); } } @@ -134,16 +154,7 @@ impl NCRequest { }); log::warn!("Spawn Done"); - Ok(NCRequest { - request_tx: tx, - rx_fetch_room_initial, - rx_fetch_room_update, - rx_fetch_chat_initial, - rx_fetch_chat_update, - rx_fetch_send_message, - rx_fetch_participants, - rx_fetch_autocomplete_users, - }) + NCRequest { request_tx: tx } } } @@ -153,102 +164,110 @@ impl NCRequestInterface for NCRequest { &self, message: String, token: &Token, - ) -> Result<(), Box> { + ) -> Result>, Box> { + let (tx, rx) = tokio::sync::oneshot::channel(); self.request_tx - .send(ApiRequests::SendMessage(token.clone(), message)) + .send(ApiRequests::SendMessage(token.clone(), message, tx)) .await - .expect("Queing request for sending of message failed."); - Ok(()) + .expect("Queuing request for sending of message failed."); + Ok(rx) } - async fn request_rooms_initial(&self) -> Result<(), Box> { + async fn request_rooms_initial( + &self, + ) -> Result, i64)>>, Box> { + let (tx, rx) = tokio::sync::oneshot::channel(); self.request_tx - .send(ApiRequests::FetchRoomsInitial) + .send(ApiRequests::FetchRoomsInitial(tx)) .await - .expect("Queing request for sending of message failed."); - Ok(()) + .expect("Queuing request for sending of message failed."); + Ok(rx) } - async fn request_autocomplete_users(&self, name: &str) -> Result<(), Box> { + async fn request_autocomplete_users( + &self, + name: &str, + ) -> Result>>, Box> { + let (tx, rx) = tokio::sync::oneshot::channel(); + self.request_tx - .send(ApiRequests::FetchAutocompleteUsers(name.to_string())) + .send(ApiRequests::FetchAutocompleteUsers(name.to_string(), tx)) .await - .expect("Queing request for sending of message failed."); - Ok(()) + .expect("Queuing request for sending of message failed."); + Ok(rx) } - async fn request_participants(&self, token: &Token) -> Result<(), Box> { + async fn request_participants( + &self, + token: &Token, + ) -> Result>>, Box> { + let (tx, rx) = tokio::sync::oneshot::channel(); + self.request_tx - .send(ApiRequests::FetchParticipants(token.clone())) + .send(ApiRequests::FetchParticipants(token.clone(), tx)) .await - .expect("Queing request for sending of message failed."); - Ok(()) + .expect("Queuing request for sending of message failed."); + Ok(rx) } - async fn request_rooms_update(&self, last_timestamp: i64) -> Result<(), Box> { + + async fn request_rooms_update( + &self, + last_timestamp: i64, + ) -> Result, i64)>>, Box> { + let (tx, rx) = tokio::sync::oneshot::channel(); + self.request_tx - .send(ApiRequests::FetchRoomsUpdate(last_timestamp)) + .send(ApiRequests::FetchRoomsUpdate(last_timestamp, tx)) .await - .expect("Queing request for sending of message failed."); - Ok(()) + .expect("Queuing request for sending of message failed."); + Ok(rx) } async fn request_chat_initial( &self, token: &Token, maxMessage: i32, - ) -> Result<(), Box> { + ) -> Result>>, Box> { + let (tx, rx) = tokio::sync::oneshot::channel(); + self.request_tx - .send(ApiRequests::FetchChatInitial(token.clone(), maxMessage)) + .send(ApiRequests::FetchChatInitial(token.clone(), maxMessage, tx)) .await - .expect("Queing request for sending of message failed."); - Ok(()) + .expect("Queuing request for sending of message failed."); + Ok(rx) } async fn request_chat_update( &self, token: &Token, maxMessage: i32, last_message: i32, - ) -> Result<(), Box> { + ) -> Result>>, Box> { + let (tx, rx) = tokio::sync::oneshot::channel(); + self.request_tx .send(ApiRequests::FetchChatUpdate( token.clone(), maxMessage, last_message, + tx, )) .await - .expect("Queing request for sending of message failed."); - Ok(()) + .expect("Queuing request for sending of message failed."); + Ok(rx) } async fn request_mark_chat_read( &self, token: &str, last_message: i32, - ) -> Result<(), Box> { + ) -> Result>, Box> { + let (tx, rx) = tokio::sync::oneshot::channel(); + self.request_tx - .send(ApiRequests::MarkChatRead(token.to_string(), last_message)) + .send(ApiRequests::MarkChatRead( + token.to_string(), + last_message, + tx, + )) .await - .expect("Queing request for sending of message failed."); - Ok(()) - } - - async fn fetch_send_message(&mut self) -> Option { - self.rx_fetch_send_message.recv().await - } - async fn fetch_rooms_initial(&mut self) -> Option<(Vec, i64)> { - self.rx_fetch_room_initial.recv().await - } - - async fn fetch_autocomplete_users(&mut self) -> Option> { - self.rx_fetch_autocomplete_users.recv().await - } - async fn fetch_participants(&mut self) -> Option> { - self.rx_fetch_participants.recv().await - } - async fn fetch_rooms_update(&mut self) -> Option<(Vec, i64)> { - self.rx_fetch_room_update.recv().await - } - async fn fetch_chat_initial(&mut self) -> Option> { - self.rx_fetch_chat_initial.recv().await - } - async fn fetch_chat_update(&mut self) -> Option> { - self.rx_fetch_chat_update.recv().await + .expect("Queuing request for sending of message failed."); + Ok(rx) } } @@ -259,38 +278,42 @@ mock! { #[async_trait] impl NCRequestInterface for NCRequest { - async fn fetch_send_message(&mut self) -> Option; - async fn fetch_autocomplete_users(&mut self) -> Option>; - async fn fetch_participants(&mut self) -> Option>; - async fn fetch_rooms_initial(&mut self) -> Option<(Vec, i64)>; - async fn fetch_rooms_update(&mut self) -> Option<(Vec, i64)>; - async fn fetch_chat_initial(&mut self) -> Option>; - async fn fetch_chat_update(&mut self) -> Option>; async fn request_send_message( &self, message: String, token: &Token, - ) -> Result<(), Box>; - async fn request_autocomplete_users(&self, name: &str) -> Result<(), Box>; - async fn request_participants(&self, token: &Token) -> Result<(), Box>; - async fn request_rooms_initial(&self) -> Result<(), Box>; - async fn request_rooms_update(&self, last_timestamp: i64) -> Result<(), Box>; - async fn request_chat_initial( - &self, - token: &Token, - maxMessage: i32, - ) -> Result<(), Box>; - async fn request_chat_update( - &self, - token: &Token, - maxMessage: i32, - last_message: i32, - ) -> Result<(), Box>; - async fn request_mark_chat_read( - &self, - token: &str, - last_message: i32, - ) -> Result<(), Box>; + ) -> Result>, Box>; + async fn request_autocomplete_users( + &self, + name: &str, + ) -> Result>>, Box>; + async fn request_participants( + &self, + token: &Token, + ) -> Result>>, Box>; + async fn request_rooms_initial( + &self, + ) -> Result, i64)>>, Box>; + async fn request_rooms_update( + &self, + last_timestamp: i64, + ) -> Result, i64)>>, Box>; + async fn request_chat_initial( + &self, + token: &Token, + maxMessage: i32, + ) -> Result>>, Box>; + async fn request_chat_update( + &self, + token: &Token, + maxMessage: i32, + last_message: i32, + ) -> Result>>, Box>; + async fn request_mark_chat_read( + &self, + token: &str, + last_message: i32, + ) -> Result>, Box>; } impl Clone for NCRequest { // specification of the trait to mock fn clone(&self) -> Self; diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 6ec1eff..455d36d 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -10,6 +10,8 @@ use log; use num_derive::FromPrimitive; use num_traits::{AsPrimitive, FromPrimitive}; use std::fmt::{Debug, Display}; +use std::sync::Arc; +use tokio::sync::Mutex; #[derive(Debug, FromPrimitive, PartialEq, Default)] pub enum NCRoomTypes { @@ -49,21 +51,21 @@ pub trait NCRoomInterface: Debug + Send + Display + Ord + Default { &mut self, message_id: i32, data_option: Option, - requester: &Requester, + requester: Arc>, ) -> Result<(), Box>; async fn send( &self, message: String, - requester: &Requester, + requester: Arc>, ) -> Result>; async fn update( &mut self, data_option: Option, - requester: &Requester, + requester: Arc>, ) -> Result, Box>; async fn mark_as_read( &self, - requester: &Requester, + requester: Arc>, ) -> Result<(), Box>; } @@ -79,7 +81,7 @@ pub struct NCRoom { impl NCRoom { pub async fn new( room_data: NCReqDataRoom, - requester: Requester, + requester: Arc>, path_to_log: std::path::PathBuf, ) -> Option { let mut tmp_path_buf = path_to_log.clone(); @@ -98,20 +100,31 @@ impl NCRoom { "Failed to parse json for {}, falling back to fetching", room_data.displayName ); - NCRoom::fetch_messages::(&requester, &room_data.token, &mut messages) - .await - .ok(); + NCRoom::fetch_messages::( + requester.clone(), + &room_data.token, + &mut messages, + ) + .await + .ok(); } } else { log::debug!("No Log File found for room {}", room_data.displayName); - NCRoom::fetch_messages::(&requester, &room_data.token, &mut messages) + NCRoom::fetch_messages::(requester.clone(), &room_data.token, &mut messages) .await .ok(); } - let participants = requester - .request_participants_participants(&room_data.token) + let response_onceshot = requester + .lock() + .await + .request_participants(&room_data.token) .await - .expect("Failed to fetch room participants"); + .unwrap(); + let participants = response_onceshot + .await + .expect("Failed for fetch chat participants") + .ok_or("Failed request") + .unwrap(); Some(NCRoom { messages, @@ -122,11 +135,21 @@ impl NCRoom { }) } async fn fetch_messages( - requester: &Requester, + requester: Arc>, token: &Token, messages: &mut Vec, ) -> Result<(), Box> { - let response = requester.fetch_chat_initial(token, 200).await?; + let response_onceshot = requester + .lock() + .await + .request_chat_initial(token, 200) + .await + .unwrap(); + let response = response_onceshot + .await + .expect("Failed for fetch chat update") + .ok_or("Failed request") + .unwrap(); for message in response { messages.push(message.into()); } @@ -240,32 +263,47 @@ impl NCRoomInterface for NCRoom { async fn send( &self, message: String, - requester: &Requester, + requester: Arc>, ) -> Result> { log::debug!("Send Message {}", &message); - let response = requester.send_message(message, &self.room_data.token).await; + let response_onceshot = requester + .lock() + .await + .request_send_message(message, &self.room_data.token) + .await + .unwrap(); + let response = response_onceshot + .await + .expect("Failed for fetch chat participants"); match response { - Ok(v) => Ok(v.message), - Err(v) => Err(v), + Some(v) => Ok(v.message), + None => Err("Failed to Send Message".into()), } } async fn update( &mut self, data_option: Option, - requester: &Requester, + requester: Arc>, ) -> Result, Box> { if let Some(data) = data_option { self.room_data = data.clone(); } - let response = requester - .fetch_chat_update( + let response_onceshot = requester + .lock() + .await + .request_chat_update( &self.room_data.token, 200, self.messages.last().unwrap().get_id(), ) .await .unwrap(); + let response = response_onceshot + .await + .expect("Failed for fetch chat update") + .ok_or("Failed request") + .unwrap(); let is_empty = response.is_empty(); let update_info = Some((self.room_data.displayName.clone(), response.len())); @@ -280,10 +318,17 @@ impl NCRoomInterface for NCRoom { for message in response { self.messages.push(message.into()); } - self.participants = requester - .fetch_participants(&self.room_data.token) + let response_onceshot = requester + .lock() + .await + .request_participants(&self.room_data.token) .await - .expect("Failed to fetch room participants"); + .unwrap(); + self.participants = response_onceshot + .await + .expect("Failed for fetch chat participants") + .ok_or("Failed request") + .unwrap(); if self.has_unread() && !is_empty { Ok(update_info) } else { @@ -292,15 +337,23 @@ impl NCRoomInterface for NCRoom { } async fn mark_as_read( &self, - requester: &Requester, + requester: Arc>, ) -> Result<(), Box> { if !self.messages.is_empty() { - requester - .mark_chat_read( + let response_onceshot = requester + .lock() + .await + .request_mark_chat_read( &self.room_data.token, self.messages.last().ok_or("No last message")?.get_id(), ) - .await?; + .await + .unwrap(); + response_onceshot + .await + .expect("Failed for fetch chat participants") + .ok_or("Failed request") + .unwrap(); } Ok(()) } @@ -308,7 +361,7 @@ impl NCRoomInterface for NCRoom { &mut self, message_id: i32, data_option: Option, - requester: &Requester, + requester: Arc>, ) -> Result<(), Box> { use std::cmp::Ordering; diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index e7f1f0f..5f5d7a5 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -9,12 +9,12 @@ use async_trait::async_trait; use itertools::Itertools; use std::{ collections::HashMap, - default, error::Error, fmt::Debug, path::{Path, PathBuf}, - sync::mpsc, + sync::Arc, }; +use tokio::sync::Mutex; use super::{ nc_request::Token, @@ -22,7 +22,7 @@ use super::{ }; #[async_trait] -pub trait NCBackend: Debug + Send + Default { +pub trait NCBackend: Debug + Send { type Room: NCRoomInterface; fn write_to_log(&mut self) -> Result<(), std::io::Error>; fn get_room(&self, token: &Token) -> &Self::Room; @@ -52,20 +52,20 @@ pub struct NCTalk { rooms: HashMap, chat_data_path: PathBuf, last_requested: i64, - requester: Requester, + requester: Arc>, } impl NCTalk { async fn parse_response( response: Vec, - requester: Requester, + raw_requester: Arc>, rooms: &mut HashMap, chat_log_path: PathBuf, ) { let v = response.into_iter().map(|child| { tokio::spawn(NCTalk::::new_room( child, - requester.clone(), + Arc::clone(&raw_requester), chat_log_path.clone(), )) }); @@ -80,7 +80,7 @@ impl NCTalk, - requester: &Requester, + requester: Arc>, chat_log_path: &Path, initial_message_ids: &mut HashMap, rooms: &mut HashMap, @@ -91,7 +91,7 @@ impl NCTalk( room.clone(), - requester.clone(), + Arc::clone(&requester), chat_log_path.to_path_buf(), )), ); @@ -105,7 +105,7 @@ impl NCTalk( message_id, Some((*initial_message_ids.get(token).unwrap()).clone()), - requester, + Arc::clone(&requester), ) .await?; rooms.insert(token.clone(), json_room); @@ -120,7 +120,7 @@ impl NCTalk>, chat_log_path: PathBuf, ) -> (Token, Option) { ( @@ -129,7 +129,7 @@ impl NCTalk Result, Box> { let chat_log_path = config.get_server_data_dir(); @@ -138,7 +138,19 @@ impl NCTalk = response @@ -160,7 +172,7 @@ impl NCTalk NCTalk>(); NCTalk::::parse_response( remaining_room_data, - requester.clone(), + Arc::clone(&requester), &mut rooms, chat_log_path.clone(), ) @@ -328,12 +340,12 @@ impl NCBackend for self.rooms .get(token) .ok_or("Room not found when it should be there")? - .send::(message, &self.requester) + .send::(message, Arc::clone(&self.requester)) .await?; self.rooms .get_mut(token) .ok_or("Room not found when it should be there")? - .update::(None, &self.requester) + .update::(None, Arc::clone(&self.requester)) .await } @@ -345,17 +357,35 @@ impl NCBackend for self.rooms .get_mut(token) .ok_or_else(|| format!("Failed to get Room ref for room selection: {token}."))? - .update::(None, &self.requester) + .update::(None, Arc::clone(&self.requester)) .await } async fn update_rooms(&mut self, force_update: bool) -> Result, Box> { let (response, timestamp) = if force_update { - self.requester - .fetch_rooms_update(self.last_requested) - .await? + let resp = self + .requester + .lock() + .await + .request_rooms_update(self.last_requested) + .await + .expect("Initial fetching of rooms on startup failed."); + resp.await + .expect("Initial fetching of rooms failed.") + .ok_or("No rooms found") + .expect("No rooms") } else { - self.requester.fetch_rooms_initial().await? + let resp = self + .requester + .lock() + .await + .request_rooms_initial() + .await + .expect("Initial fetching of rooms on startup failed."); + resp.await + .expect("Initial fetching of rooms failed.") + .ok_or("No rooms found") + .expect("No rooms") }; self.last_requested = timestamp; let mut new_room_token: Vec = vec![]; @@ -367,14 +397,14 @@ impl NCBackend for .ok_or("Failed to get Room ref for update.")?; if force_update { room_ref - .update::(Some(room), &self.requester) + .update::(Some(room), Arc::clone(&self.requester)) .await?; } else { room_ref .update_if_id_is_newer::( room.lastMessage.id, Some(room), - &self.requester, + Arc::clone(&self.requester), ) .await?; } @@ -395,7 +425,9 @@ impl NCBackend for &self, token: &Token, ) -> Result<(), Box> { - self.rooms[token].mark_as_read(&self.requester).await + self.rooms[token] + .mark_as_read(Arc::clone(&self.requester)) + .await } fn get_room(&self, token: &Token) -> &Self::Room { &self.rooms[token] @@ -440,272 +472,272 @@ impl std::fmt::Display for MockNCTalk { } } -#[cfg(test)] -mod tests { - - use mockall::Sequence; - - use super::*; - use crate::{ - backend::nc_request::{ - MockNCRequest, NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, - }, - config::init, - }; - - #[tokio::test] - async fn create_backend() { - let dir = tempfile::tempdir().unwrap(); - - std::env::set_var("HOME", dir.path().as_os_str()); - let config = init("./test/").unwrap(); - let mut mock_requester = crate::backend::nc_request::MockNCRequest::new(); - let mut mock_requester_file = crate::backend::nc_request::MockNCRequest::new(); - let mut mock_requester_fetch = crate::backend::nc_request::MockNCRequest::new(); - let mock_requester_room = crate::backend::nc_request::MockNCRequest::new(); - - let default_room = NCReqDataRoom { - displayName: "General".to_string(), - roomtype: 2, // Group Chat - ..Default::default() - }; - - let default_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 1, - ..Default::default() - }; - let update_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 2, - ..Default::default() - }; - - mock_requester - .expect_fetch_rooms_initial() - .once() - .returning_st(move || Ok((vec![default_room.clone()], 0))); - mock_requester_fetch - .expect_fetch_chat_initial() - .return_once_st(move |_, _| Ok(vec![default_message.clone()])); - mock_requester_fetch - .expect_fetch_participants() - .times(1) - .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); - - mock_requester - .expect_fetch_chat_update() - .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); - - mock_requester - .expect_fetch_participants() - .times(1) - .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); - - mock_requester_file - .expect_clone() - .return_once_st(|| mock_requester_fetch); - - mock_requester - .expect_clone() - .return_once_st(|| mock_requester_file); - - mock_requester - .expect_clone() - .return_once_st(|| mock_requester_room); - - let backend = NCTalk::new(mock_requester, &config) - .await - .expect("Failed to create Backend"); - assert_eq!(backend.rooms.len(), 1); - } - - #[tokio::test] - async fn room_handling() { - let init = init("./test/").unwrap(); - let config = init; - - let mut mock_requester = crate::backend::nc_request::MockNCRequest::new(); - let mut mock_requester_file = crate::backend::nc_request::MockNCRequest::new(); - let mut mock_requester_fetch = crate::backend::nc_request::MockNCRequest::new(); - let mock_requester_room = crate::backend::nc_request::MockNCRequest::new(); - - let default_token = Token::from("123"); - - let default_room = NCReqDataRoom { - displayName: "General".to_string(), - roomtype: 2, // Group Chat - token: default_token.clone(), - ..Default::default() - }; - - let default_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 1, - ..Default::default() - }; - let update_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 2, - ..Default::default() - }; - - let post_send_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 3, - ..Default::default() - }; - - let mut seq = Sequence::new(); - - mock_requester - .expect_fetch_rooms_initial() - .times(2) - .returning_st(move || Ok((vec![default_room.clone()], 0))); - mock_requester_fetch - .expect_fetch_chat_initial() - .return_once_st(move |_, _| Ok(vec![default_message.clone()])); - mock_requester_fetch - .expect_fetch_participants() - .times(1) - .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); - mock_requester - .expect_fetch_participants() - .times(2) - .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); - mock_requester - .expect_fetch_chat_update() - .with(eq(default_token.clone()), eq(200), eq(1)) - .once() - .in_sequence(&mut seq) - .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); - mock_requester - .expect_send_message() - .once() - .in_sequence(&mut seq) - .withf(|message: &String, token: &Token| message == "Test" && *token == "123") - .return_once_st(|_, _| Ok(NCReqDataMessage::default())); - - mock_requester - .expect_fetch_chat_update() - .once() - .with(eq(Token::from("123")), eq(200), eq(2)) - .in_sequence(&mut seq) - .return_once_st(move |_, _, _| Ok(vec![post_send_message.clone()])); - - mock_requester_file - .expect_clone() - .return_once_st(|| mock_requester_fetch); - - mock_requester - .expect_clone() - .return_once_st(|| mock_requester_file); - - mock_requester - .expect_clone() - .return_once_st(|| mock_requester_room); - - let backend = NCTalk::new(mock_requester, &config) - .await - .expect("Failed to create Backend"); - - check_results(backend).await; - } - - async fn check_results(mut backend: NCTalk) { - assert!(backend - .send_message("Test".to_owned(), &Token::from("123")) - .await - .is_ok()); - - assert!(backend.update_rooms(false).await.is_ok()); - - assert_eq!( - backend.get_room(&"123".into()).to_token(), - Token::from("123") - ); - assert_eq!(backend.get_unread_rooms().len(), 0); - assert_eq!( - backend.get_room_by_displayname("General"), - Token::from("123") - ); - assert_eq!(backend.get_dm_keys_display_name_mapping(), vec![]); - assert_eq!( - backend.get_group_keys_display_name_mapping(), - vec![("123".into(), "General".to_string())] - ); - assert_eq!(backend.get_room_keys(), vec![&Token::from("123")]); - } - - #[tokio::test] - async fn write_to_log() { - let dir = tempfile::tempdir().unwrap(); - - std::env::set_var("HOME", dir.path().as_os_str()); - let config = init("./test/").unwrap(); - - println!("Path is {}", config.get_data_dir().display()); - - let mut mock_requester = crate::backend::nc_request::MockNCRequest::new(); - let mut mock_requester_file = crate::backend::nc_request::MockNCRequest::new(); - let mut mock_requester_fetch = crate::backend::nc_request::MockNCRequest::new(); - let mock_requester_room = crate::backend::nc_request::MockNCRequest::new(); - - let default_room = NCReqDataRoom { - displayName: "General".to_string(), - token: "a123".into(), - roomtype: 2, // Group Chat - ..Default::default() - }; - - let default_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 1, - ..Default::default() - }; - let update_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 2, - ..Default::default() - }; - - mock_requester - .expect_fetch_rooms_initial() - .once() - .returning_st(move || Ok((vec![default_room.clone()], 0))); - mock_requester_fetch - .expect_fetch_chat_initial() - .return_once_st(move |_, _| Ok(vec![default_message.clone()])); - mock_requester_fetch - .expect_fetch_participants() - .times(1) - .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); - mock_requester - .expect_fetch_participants() - .times(1) - .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); - mock_requester - .expect_fetch_chat_update() - .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); - - mock_requester_file - .expect_clone() - .return_once_st(|| mock_requester_fetch); - - mock_requester - .expect_clone() - .return_once_st(|| mock_requester_file); - - mock_requester - .expect_clone() - .return_once_st(|| mock_requester_room); - - let mut backend = NCTalk::new(mock_requester, &config) - .await - .expect("Failed to create Backend"); - assert_eq!(backend.rooms.len(), 1); - - backend.write_to_log().unwrap(); - dir.close().unwrap(); - } -} +// #[cfg(test)] +// mod tests { + +// use mockall::Sequence; + +// use super::*; +// use crate::{ +// backend::nc_request::{ +// nc_requester::MockNCRequest, NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, +// }, +// config::init, +// }; + +// #[tokio::test] +// async fn create_backend() { +// let dir = tempfile::tempdir().unwrap(); + +// std::env::set_var("HOME", dir.path().as_os_str()); +// let config = init("./test/").unwrap(); +// let mut mock_requester = MockNCRequest::new(); +// let mut mock_requester_file = MockNCRequest::new(); +// let mut mock_requester_fetch = MockNCRequest::new(); +// let mock_requester_room = MockNCRequest::new(); + +// let default_room = NCReqDataRoom { +// displayName: "General".to_string(), +// roomtype: 2, // Group Chat +// ..Default::default() +// }; + +// let default_message = NCReqDataMessage { +// messageType: "comment".to_string(), +// id: 1, +// ..Default::default() +// }; +// let update_message = NCReqDataMessage { +// messageType: "comment".to_string(), +// id: 2, +// ..Default::default() +// }; + +// mock_requester +// .expect_fetch_rooms_initial() +// .once() +// .returning_st(move || Ok((vec![default_room.clone()], 0))); +// mock_requester_fetch +// .expect_fetch_chat_initial() +// .return_once_st(move |_, _| Ok(vec![default_message.clone()])); +// mock_requester_fetch +// .expect_fetch_participants() +// .times(1) +// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); + +// mock_requester +// .expect_fetch_chat_update() +// .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); + +// mock_requester +// .expect_fetch_participants() +// .times(1) +// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); + +// mock_requester_file +// .expect_clone() +// .return_once_st(|| mock_requester_fetch); + +// mock_requester +// .expect_clone() +// .return_once_st(|| mock_requester_file); + +// mock_requester +// .expect_clone() +// .return_once_st(|| mock_requester_room); + +// let backend = NCTalk::new(mock_requester, &config) +// .await +// .expect("Failed to create Backend"); +// assert_eq!(backend.rooms.len(), 1); +// } + +// #[tokio::test] +// async fn room_handling() { +// let init = init("./test/").unwrap(); +// let config = init; + +// let mut mock_requester = MockNCRequest::new(); +// let mut mock_requester_file = MockNCRequest::new(); +// let mut mock_requester_fetch = MockNCRequest::new(); +// let mock_requester_room = MockNCRequest::new(); + +// let default_token = Token::from("123"); + +// let default_room = NCReqDataRoom { +// displayName: "General".to_string(), +// roomtype: 2, // Group Chat +// token: default_token.clone(), +// ..Default::default() +// }; + +// let default_message = NCReqDataMessage { +// messageType: "comment".to_string(), +// id: 1, +// ..Default::default() +// }; +// let update_message = NCReqDataMessage { +// messageType: "comment".to_string(), +// id: 2, +// ..Default::default() +// }; + +// let post_send_message = NCReqDataMessage { +// messageType: "comment".to_string(), +// id: 3, +// ..Default::default() +// }; + +// let mut seq = Sequence::new(); + +// mock_requester +// .expect_fetch_rooms_initial() +// .times(2) +// .returning_st(move || Ok((vec![default_room.clone()], 0))); +// mock_requester_fetch +// .expect_fetch_chat_initial() +// .return_once_st(move |_, _| Ok(vec![default_message.clone()])); +// mock_requester_fetch +// .expect_fetch_participants() +// .times(1) +// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); +// mock_requester +// .expect_fetch_participants() +// .times(2) +// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); +// mock_requester +// .expect_fetch_chat_update() +// .with(eq(default_token.clone()), eq(200), eq(1)) +// .once() +// .in_sequence(&mut seq) +// .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); +// mock_requester +// .expect_send_message() +// .once() +// .in_sequence(&mut seq) +// .withf(|message: &String, token: &Token| message == "Test" && *token == "123") +// .return_once_st(|_, _| Ok(NCReqDataMessage::default())); + +// mock_requester +// .expect_fetch_chat_update() +// .once() +// .with(eq(Token::from("123")), eq(200), eq(2)) +// .in_sequence(&mut seq) +// .return_once_st(move |_, _, _| Ok(vec![post_send_message.clone()])); + +// mock_requester_file +// .expect_clone() +// .return_once_st(|| mock_requester_fetch); + +// mock_requester +// .expect_clone() +// .return_once_st(|| mock_requester_file); + +// mock_requester +// .expect_clone() +// .return_once_st(|| mock_requester_room); + +// let backend = NCTalk::new(mock_requester, &config) +// .await +// .expect("Failed to create Backend"); + +// check_results(backend).await; +// } + +// async fn check_results(mut backend: NCTalk) { +// assert!(backend +// .send_message("Test".to_owned(), &Token::from("123")) +// .await +// .is_ok()); + +// assert!(backend.update_rooms(false).await.is_ok()); + +// assert_eq!( +// backend.get_room(&"123".into()).to_token(), +// Token::from("123") +// ); +// assert_eq!(backend.get_unread_rooms().len(), 0); +// assert_eq!( +// backend.get_room_by_displayname("General"), +// Token::from("123") +// ); +// assert_eq!(backend.get_dm_keys_display_name_mapping(), vec![]); +// assert_eq!( +// backend.get_group_keys_display_name_mapping(), +// vec![("123".into(), "General".to_string())] +// ); +// assert_eq!(backend.get_room_keys(), vec![&Token::from("123")]); +// } + +// #[tokio::test] +// async fn write_to_log() { +// let dir = tempfile::tempdir().unwrap(); + +// std::env::set_var("HOME", dir.path().as_os_str()); +// let config = init("./test/").unwrap(); + +// println!("Path is {}", config.get_data_dir().display()); + +// let mut mock_requester = MockNCRequest::new(); +// let mut mock_requester_file = MockNCRequest::new(); +// let mut mock_requester_fetch = MockNCRequest::new(); +// let mock_requester_room = MockNCRequest::new(); + +// let default_room = NCReqDataRoom { +// displayName: "General".to_string(), +// token: "a123".into(), +// roomtype: 2, // Group Chat +// ..Default::default() +// }; + +// let default_message = NCReqDataMessage { +// messageType: "comment".to_string(), +// id: 1, +// ..Default::default() +// }; +// let update_message = NCReqDataMessage { +// messageType: "comment".to_string(), +// id: 2, +// ..Default::default() +// }; + +// mock_requester +// .expect_fetch_rooms_initial() +// .once() +// .returning_st(move || Ok((vec![default_room.clone()], 0))); +// mock_requester_fetch +// .expect_fetch_chat_initial() +// .return_once_st(move |_, _| Ok(vec![default_message.clone()])); +// mock_requester_fetch +// .expect_fetch_participants() +// .times(1) +// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); +// mock_requester +// .expect_fetch_participants() +// .times(1) +// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); +// mock_requester +// .expect_fetch_chat_update() +// .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); + +// mock_requester_file +// .expect_clone() +// .return_once_st(|| mock_requester_fetch); + +// mock_requester +// .expect_clone() +// .return_once_st(|| mock_requester_file); + +// mock_requester +// .expect_clone() +// .return_once_st(|| mock_requester_room); + +// let mut backend = NCTalk::new(mock_requester, &config) +// .await +// .expect("Failed to create Backend"); +// assert_eq!(backend.rooms.len(), 1); + +// backend.write_to_log().unwrap(); +// dir.close().unwrap(); +// } +// } diff --git a/src/main.rs b/src/main.rs index c9ba41f..10febf4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,8 +26,7 @@ async fn main() -> Result<(), Box> { log::warn!("Entering Sechat-rs, please be aware this is {pre} SW!"); } - let requester = backend::nc_request::nc_requester::NCRequest::new(&config) - .expect("cannot create NCRequest"); + let requester = backend::nc_request::nc_requester::NCRequest::new(&config); let backend = backend::nc_talk::NCTalk::new(requester, &config).await?; let mut ui: ui::app::App<'_, _> = ui::app::App::new(backend, &config); From 78ed61ede515dce249ae3a30583cef754cc31aa9 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Thu, 19 Dec 2024 12:06:44 +0100 Subject: [PATCH 07/16] further refactoring --- src/backend/nc_request/nc_req_worker.rs | 2 +- src/backend/nc_request/nc_requester.rs | 43 ++++++++++++++++++------- src/backend/nc_room.rs | 13 +------- src/config/mod.rs | 2 +- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/backend/nc_request/nc_req_worker.rs b/src/backend/nc_request/nc_req_worker.rs index 0e5e323..ec683fd 100644 --- a/src/backend/nc_request/nc_req_worker.rs +++ b/src/backend/nc_request/nc_req_worker.rs @@ -57,7 +57,7 @@ impl NCRequestWorker { .default_headers(headers.clone()) .build()?; - log::warn!("Worker Ready"); + log::info!("Worker Ready {}", base_url.to_string()); Ok(NCRequestWorker { base_url: base_url.to_string(), diff --git a/src/backend/nc_request/nc_requester.rs b/src/backend/nc_request/nc_requester.rs index cc485d8..3efb47e 100644 --- a/src/backend/nc_request/nc_requester.rs +++ b/src/backend/nc_request/nc_requester.rs @@ -1,16 +1,13 @@ -use tokio::{ - sync::{ - mpsc::{self, Sender}, - oneshot, - }, - time::{sleep, Duration}, +use tokio::sync::{ + mpsc::{self, Sender}, + oneshot, }; use crate::config::Config; use async_trait::async_trait; -use std::error::Error; use std::fmt::Debug; +use std::{error::Error, fmt}; #[cfg(test)] use mockall::{mock, predicate::*}; @@ -20,7 +17,7 @@ use super::{ NCReqDataUser, Token, }; -#[derive(Default)] +#[derive(Default, Debug)] pub enum ApiRequests { #[default] None, @@ -39,6 +36,30 @@ pub enum ApiRequests { MarkChatRead(Token, i32, oneshot::Sender>), } +impl fmt::Display for ApiRequests { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ApiRequests::None => write!(f, "Invalid"), + ApiRequests::SendMessage(token, _, _) => write!(f, "SendMessage {token}"), + ApiRequests::FetchRoomsInitial(_) => write!(f, "FetchRoomsInitial"), + ApiRequests::FetchRoomsUpdate(last_timestamp, _) => { + write!(f, "FetchRoomsUpdate {last_timestamp}") + } + ApiRequests::FetchParticipants(token, _) => write!(f, "FetchParticipants {token}"), + ApiRequests::FetchChatInitial(token, maxMessage, _) => { + write!(f, "FetchChatInitial {token} {maxMessage}") + } + ApiRequests::FetchChatUpdate(token, maxMessage, last_message, _) => { + write!(f, "FetchChatUpdate {token} {maxMessage} {last_message}") + } + ApiRequests::FetchAutocompleteUsers(name, _) => { + write!(f, "FetchAutocompleteUsers {name}") + } + ApiRequests::MarkChatRead(token, i32, _) => write!(f, "MarkChatRead {token}"), + } + } +} + #[async_trait] pub trait NCRequestInterface: Debug + Send + Send + Sync { async fn request_send_message( @@ -89,11 +110,11 @@ impl NCRequest { let (tx, mut rx) = mpsc::channel::(50); let worker = NCRequestWorker::new(config).unwrap(); - log::warn!("Spawn Now"); tokio::spawn(async move { loop { if let Some(req) = rx.recv().await { + log::debug!("got a new API Request {}", req); match req { ApiRequests::FetchChatInitial(token, maxMessage, response) => { response @@ -116,7 +137,6 @@ impl NCRequest { response .send(Some(worker.fetch_rooms_initial().await.unwrap())) .expect("could not Send."); - log::warn!("Send Room Fetch Request"); } ApiRequests::FetchRoomsUpdate(last_timestamp, response) => { response @@ -149,10 +169,9 @@ impl NCRequest { } } }; - sleep(Duration::from_millis(100)).await; } }); - log::warn!("Spawn Done"); + log::info!("Spawned API Thread"); NCRequest { request_tx: tx } } diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 455d36d..4f22ca1 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -114,23 +114,12 @@ impl NCRoom { .await .ok(); } - let response_onceshot = requester - .lock() - .await - .request_participants(&room_data.token) - .await - .unwrap(); - let participants = response_onceshot - .await - .expect("Failed for fetch chat participants") - .ok_or("Failed request") - .unwrap(); Some(NCRoom { messages, path_to_log: tmp_path_buf, room_type: FromPrimitive::from_i32(room_data.roomtype).unwrap(), - participants, + participants: vec![], room_data, }) } diff --git a/src/config/mod.rs b/src/config/mod.rs index 9ca9ced..efd61eb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -179,7 +179,7 @@ impl Config { let log_file = FileAppender::builder() // Pattern: https://docs.rs/log4rs/*/log4rs/encode/pattern/index.html .encoder(Box::new(PatternEncoder::new( - "{d(%H:%M:%S)} {l} {M}: {m}{n}", + "{d(%H:%M:%S%.3f)} {l} {i} {M}: {m}{n}", ))) .append(false) .build(log_path) From 683a9fb4440a465c3a4f8b6f420e1073c84dcd09 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Thu, 19 Dec 2024 15:00:21 +0100 Subject: [PATCH 08/16] make requester use a thread pool to distribute the requests --- src/backend/nc_request/nc_requester.rs | 134 +++++++++++++++---------- 1 file changed, 79 insertions(+), 55 deletions(-) diff --git a/src/backend/nc_request/nc_requester.rs b/src/backend/nc_request/nc_requester.rs index 3efb47e..3f91aac 100644 --- a/src/backend/nc_request/nc_requester.rs +++ b/src/backend/nc_request/nc_requester.rs @@ -106,68 +106,92 @@ pub struct NCRequest { } impl NCRequest { + async fn handle_req(worker: &NCRequestWorker, req: ApiRequests) { + log::debug!("got a new API Request {}", req); + match req { + ApiRequests::FetchChatInitial(token, maxMessage, response) => { + response + .send(Some( + worker.fetch_chat_initial(&token, maxMessage).await.unwrap(), + )) + .expect("could not Send."); + } + ApiRequests::FetchChatUpdate(token, maxMessage, last_message, response) => { + response + .send(Some( + worker + .fetch_chat_update(&token, maxMessage, last_message) + .await + .unwrap(), + )) + .expect("could not Send."); + } + ApiRequests::FetchRoomsInitial(response) => { + response + .send(Some(worker.fetch_rooms_initial().await.unwrap())) + .expect("could not Send."); + } + ApiRequests::FetchRoomsUpdate(last_timestamp, response) => { + response + .send(Some( + worker.fetch_rooms_update(last_timestamp).await.unwrap(), + )) + .expect("could not Send."); + } + ApiRequests::SendMessage(token, message, response) => { + response + .send(Some(worker.send_message(message, &token).await.unwrap())) + .expect("could not Send."); + } + ApiRequests::FetchAutocompleteUsers(name, response) => { + response + .send(Some(worker.fetch_autocomplete_users(&name).await.unwrap())) + .expect("could not Send."); + } + ApiRequests::FetchParticipants(token, response) => { + response + .send(Some(worker.fetch_participants(&token).await.unwrap())) + .expect("could not Send."); + } + ApiRequests::MarkChatRead(token, last_message, response) => { + worker.mark_chat_read(&token, last_message).await.unwrap(); + response.send(Some(())).expect("could not Send."); + } + ApiRequests::None => { + log::warn!("Unknown Request"); + } + } + } pub fn new(config: &Config) -> Self { let (tx, mut rx) = mpsc::channel::(50); - let worker = NCRequestWorker::new(config).unwrap(); + let mut worker_queue = vec![]; + + for i in 1..6 { + let (tx_worker, mut rx_worker) = mpsc::channel::(10); + + worker_queue.push(tx_worker); + let worker = NCRequestWorker::new(config).unwrap(); + + tokio::spawn(async move { + loop { + if let Some(req) = rx_worker.recv().await { + NCRequest::handle_req(&worker, req).await; + }; + } + }); + } tokio::spawn(async move { loop { if let Some(req) = rx.recv().await { - log::debug!("got a new API Request {}", req); - match req { - ApiRequests::FetchChatInitial(token, maxMessage, response) => { - response - .send(Some( - worker.fetch_chat_initial(&token, maxMessage).await.unwrap(), - )) - .expect("could not Send."); - } - ApiRequests::FetchChatUpdate(token, maxMessage, last_message, response) => { - response - .send(Some( - worker - .fetch_chat_update(&token, maxMessage, last_message) - .await - .unwrap(), - )) - .expect("could not Send."); - } - ApiRequests::FetchRoomsInitial(response) => { - response - .send(Some(worker.fetch_rooms_initial().await.unwrap())) - .expect("could not Send."); - } - ApiRequests::FetchRoomsUpdate(last_timestamp, response) => { - response - .send(Some( - worker.fetch_rooms_update(last_timestamp).await.unwrap(), - )) - .expect("could not Send."); - } - ApiRequests::SendMessage(token, message, response) => { - response - .send(Some(worker.send_message(message, &token).await.unwrap())) - .expect("could not Send."); - } - ApiRequests::FetchAutocompleteUsers(name, response) => { - response - .send(Some(worker.fetch_autocomplete_users(&name).await.unwrap())) - .expect("could not Send."); - } - ApiRequests::FetchParticipants(token, response) => { - response - .send(Some(worker.fetch_participants(&token).await.unwrap())) - .expect("could not Send."); - } - ApiRequests::MarkChatRead(token, last_message, response) => { - worker.mark_chat_read(&token, last_message).await.unwrap(); - response.send(Some(())).expect("could not Send."); - } - ApiRequests::None => { - log::warn!("Unknown Request"); - } - } + worker_queue.sort_by_key(tokio::sync::mpsc::Sender::capacity); + worker_queue + .first() + .expect("No Thread?") + .send(req) + .await + .expect("Failed to fwd request to worker."); }; } }); From 28f65dad17681ce99657e610a971ce7df0543d26 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Thu, 19 Dec 2024 16:31:12 +0100 Subject: [PATCH 09/16] optimize worker queue --- src/backend/nc_request/nc_requester.rs | 26 +++++++- src/backend/nc_room.rs | 85 +++++++++++++++----------- src/backend/nc_talk.rs | 65 +++++++++++--------- 3 files changed, 108 insertions(+), 68 deletions(-) diff --git a/src/backend/nc_request/nc_requester.rs b/src/backend/nc_request/nc_requester.rs index 3f91aac..73d675d 100644 --- a/src/backend/nc_request/nc_requester.rs +++ b/src/backend/nc_request/nc_requester.rs @@ -184,15 +184,35 @@ impl NCRequest { tokio::spawn(async move { loop { - if let Some(req) = rx.recv().await { + let mut buffer: Vec = vec![]; + let added = rx.recv_many(&mut buffer, 5).await; + log::debug!("got {} requests to API", added); + + if added == 0 { + buffer.push(rx.recv().await.expect("Failed to get message")); + } + + while worker_queue + .first() + .expect("No Element in worker queue") + .capacity() + < 5 + { worker_queue.sort_by_key(tokio::sync::mpsc::Sender::capacity); + } + log::debug!( + "Capacity of first {} and last {} worker", + worker_queue.first().unwrap().capacity(), + worker_queue.last().unwrap().capacity() + ); + for message in buffer { worker_queue .first() .expect("No Thread?") - .send(req) + .send(message) .await .expect("Failed to fwd request to worker."); - }; + } } }); log::info!("Spawned API Thread"); diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 4f22ca1..c386445 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -128,12 +128,14 @@ impl NCRoom { token: &Token, messages: &mut Vec, ) -> Result<(), Box> { - let response_onceshot = requester - .lock() - .await - .request_chat_initial(token, 200) - .await - .unwrap(); + let response_onceshot = { + requester + .lock() + .await + .request_chat_initial(token, 200) + .await + .unwrap() + }; let response = response_onceshot .await .expect("Failed for fetch chat update") @@ -255,12 +257,14 @@ impl NCRoomInterface for NCRoom { requester: Arc>, ) -> Result> { log::debug!("Send Message {}", &message); - let response_onceshot = requester - .lock() - .await - .request_send_message(message, &self.room_data.token) - .await - .unwrap(); + let response_onceshot = { + requester + .lock() + .await + .request_send_message(message, &self.room_data.token) + .await + .unwrap() + }; let response = response_onceshot .await .expect("Failed for fetch chat participants"); @@ -278,16 +282,18 @@ impl NCRoomInterface for NCRoom { if let Some(data) = data_option { self.room_data = data.clone(); } - let response_onceshot = requester - .lock() - .await - .request_chat_update( - &self.room_data.token, - 200, - self.messages.last().unwrap().get_id(), - ) - .await - .unwrap(); + let response_onceshot = { + requester + .lock() + .await + .request_chat_update( + &self.room_data.token, + 200, + self.messages.last().unwrap().get_id(), + ) + .await + .unwrap() + }; let response = response_onceshot .await .expect("Failed for fetch chat update") @@ -307,12 +313,15 @@ impl NCRoomInterface for NCRoom { for message in response { self.messages.push(message.into()); } - let response_onceshot = requester - .lock() - .await - .request_participants(&self.room_data.token) - .await - .unwrap(); + let response_onceshot = { + requester + .lock() + .await + .request_participants(&self.room_data.token) + .await + .unwrap() + }; + self.participants = response_onceshot .await .expect("Failed for fetch chat participants") @@ -329,15 +338,17 @@ impl NCRoomInterface for NCRoom { requester: Arc>, ) -> Result<(), Box> { if !self.messages.is_empty() { - let response_onceshot = requester - .lock() - .await - .request_mark_chat_read( - &self.room_data.token, - self.messages.last().ok_or("No last message")?.get_id(), - ) - .await - .unwrap(); + let response_onceshot = { + requester + .lock() + .await + .request_mark_chat_read( + &self.room_data.token, + self.messages.last().ok_or("No last message")?.get_id(), + ) + .await + .unwrap() + }; response_onceshot .await .expect("Failed for fetch chat participants") diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 5f5d7a5..0f2324f 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -14,7 +14,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use tokio::sync::Mutex; +use tokio::{sync::Mutex, task::JoinHandle}; use super::{ nc_request::Token, @@ -62,13 +62,17 @@ impl NCTalk, chat_log_path: PathBuf, ) { - let v = response.into_iter().map(|child| { - tokio::spawn(NCTalk::::new_room( - child, - Arc::clone(&raw_requester), - chat_log_path.clone(), - )) - }); + let v: Vec)>> = response + .into_iter() + .map(|child| { + tokio::spawn(NCTalk::::new_room( + child, + Arc::clone(&raw_requester), + chat_log_path.clone(), + )) + }) + .collect(); + log::debug!("Got {} initial threads", v.len()); for jh in v { let (name, room_option) = jh.await.unwrap(); if let Some(room) = room_option { @@ -96,6 +100,7 @@ impl NCTalk NCTalk NCBackend for async fn update_rooms(&mut self, force_update: bool) -> Result, Box> { let (response, timestamp) = if force_update { - let resp = self - .requester - .lock() - .await - .request_rooms_update(self.last_requested) - .await - .expect("Initial fetching of rooms on startup failed."); + let resp = { + self.requester + .lock() + .await + .request_rooms_update(self.last_requested) + .await + .expect("Initial fetching of rooms on startup failed.") + }; resp.await .expect("Initial fetching of rooms failed.") .ok_or("No rooms found") .expect("No rooms") } else { - let resp = self - .requester - .lock() - .await - .request_rooms_initial() - .await - .expect("Initial fetching of rooms on startup failed."); + let resp = { + self.requester + .lock() + .await + .request_rooms_initial() + .await + .expect("Initial fetching of rooms on startup failed.") + }; resp.await .expect("Initial fetching of rooms failed.") .ok_or("No rooms found") From abeb09b3278ec3e50ac3c3250cc0ed9aa75393a8 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Thu, 19 Dec 2024 17:16:53 +0100 Subject: [PATCH 10/16] fix tests --- src/backend/nc_room.rs | 2 +- src/backend/nc_talk.rs | 567 ++++++++++++++++++++++------------------- 2 files changed, 299 insertions(+), 270 deletions(-) diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index c386445..849e3b7 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -377,7 +377,7 @@ impl NCRoomInterface for NCRoom { self.update(data_option, requester).await?; } Ordering::Less => { - log::warn!( + log::info!( "Message Id was older than message stored '{}'! Stored {} Upstream {}", self.to_string(), last_internal_id, diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 0f2324f..fb9085f 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -481,272 +481,301 @@ impl std::fmt::Display for MockNCTalk { } } -// #[cfg(test)] -// mod tests { - -// use mockall::Sequence; - -// use super::*; -// use crate::{ -// backend::nc_request::{ -// nc_requester::MockNCRequest, NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, -// }, -// config::init, -// }; - -// #[tokio::test] -// async fn create_backend() { -// let dir = tempfile::tempdir().unwrap(); - -// std::env::set_var("HOME", dir.path().as_os_str()); -// let config = init("./test/").unwrap(); -// let mut mock_requester = MockNCRequest::new(); -// let mut mock_requester_file = MockNCRequest::new(); -// let mut mock_requester_fetch = MockNCRequest::new(); -// let mock_requester_room = MockNCRequest::new(); - -// let default_room = NCReqDataRoom { -// displayName: "General".to_string(), -// roomtype: 2, // Group Chat -// ..Default::default() -// }; - -// let default_message = NCReqDataMessage { -// messageType: "comment".to_string(), -// id: 1, -// ..Default::default() -// }; -// let update_message = NCReqDataMessage { -// messageType: "comment".to_string(), -// id: 2, -// ..Default::default() -// }; - -// mock_requester -// .expect_fetch_rooms_initial() -// .once() -// .returning_st(move || Ok((vec![default_room.clone()], 0))); -// mock_requester_fetch -// .expect_fetch_chat_initial() -// .return_once_st(move |_, _| Ok(vec![default_message.clone()])); -// mock_requester_fetch -// .expect_fetch_participants() -// .times(1) -// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); - -// mock_requester -// .expect_fetch_chat_update() -// .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); - -// mock_requester -// .expect_fetch_participants() -// .times(1) -// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); - -// mock_requester_file -// .expect_clone() -// .return_once_st(|| mock_requester_fetch); - -// mock_requester -// .expect_clone() -// .return_once_st(|| mock_requester_file); - -// mock_requester -// .expect_clone() -// .return_once_st(|| mock_requester_room); - -// let backend = NCTalk::new(mock_requester, &config) -// .await -// .expect("Failed to create Backend"); -// assert_eq!(backend.rooms.len(), 1); -// } - -// #[tokio::test] -// async fn room_handling() { -// let init = init("./test/").unwrap(); -// let config = init; - -// let mut mock_requester = MockNCRequest::new(); -// let mut mock_requester_file = MockNCRequest::new(); -// let mut mock_requester_fetch = MockNCRequest::new(); -// let mock_requester_room = MockNCRequest::new(); - -// let default_token = Token::from("123"); - -// let default_room = NCReqDataRoom { -// displayName: "General".to_string(), -// roomtype: 2, // Group Chat -// token: default_token.clone(), -// ..Default::default() -// }; - -// let default_message = NCReqDataMessage { -// messageType: "comment".to_string(), -// id: 1, -// ..Default::default() -// }; -// let update_message = NCReqDataMessage { -// messageType: "comment".to_string(), -// id: 2, -// ..Default::default() -// }; - -// let post_send_message = NCReqDataMessage { -// messageType: "comment".to_string(), -// id: 3, -// ..Default::default() -// }; - -// let mut seq = Sequence::new(); - -// mock_requester -// .expect_fetch_rooms_initial() -// .times(2) -// .returning_st(move || Ok((vec![default_room.clone()], 0))); -// mock_requester_fetch -// .expect_fetch_chat_initial() -// .return_once_st(move |_, _| Ok(vec![default_message.clone()])); -// mock_requester_fetch -// .expect_fetch_participants() -// .times(1) -// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); -// mock_requester -// .expect_fetch_participants() -// .times(2) -// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); -// mock_requester -// .expect_fetch_chat_update() -// .with(eq(default_token.clone()), eq(200), eq(1)) -// .once() -// .in_sequence(&mut seq) -// .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); -// mock_requester -// .expect_send_message() -// .once() -// .in_sequence(&mut seq) -// .withf(|message: &String, token: &Token| message == "Test" && *token == "123") -// .return_once_st(|_, _| Ok(NCReqDataMessage::default())); - -// mock_requester -// .expect_fetch_chat_update() -// .once() -// .with(eq(Token::from("123")), eq(200), eq(2)) -// .in_sequence(&mut seq) -// .return_once_st(move |_, _, _| Ok(vec![post_send_message.clone()])); - -// mock_requester_file -// .expect_clone() -// .return_once_st(|| mock_requester_fetch); - -// mock_requester -// .expect_clone() -// .return_once_st(|| mock_requester_file); - -// mock_requester -// .expect_clone() -// .return_once_st(|| mock_requester_room); - -// let backend = NCTalk::new(mock_requester, &config) -// .await -// .expect("Failed to create Backend"); - -// check_results(backend).await; -// } - -// async fn check_results(mut backend: NCTalk) { -// assert!(backend -// .send_message("Test".to_owned(), &Token::from("123")) -// .await -// .is_ok()); - -// assert!(backend.update_rooms(false).await.is_ok()); - -// assert_eq!( -// backend.get_room(&"123".into()).to_token(), -// Token::from("123") -// ); -// assert_eq!(backend.get_unread_rooms().len(), 0); -// assert_eq!( -// backend.get_room_by_displayname("General"), -// Token::from("123") -// ); -// assert_eq!(backend.get_dm_keys_display_name_mapping(), vec![]); -// assert_eq!( -// backend.get_group_keys_display_name_mapping(), -// vec![("123".into(), "General".to_string())] -// ); -// assert_eq!(backend.get_room_keys(), vec![&Token::from("123")]); -// } - -// #[tokio::test] -// async fn write_to_log() { -// let dir = tempfile::tempdir().unwrap(); - -// std::env::set_var("HOME", dir.path().as_os_str()); -// let config = init("./test/").unwrap(); - -// println!("Path is {}", config.get_data_dir().display()); - -// let mut mock_requester = MockNCRequest::new(); -// let mut mock_requester_file = MockNCRequest::new(); -// let mut mock_requester_fetch = MockNCRequest::new(); -// let mock_requester_room = MockNCRequest::new(); - -// let default_room = NCReqDataRoom { -// displayName: "General".to_string(), -// token: "a123".into(), -// roomtype: 2, // Group Chat -// ..Default::default() -// }; - -// let default_message = NCReqDataMessage { -// messageType: "comment".to_string(), -// id: 1, -// ..Default::default() -// }; -// let update_message = NCReqDataMessage { -// messageType: "comment".to_string(), -// id: 2, -// ..Default::default() -// }; - -// mock_requester -// .expect_fetch_rooms_initial() -// .once() -// .returning_st(move || Ok((vec![default_room.clone()], 0))); -// mock_requester_fetch -// .expect_fetch_chat_initial() -// .return_once_st(move |_, _| Ok(vec![default_message.clone()])); -// mock_requester_fetch -// .expect_fetch_participants() -// .times(1) -// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); -// mock_requester -// .expect_fetch_participants() -// .times(1) -// .returning_st(move |_| Ok(vec![NCReqDataParticipants::default()])); -// mock_requester -// .expect_fetch_chat_update() -// .return_once_st(move |_, _, _| Ok(vec![update_message.clone()])); - -// mock_requester_file -// .expect_clone() -// .return_once_st(|| mock_requester_fetch); - -// mock_requester -// .expect_clone() -// .return_once_st(|| mock_requester_file); - -// mock_requester -// .expect_clone() -// .return_once_st(|| mock_requester_room); - -// let mut backend = NCTalk::new(mock_requester, &config) -// .await -// .expect("Failed to create Backend"); -// assert_eq!(backend.rooms.len(), 1); - -// backend.write_to_log().unwrap(); -// dir.close().unwrap(); -// } -// } +#[cfg(test)] +mod tests { + + use mockall::Sequence; + + use super::*; + use crate::{ + backend::nc_request::{ + nc_requester::MockNCRequest, NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, + }, + config::init, + }; + + #[tokio::test] + async fn create_backend() { + let dir = tempfile::tempdir().unwrap(); + + std::env::set_var("HOME", dir.path().as_os_str()); + let config = init("./test/").unwrap(); + let mut mock_requester = MockNCRequest::new(); + + let (tx, rx) = tokio::sync::oneshot::channel(); + let (chat_tx, chat_rx) = tokio::sync::oneshot::channel(); + let (update_tx, update_rx) = tokio::sync::oneshot::channel(); + let (pat_tx, pat_rx) = tokio::sync::oneshot::channel(); + + let default_room = NCReqDataRoom { + displayName: "General".to_string(), + token: "a123".into(), + roomtype: 2, // Group Chat + ..Default::default() + }; + tx.send(Some((vec![default_room], 1))) + .expect("Sending Failed."); + + let default_message = NCReqDataMessage { + messageType: "comment".to_string(), + id: 1, + ..Default::default() + }; + + chat_tx + .send(Some(vec![default_message.clone()])) + .expect("Sending Failed."); + + let update_message = NCReqDataMessage { + messageType: "comment".to_string(), + id: 2, + ..Default::default() + }; + update_tx + .send(Some(vec![update_message.clone()])) + .expect("Sending Failed."); + + pat_tx + .send(Some(vec![NCReqDataParticipants::default()])) + .expect("Sending Failed."); + + mock_requester + .expect_request_rooms_initial() + .once() + .return_once(move || Ok(rx)); + mock_requester + .expect_request_chat_initial() + .return_once(move |_, _| Ok(chat_rx)); + + mock_requester + .expect_request_participants() + .times(1) + .return_once(move |_| Ok(pat_rx)); + mock_requester + .expect_request_chat_update() + .return_once_st(move |_, _, _| Ok(update_rx)); + + let backend = NCTalk::new(mock_requester, &config) + .await + .expect("Failed to create Backend"); + assert_eq!(backend.rooms.len(), 1); + } + + #[tokio::test] + async fn room_handling() { + let init = init("./test/").unwrap(); + let config = init; + + let mut mock_requester = MockNCRequest::new(); + + let (tx, rx) = tokio::sync::oneshot::channel(); + let (tx2, rx2) = tokio::sync::oneshot::channel(); + let (chat_tx, chat_rx) = tokio::sync::oneshot::channel(); + let (update_tx, update_rx) = tokio::sync::oneshot::channel(); + let (pat_tx, pat_rx) = tokio::sync::oneshot::channel(); + let (pat2_tx, pat2_rx) = tokio::sync::oneshot::channel(); + let (send_tx, send_rx) = tokio::sync::oneshot::channel(); + let (chat_update_tx, chat_update_rx) = tokio::sync::oneshot::channel(); + + let default_token = Token::from("123"); + + let default_room = NCReqDataRoom { + displayName: "General".to_string(), + roomtype: 2, // Group Chat + token: default_token.clone(), + ..Default::default() + }; + + let default_message = NCReqDataMessage { + messageType: "comment".to_string(), + id: 1, + ..Default::default() + }; + let update_message = NCReqDataMessage { + messageType: "comment".to_string(), + id: 2, + ..Default::default() + }; + + let post_send_message = NCReqDataMessage { + messageType: "comment".to_string(), + id: 3, + ..Default::default() + }; + + let mut seq = Sequence::new(); + + tx.send(Some((vec![default_room.clone()], 1))) + .expect("Sending Failed."); + tx2.send(Some((vec![default_room.clone()], 1))) + .expect("Sending Failed."); + + chat_tx + .send(Some(vec![default_message.clone()])) + .expect("Sending Failed."); + + update_tx + .send(Some(vec![update_message.clone()])) + .expect("Sending Failed."); + + pat_tx + .send(Some(vec![NCReqDataParticipants::default()])) + .expect("Sending Failed."); + pat2_tx + .send(Some(vec![NCReqDataParticipants::default()])) + .expect("Sending Failed."); + + send_tx + .send(Some(NCReqDataMessage::default())) + .expect("Sending Failed"); + + chat_update_tx + .send(Some(vec![post_send_message.clone()])) + .expect("Failed to send"); + + mock_requester + .expect_request_rooms_initial() + .once() + .return_once(move || Ok(rx)); + mock_requester + .expect_request_rooms_initial() + .once() + .return_once(move || Ok(rx2)); + mock_requester + .expect_request_chat_initial() + .return_once(move |_, _| Ok(chat_rx)); + + mock_requester + .expect_request_participants() + .times(1) + .return_once(move |_| Ok(pat_rx)); + mock_requester + .expect_request_chat_update() + .once() + .in_sequence(&mut seq) + .return_once(move |_, _, _| Ok(update_rx)); + + mock_requester + .expect_request_send_message() + .once() + .in_sequence(&mut seq) + .withf(|message: &String, token: &Token| message == "Test" && *token == "123") + .return_once(|_, _| Ok(send_rx)); + + mock_requester + .expect_request_chat_update() + .once() + .with(eq(Token::from("123")), eq(200), eq(2)) + .in_sequence(&mut seq) + .return_once(move |_, _, _| Ok(chat_update_rx)); + + mock_requester + .expect_request_participants() + .times(1) + .return_once(move |_| Ok(pat2_rx)); + + let backend = NCTalk::new(mock_requester, &config) + .await + .expect("Failed to create Backend"); + + check_results(backend).await; + } + + async fn check_results(mut backend: NCTalk) { + assert!(backend + .send_message("Test".to_owned(), &Token::from("123")) + .await + .is_ok()); + + assert!(backend.update_rooms(false).await.is_ok()); + + assert_eq!( + backend.get_room(&"123".into()).to_token(), + Token::from("123") + ); + assert_eq!(backend.get_unread_rooms().len(), 0); + assert_eq!( + backend.get_room_by_displayname("General"), + Token::from("123") + ); + assert_eq!(backend.get_dm_keys_display_name_mapping(), vec![]); + assert_eq!( + backend.get_group_keys_display_name_mapping(), + vec![("123".into(), "General".to_string())] + ); + assert_eq!(backend.get_room_keys(), vec![&Token::from("123")]); + } + + #[tokio::test] + async fn write_to_log() { + let dir = tempfile::tempdir().unwrap(); + + std::env::set_var("HOME", dir.path().as_os_str()); + let config = init("./test/").unwrap(); + + println!("Path is {}", config.get_data_dir().display()); + + let mut mock_requester = MockNCRequest::new(); + + let (tx, rx) = tokio::sync::oneshot::channel(); + let (chat_tx, chat_rx) = tokio::sync::oneshot::channel(); + let (update_tx, update_rx) = tokio::sync::oneshot::channel(); + let (pat_tx, pat_rx) = tokio::sync::oneshot::channel(); + + let default_room = NCReqDataRoom { + displayName: "General".to_string(), + token: "a123".into(), + roomtype: 2, // Group Chat + ..Default::default() + }; + tx.send(Some((vec![default_room], 1))) + .expect("Sending Failed."); + + let default_message = NCReqDataMessage { + messageType: "comment".to_string(), + id: 1, + ..Default::default() + }; + + chat_tx + .send(Some(vec![default_message.clone()])) + .expect("Sending Failed."); + + let update_message = NCReqDataMessage { + messageType: "comment".to_string(), + id: 2, + ..Default::default() + }; + update_tx + .send(Some(vec![update_message.clone()])) + .expect("Sending Failed."); + + pat_tx + .send(Some(vec![NCReqDataParticipants::default()])) + .expect("Sending Failed."); + + mock_requester + .expect_request_rooms_initial() + .once() + .return_once(move || Ok(rx)); + mock_requester + .expect_request_chat_initial() + .return_once(move |_, _| Ok(chat_rx)); + + mock_requester + .expect_request_participants() + .times(1) + .return_once(move |_| Ok(pat_rx)); + mock_requester + .expect_request_chat_update() + .return_once_st(move |_, _, _| Ok(update_rx)); + + let mut backend = NCTalk::new(mock_requester, &config) + .await + .expect("Failed to create Backend"); + assert_eq!(backend.rooms.len(), 1); + + backend.write_to_log().unwrap(); + dir.close().unwrap(); + } +} From 7c65402cec591ed0e3011b2353eea25b9741d184 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Thu, 19 Dec 2024 17:48:47 +0100 Subject: [PATCH 11/16] introduce type --- src/backend/nc_request/nc_requester.rs | 154 +++++++++++-------------- 1 file changed, 67 insertions(+), 87 deletions(-) diff --git a/src/backend/nc_request/nc_requester.rs b/src/backend/nc_request/nc_requester.rs index 73d675d..46cd196 100644 --- a/src/backend/nc_request/nc_requester.rs +++ b/src/backend/nc_request/nc_requester.rs @@ -21,19 +21,36 @@ use super::{ pub enum ApiRequests { #[default] None, - SendMessage(Token, String, oneshot::Sender>), - FetchRoomsInitial(oneshot::Sender, i64)>>), - FetchRoomsUpdate(i64, oneshot::Sender, i64)>>), - FetchParticipants(Token, oneshot::Sender>>), - FetchChatInitial(Token, i32, oneshot::Sender>>), + SendMessage( + Token, + String, + oneshot::Sender>>, + ), + FetchRoomsInitial(oneshot::Sender, i64), Box>>), + FetchRoomsUpdate( + i64, + oneshot::Sender, i64), Box>>, + ), + FetchParticipants( + Token, + oneshot::Sender, Box>>, + ), + FetchChatInitial( + Token, + i32, + oneshot::Sender, Box>>, + ), FetchChatUpdate( Token, i32, i32, - oneshot::Sender>>, + oneshot::Sender, Box>>, + ), + FetchAutocompleteUsers( + String, + oneshot::Sender, Box>>, ), - FetchAutocompleteUsers(String, oneshot::Sender>>), - MarkChatRead(Token, i32, oneshot::Sender>), + MarkChatRead(Token, i32, oneshot::Sender>>), } impl fmt::Display for ApiRequests { @@ -60,44 +77,35 @@ impl fmt::Display for ApiRequests { } } +type ApiResult = + Result>>, Box>; + #[async_trait] pub trait NCRequestInterface: Debug + Send + Send + Sync { async fn request_send_message( &self, message: String, token: &Token, - ) -> Result>, Box>; - async fn request_autocomplete_users( - &self, - name: &str, - ) -> Result>>, Box>; - async fn request_participants( - &self, - token: &Token, - ) -> Result>>, Box>; - async fn request_rooms_initial( - &self, - ) -> Result, i64)>>, Box>; + ) -> ApiResult; + async fn request_autocomplete_users(&self, name: &str) -> ApiResult>; + async fn request_participants(&self, token: &Token) -> ApiResult>; + async fn request_rooms_initial(&self) -> ApiResult<(Vec, i64)>; async fn request_rooms_update( &self, last_timestamp: i64, - ) -> Result, i64)>>, Box>; + ) -> ApiResult<(Vec, i64)>; async fn request_chat_initial( &self, token: &Token, maxMessage: i32, - ) -> Result>>, Box>; + ) -> ApiResult>; async fn request_chat_update( &self, token: &Token, maxMessage: i32, last_message: i32, - ) -> Result>>, Box>; - async fn request_mark_chat_read( - &self, - token: &str, - last_message: i32, - ) -> Result>, Box>; + ) -> ApiResult>; + async fn request_mark_chat_read(&self, token: &str, last_message: i32) -> ApiResult<()>; } #[derive(Debug)] @@ -111,51 +119,48 @@ impl NCRequest { match req { ApiRequests::FetchChatInitial(token, maxMessage, response) => { response - .send(Some( - worker.fetch_chat_initial(&token, maxMessage).await.unwrap(), - )) + .send(Ok(worker + .fetch_chat_initial(&token, maxMessage) + .await + .unwrap())) .expect("could not Send."); } ApiRequests::FetchChatUpdate(token, maxMessage, last_message, response) => { response - .send(Some( - worker - .fetch_chat_update(&token, maxMessage, last_message) - .await - .unwrap(), - )) + .send(Ok(worker + .fetch_chat_update(&token, maxMessage, last_message) + .await + .unwrap())) .expect("could not Send."); } ApiRequests::FetchRoomsInitial(response) => { response - .send(Some(worker.fetch_rooms_initial().await.unwrap())) + .send(Ok(worker.fetch_rooms_initial().await.unwrap())) .expect("could not Send."); } ApiRequests::FetchRoomsUpdate(last_timestamp, response) => { response - .send(Some( - worker.fetch_rooms_update(last_timestamp).await.unwrap(), - )) + .send(Ok(worker.fetch_rooms_update(last_timestamp).await.unwrap())) .expect("could not Send."); } ApiRequests::SendMessage(token, message, response) => { response - .send(Some(worker.send_message(message, &token).await.unwrap())) + .send(Ok(worker.send_message(message, &token).await.unwrap())) .expect("could not Send."); } ApiRequests::FetchAutocompleteUsers(name, response) => { response - .send(Some(worker.fetch_autocomplete_users(&name).await.unwrap())) + .send(Ok(worker.fetch_autocomplete_users(&name).await.unwrap())) .expect("could not Send."); } ApiRequests::FetchParticipants(token, response) => { response - .send(Some(worker.fetch_participants(&token).await.unwrap())) + .send(Ok(worker.fetch_participants(&token).await.unwrap())) .expect("could not Send."); } ApiRequests::MarkChatRead(token, last_message, response) => { worker.mark_chat_read(&token, last_message).await.unwrap(); - response.send(Some(())).expect("could not Send."); + response.send(Ok(())).expect("could not Send."); } ApiRequests::None => { log::warn!("Unknown Request"); @@ -175,7 +180,7 @@ impl NCRequest { tokio::spawn(async move { loop { - if let Some(req) = rx_worker.recv().await { + if let Ok(req) = rx_worker.recv().await { NCRequest::handle_req(&worker, req).await; }; } @@ -227,7 +232,7 @@ impl NCRequestInterface for NCRequest { &self, message: String, token: &Token, - ) -> Result>, Box> { + ) -> ApiResult { let (tx, rx) = tokio::sync::oneshot::channel(); self.request_tx .send(ApiRequests::SendMessage(token.clone(), message, tx)) @@ -235,10 +240,7 @@ impl NCRequestInterface for NCRequest { .expect("Queuing request for sending of message failed."); Ok(rx) } - - async fn request_rooms_initial( - &self, - ) -> Result, i64)>>, Box> { + async fn request_rooms_initial(&self) -> ApiResult<(Vec, i64)> { let (tx, rx) = tokio::sync::oneshot::channel(); self.request_tx .send(ApiRequests::FetchRoomsInitial(tx)) @@ -246,10 +248,7 @@ impl NCRequestInterface for NCRequest { .expect("Queuing request for sending of message failed."); Ok(rx) } - async fn request_autocomplete_users( - &self, - name: &str, - ) -> Result>>, Box> { + async fn request_autocomplete_users(&self, name: &str) -> ApiResult> { let (tx, rx) = tokio::sync::oneshot::channel(); self.request_tx @@ -258,10 +257,7 @@ impl NCRequestInterface for NCRequest { .expect("Queuing request for sending of message failed."); Ok(rx) } - async fn request_participants( - &self, - token: &Token, - ) -> Result>>, Box> { + async fn request_participants(&self, token: &Token) -> ApiResult> { let (tx, rx) = tokio::sync::oneshot::channel(); self.request_tx @@ -274,7 +270,7 @@ impl NCRequestInterface for NCRequest { async fn request_rooms_update( &self, last_timestamp: i64, - ) -> Result, i64)>>, Box> { + ) -> ApiResult<(Vec, i64)> { let (tx, rx) = tokio::sync::oneshot::channel(); self.request_tx @@ -283,11 +279,7 @@ impl NCRequestInterface for NCRequest { .expect("Queuing request for sending of message failed."); Ok(rx) } - async fn request_chat_initial( - &self, - token: &Token, - maxMessage: i32, - ) -> Result>>, Box> { + async fn request_chat_initial(&self, token: &Token, maxMessage: i32) -> ApiResult<()> { let (tx, rx) = tokio::sync::oneshot::channel(); self.request_tx @@ -301,7 +293,7 @@ impl NCRequestInterface for NCRequest { token: &Token, maxMessage: i32, last_message: i32, - ) -> Result>>, Box> { + ) -> ApiResult> { let (tx, rx) = tokio::sync::oneshot::channel(); self.request_tx @@ -342,41 +334,29 @@ mock! { #[async_trait] impl NCRequestInterface for NCRequest { async fn request_send_message( - &self, - message: String, - token: &Token, - ) -> Result>, Box>; - async fn request_autocomplete_users( - &self, - name: &str, - ) -> Result>>, Box>; - async fn request_participants( &self, + message: String, token: &Token, - ) -> Result>>, Box>; - async fn request_rooms_initial( - &self, - ) -> Result, i64)>>, Box>; + ) -> ApiResult; + async fn request_autocomplete_users(&self, name: &str) -> ApiResult>; + async fn request_participants(&self, token: &Token) -> ApiResult>; + async fn request_rooms_initial(&self) -> ApiResult<(Vec, i64)>; async fn request_rooms_update( &self, last_timestamp: i64, - ) -> Result, i64)>>, Box>; + ) -> ApiResult<(Vec, i64)>; async fn request_chat_initial( &self, token: &Token, maxMessage: i32, - ) -> Result>>, Box>; + ) -> ApiResult>; async fn request_chat_update( &self, token: &Token, maxMessage: i32, last_message: i32, - ) -> Result>>, Box>; - async fn request_mark_chat_read( - &self, - token: &str, - last_message: i32, - ) -> Result>, Box>; + ) -> ApiResult>; + async fn request_mark_chat_read(&self, token: &str, last_message: i32) -> ApiResult<()>; } impl Clone for NCRequest { // specification of the trait to mock fn clone(&self) -> Self; From 4dfae37bc180ba846224be52d913636583727788 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 20 Dec 2024 08:47:40 +0100 Subject: [PATCH 12/16] refactor requester with new type defs and returning results --- src/backend/nc_request/nc_requester.rs | 61 +++++++++----------------- src/backend/nc_room.rs | 16 +++---- src/backend/nc_talk.rs | 41 ++++++++--------- 3 files changed, 45 insertions(+), 73 deletions(-) diff --git a/src/backend/nc_request/nc_requester.rs b/src/backend/nc_request/nc_requester.rs index 46cd196..4396b48 100644 --- a/src/backend/nc_request/nc_requester.rs +++ b/src/backend/nc_request/nc_requester.rs @@ -6,8 +6,8 @@ use tokio::sync::{ use crate::config::Config; use async_trait::async_trait; -use std::fmt::Debug; use std::{error::Error, fmt}; +use std::{fmt::Debug, sync::Arc}; #[cfg(test)] use mockall::{mock, predicate::*}; @@ -17,40 +17,22 @@ use super::{ NCReqDataUser, Token, }; +type ApiResult = + Result>>, Box>; +type ApiResponseChannel = oneshot::Sender>>; + #[derive(Default, Debug)] pub enum ApiRequests { #[default] None, - SendMessage( - Token, - String, - oneshot::Sender>>, - ), - FetchRoomsInitial(oneshot::Sender, i64), Box>>), - FetchRoomsUpdate( - i64, - oneshot::Sender, i64), Box>>, - ), - FetchParticipants( - Token, - oneshot::Sender, Box>>, - ), - FetchChatInitial( - Token, - i32, - oneshot::Sender, Box>>, - ), - FetchChatUpdate( - Token, - i32, - i32, - oneshot::Sender, Box>>, - ), - FetchAutocompleteUsers( - String, - oneshot::Sender, Box>>, - ), - MarkChatRead(Token, i32, oneshot::Sender>>), + SendMessage(Token, String, ApiResponseChannel), + FetchRoomsInitial(ApiResponseChannel<(Vec, i64)>), + FetchRoomsUpdate(i64, ApiResponseChannel<(Vec, i64)>), + FetchParticipants(Token, ApiResponseChannel>), + FetchChatInitial(Token, i32, ApiResponseChannel>), + FetchChatUpdate(Token, i32, i32, ApiResponseChannel>), + FetchAutocompleteUsers(String, ApiResponseChannel>), + MarkChatRead(Token, i32, ApiResponseChannel<()>), } impl fmt::Display for ApiRequests { @@ -77,9 +59,6 @@ impl fmt::Display for ApiRequests { } } -type ApiResult = - Result>>, Box>; - #[async_trait] pub trait NCRequestInterface: Debug + Send + Send + Sync { async fn request_send_message( @@ -180,7 +159,7 @@ impl NCRequest { tokio::spawn(async move { loop { - if let Ok(req) = rx_worker.recv().await { + if let Some(req) = rx_worker.recv().await { NCRequest::handle_req(&worker, req).await; }; } @@ -279,7 +258,11 @@ impl NCRequestInterface for NCRequest { .expect("Queuing request for sending of message failed."); Ok(rx) } - async fn request_chat_initial(&self, token: &Token, maxMessage: i32) -> ApiResult<()> { + async fn request_chat_initial( + &self, + token: &Token, + maxMessage: i32, + ) -> ApiResult> { let (tx, rx) = tokio::sync::oneshot::channel(); self.request_tx @@ -307,11 +290,7 @@ impl NCRequestInterface for NCRequest { .expect("Queuing request for sending of message failed."); Ok(rx) } - async fn request_mark_chat_read( - &self, - token: &str, - last_message: i32, - ) -> Result>, Box> { + async fn request_mark_chat_read(&self, token: &str, last_message: i32) -> ApiResult<()> { let (tx, rx) = tokio::sync::oneshot::channel(); self.request_tx diff --git a/src/backend/nc_room.rs b/src/backend/nc_room.rs index 849e3b7..13dc739 100644 --- a/src/backend/nc_room.rs +++ b/src/backend/nc_room.rs @@ -139,8 +139,7 @@ impl NCRoom { let response = response_onceshot .await .expect("Failed for fetch chat update") - .ok_or("Failed request") - .unwrap(); + .expect("Failed request"); for message in response { messages.push(message.into()); } @@ -269,8 +268,8 @@ impl NCRoomInterface for NCRoom { .await .expect("Failed for fetch chat participants"); match response { - Some(v) => Ok(v.message), - None => Err("Failed to Send Message".into()), + Ok(v) => Ok(v.message), + Err(why) => Err(why.into()), } } @@ -297,8 +296,7 @@ impl NCRoomInterface for NCRoom { let response = response_onceshot .await .expect("Failed for fetch chat update") - .ok_or("Failed request") - .unwrap(); + .expect("Failed request"); let is_empty = response.is_empty(); let update_info = Some((self.room_data.displayName.clone(), response.len())); @@ -325,8 +323,7 @@ impl NCRoomInterface for NCRoom { self.participants = response_onceshot .await .expect("Failed for fetch chat participants") - .ok_or("Failed request") - .unwrap(); + .expect("Failed request"); if self.has_unread() && !is_empty { Ok(update_info) } else { @@ -352,8 +349,7 @@ impl NCRoomInterface for NCRoom { response_onceshot .await .expect("Failed for fetch chat participants") - .ok_or("Failed request") - .unwrap(); + .expect("Failed request"); } Ok(()) } diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index fb9085f..585ab4f 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -156,8 +156,7 @@ impl NCTalk = response @@ -380,8 +379,7 @@ impl NCBackend for }; resp.await .expect("Initial fetching of rooms failed.") - .ok_or("No rooms found") - .expect("No rooms") + .expect("No rooms found") } else { let resp = { self.requester @@ -393,8 +391,7 @@ impl NCBackend for }; resp.await .expect("Initial fetching of rooms failed.") - .ok_or("No rooms found") - .expect("No rooms") + .expect("No rooms found") }; self.last_requested = timestamp; let mut new_room_token: Vec = vec![]; @@ -513,7 +510,7 @@ mod tests { roomtype: 2, // Group Chat ..Default::default() }; - tx.send(Some((vec![default_room], 1))) + tx.send(Ok((vec![default_room], 1))) .expect("Sending Failed."); let default_message = NCReqDataMessage { @@ -523,7 +520,7 @@ mod tests { }; chat_tx - .send(Some(vec![default_message.clone()])) + .send(Ok(vec![default_message.clone()])) .expect("Sending Failed."); let update_message = NCReqDataMessage { @@ -532,11 +529,11 @@ mod tests { ..Default::default() }; update_tx - .send(Some(vec![update_message.clone()])) + .send(Ok(vec![update_message.clone()])) .expect("Sending Failed."); pat_tx - .send(Some(vec![NCReqDataParticipants::default()])) + .send(Ok(vec![NCReqDataParticipants::default()])) .expect("Sending Failed."); mock_requester @@ -605,32 +602,32 @@ mod tests { let mut seq = Sequence::new(); - tx.send(Some((vec![default_room.clone()], 1))) + tx.send(Ok((vec![default_room.clone()], 1))) .expect("Sending Failed."); - tx2.send(Some((vec![default_room.clone()], 1))) + tx2.send(Ok((vec![default_room.clone()], 1))) .expect("Sending Failed."); chat_tx - .send(Some(vec![default_message.clone()])) + .send(Ok(vec![default_message.clone()])) .expect("Sending Failed."); update_tx - .send(Some(vec![update_message.clone()])) + .send(Ok(vec![update_message.clone()])) .expect("Sending Failed."); pat_tx - .send(Some(vec![NCReqDataParticipants::default()])) + .send(Ok(vec![NCReqDataParticipants::default()])) .expect("Sending Failed."); pat2_tx - .send(Some(vec![NCReqDataParticipants::default()])) + .send(Ok(vec![NCReqDataParticipants::default()])) .expect("Sending Failed."); send_tx - .send(Some(NCReqDataMessage::default())) + .send(Ok(NCReqDataMessage::default())) .expect("Sending Failed"); chat_update_tx - .send(Some(vec![post_send_message.clone()])) + .send(Ok(vec![post_send_message.clone()])) .expect("Failed to send"); mock_requester @@ -728,7 +725,7 @@ mod tests { roomtype: 2, // Group Chat ..Default::default() }; - tx.send(Some((vec![default_room], 1))) + tx.send(Ok((vec![default_room], 1))) .expect("Sending Failed."); let default_message = NCReqDataMessage { @@ -738,7 +735,7 @@ mod tests { }; chat_tx - .send(Some(vec![default_message.clone()])) + .send(Ok(vec![default_message.clone()])) .expect("Sending Failed."); let update_message = NCReqDataMessage { @@ -747,11 +744,11 @@ mod tests { ..Default::default() }; update_tx - .send(Some(vec![update_message.clone()])) + .send(Ok(vec![update_message.clone()])) .expect("Sending Failed."); pat_tx - .send(Some(vec![NCReqDataParticipants::default()])) + .send(Ok(vec![NCReqDataParticipants::default()])) .expect("Sending Failed."); mock_requester From 3c3efb177e122e29b6f643c43e54f4645bfd2634 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 20 Dec 2024 12:00:07 +0100 Subject: [PATCH 13/16] more tests for NCTalk --- src/backend/nc_talk.rs | 244 ++++++++++++++++++----------------------- 1 file changed, 104 insertions(+), 140 deletions(-) diff --git a/src/backend/nc_talk.rs b/src/backend/nc_talk.rs index 585ab4f..dafbde2 100644 --- a/src/backend/nc_talk.rs +++ b/src/backend/nc_talk.rs @@ -481,8 +481,6 @@ impl std::fmt::Display for MockNCTalk { #[cfg(test)] mod tests { - use mockall::Sequence; - use super::*; use crate::{ backend::nc_request::{ @@ -490,37 +488,38 @@ mod tests { }, config::init, }; + fn get_default_token() -> Token { + Token::from("123") + } - #[tokio::test] - async fn create_backend() { - let dir = tempfile::tempdir().unwrap(); + fn get_default_room() -> NCReqDataRoom { + NCReqDataRoom { + displayName: "General".to_string(), + token: get_default_token(), + roomtype: 2, // Group Chat + ..Default::default() + } + } - std::env::set_var("HOME", dir.path().as_os_str()); - let config = init("./test/").unwrap(); - let mut mock_requester = MockNCRequest::new(); + fn get_default_message() -> NCReqDataMessage { + NCReqDataMessage { + messageType: "comment".to_string(), + id: 1, + ..Default::default() + } + } + fn prep_backend_creation(mock_requester: &mut MockNCRequest) { let (tx, rx) = tokio::sync::oneshot::channel(); let (chat_tx, chat_rx) = tokio::sync::oneshot::channel(); let (update_tx, update_rx) = tokio::sync::oneshot::channel(); let (pat_tx, pat_rx) = tokio::sync::oneshot::channel(); - let default_room = NCReqDataRoom { - displayName: "General".to_string(), - token: "a123".into(), - roomtype: 2, // Group Chat - ..Default::default() - }; - tx.send(Ok((vec![default_room], 1))) + tx.send(Ok((vec![get_default_room()], 1))) .expect("Sending Failed."); - let default_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 1, - ..Default::default() - }; - chat_tx - .send(Ok(vec![default_message.clone()])) + .send(Ok(vec![get_default_message()])) .expect("Sending Failed."); let update_message = NCReqDataMessage { @@ -542,6 +541,7 @@ mod tests { .return_once(move || Ok(rx)); mock_requester .expect_request_chat_initial() + .with(eq(get_default_token()), eq(200)) .return_once(move |_, _| Ok(chat_rx)); mock_requester @@ -550,49 +550,101 @@ mod tests { .return_once(move |_| Ok(pat_rx)); mock_requester .expect_request_chat_update() + .with(eq(get_default_token()), eq(200), eq(1)) .return_once_st(move |_, _, _| Ok(update_rx)); + } + + #[tokio::test] + async fn create_backend() { + let dir = tempfile::tempdir().unwrap(); + std::env::set_var("HOME", dir.path().as_os_str()); + let config = init("./test/").unwrap(); + let mut mock_requester = MockNCRequest::new(); + prep_backend_creation(&mut mock_requester); let backend = NCTalk::new(mock_requester, &config) .await .expect("Failed to create Backend"); assert_eq!(backend.rooms.len(), 1); } + #[tokio::test] + async fn mark_room_as_read() { + let dir = tempfile::tempdir().unwrap(); + + std::env::set_var("HOME", dir.path().as_os_str()); + let config = init("./test/").unwrap(); + let (chat_tx, chat_rx) = tokio::sync::oneshot::channel(); + chat_tx.send(Ok(())).expect("Sending Failed."); + let mut mock_requester = MockNCRequest::new(); + prep_backend_creation(&mut mock_requester); + mock_requester + .expect_request_mark_chat_read() + .with(eq(get_default_token()), eq(2)) + .return_once(move |_, _| Ok(chat_rx)); + + let backend = NCTalk::new(mock_requester, &config) + .await + .expect("Failed to create Backend"); + assert!(backend + .mark_current_room_as_read(&get_default_token()) + .await + .is_ok()); + } #[tokio::test] - async fn room_handling() { - let init = init("./test/").unwrap(); - let config = init; + async fn force_room_update() { + let dir = tempfile::tempdir().unwrap(); + std::env::set_var("HOME", dir.path().as_os_str()); + let config = init("./test/").unwrap(); let mut mock_requester = MockNCRequest::new(); - let (tx, rx) = tokio::sync::oneshot::channel(); + prep_backend_creation(&mut mock_requester); + let (tx2, rx2) = tokio::sync::oneshot::channel(); let (chat_tx, chat_rx) = tokio::sync::oneshot::channel(); - let (update_tx, update_rx) = tokio::sync::oneshot::channel(); - let (pat_tx, pat_rx) = tokio::sync::oneshot::channel(); - let (pat2_tx, pat2_rx) = tokio::sync::oneshot::channel(); - let (send_tx, send_rx) = tokio::sync::oneshot::channel(); - let (chat_update_tx, chat_update_rx) = tokio::sync::oneshot::channel(); - let default_token = Token::from("123"); - - let default_room = NCReqDataRoom { - displayName: "General".to_string(), + let new_room = NCReqDataRoom { + displayName: "General2".to_string(), + token: Token::from("3456"), roomtype: 2, // Group Chat - token: default_token.clone(), ..Default::default() }; + tx2.send(Ok((vec![get_default_room(), new_room], 2))) + .expect("Sending Failed."); - let default_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 1, - ..Default::default() - }; - let update_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 2, - ..Default::default() - }; + chat_tx + .send(Ok(vec![get_default_message()])) + .expect("Sending Failed."); + + mock_requester + .expect_request_rooms_initial() + .once() + .return_once(move || Ok(rx2)); + mock_requester + .expect_request_chat_initial() + .with(eq(Token::from("3456")), eq(200)) + .return_once(move |_, _| Ok(chat_rx)); + + let mut backend = NCTalk::new(mock_requester, &config) + .await + .expect("Failed to create Backend"); + assert_eq!(backend.rooms.len(), 1); + + assert!(backend.update_rooms(false).await.is_ok()); + } + + #[tokio::test] + async fn room_handling() { + let init = init("./test/").unwrap(); + let config = init; + + let mut mock_requester = MockNCRequest::new(); + + let (tx2, rx2) = tokio::sync::oneshot::channel(); + let (pat2_tx, pat2_rx) = tokio::sync::oneshot::channel(); + let (send_tx, send_rx) = tokio::sync::oneshot::channel(); + let (chat_update_tx, chat_update_rx) = tokio::sync::oneshot::channel(); let post_send_message = NCReqDataMessage { messageType: "comment".to_string(), @@ -600,24 +652,9 @@ mod tests { ..Default::default() }; - let mut seq = Sequence::new(); - - tx.send(Ok((vec![default_room.clone()], 1))) - .expect("Sending Failed."); - tx2.send(Ok((vec![default_room.clone()], 1))) - .expect("Sending Failed."); - - chat_tx - .send(Ok(vec![default_message.clone()])) - .expect("Sending Failed."); - - update_tx - .send(Ok(vec![update_message.clone()])) + tx2.send(Ok((vec![get_default_room()], 1))) .expect("Sending Failed."); - pat_tx - .send(Ok(vec![NCReqDataParticipants::default()])) - .expect("Sending Failed."); pat2_tx .send(Ok(vec![NCReqDataParticipants::default()])) .expect("Sending Failed."); @@ -630,40 +667,23 @@ mod tests { .send(Ok(vec![post_send_message.clone()])) .expect("Failed to send"); - mock_requester - .expect_request_rooms_initial() - .once() - .return_once(move || Ok(rx)); + prep_backend_creation(&mut mock_requester); + mock_requester .expect_request_rooms_initial() .once() .return_once(move || Ok(rx2)); - mock_requester - .expect_request_chat_initial() - .return_once(move |_, _| Ok(chat_rx)); - - mock_requester - .expect_request_participants() - .times(1) - .return_once(move |_| Ok(pat_rx)); - mock_requester - .expect_request_chat_update() - .once() - .in_sequence(&mut seq) - .return_once(move |_, _, _| Ok(update_rx)); mock_requester .expect_request_send_message() .once() - .in_sequence(&mut seq) .withf(|message: &String, token: &Token| message == "Test" && *token == "123") .return_once(|_, _| Ok(send_rx)); mock_requester .expect_request_chat_update() .once() - .with(eq(Token::from("123")), eq(200), eq(2)) - .in_sequence(&mut seq) + .with(eq(get_default_token()), eq(200), eq(2)) .return_once(move |_, _, _| Ok(chat_update_rx)); mock_requester @@ -671,14 +691,10 @@ mod tests { .times(1) .return_once(move |_| Ok(pat2_rx)); - let backend = NCTalk::new(mock_requester, &config) + let mut backend = NCTalk::new(mock_requester, &config) .await .expect("Failed to create Backend"); - check_results(backend).await; - } - - async fn check_results(mut backend: NCTalk) { assert!(backend .send_message("Test".to_owned(), &Token::from("123")) .await @@ -713,59 +729,7 @@ mod tests { println!("Path is {}", config.get_data_dir().display()); let mut mock_requester = MockNCRequest::new(); - - let (tx, rx) = tokio::sync::oneshot::channel(); - let (chat_tx, chat_rx) = tokio::sync::oneshot::channel(); - let (update_tx, update_rx) = tokio::sync::oneshot::channel(); - let (pat_tx, pat_rx) = tokio::sync::oneshot::channel(); - - let default_room = NCReqDataRoom { - displayName: "General".to_string(), - token: "a123".into(), - roomtype: 2, // Group Chat - ..Default::default() - }; - tx.send(Ok((vec![default_room], 1))) - .expect("Sending Failed."); - - let default_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 1, - ..Default::default() - }; - - chat_tx - .send(Ok(vec![default_message.clone()])) - .expect("Sending Failed."); - - let update_message = NCReqDataMessage { - messageType: "comment".to_string(), - id: 2, - ..Default::default() - }; - update_tx - .send(Ok(vec![update_message.clone()])) - .expect("Sending Failed."); - - pat_tx - .send(Ok(vec![NCReqDataParticipants::default()])) - .expect("Sending Failed."); - - mock_requester - .expect_request_rooms_initial() - .once() - .return_once(move || Ok(rx)); - mock_requester - .expect_request_chat_initial() - .return_once(move |_, _| Ok(chat_rx)); - - mock_requester - .expect_request_participants() - .times(1) - .return_once(move |_| Ok(pat_rx)); - mock_requester - .expect_request_chat_update() - .return_once_st(move |_, _, _| Ok(update_rx)); + prep_backend_creation(&mut mock_requester); let mut backend = NCTalk::new(mock_requester, &config) .await From 97af44a301c514631fa5c93b38557724027ccb93 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz und Neuhaus Date: Fri, 20 Dec 2024 12:09:55 +0100 Subject: [PATCH 14/16] write test for notify --- src/ui/notifications.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/ui/notifications.rs b/src/ui/notifications.rs index bdefcad..0791fe6 100644 --- a/src/ui/notifications.rs +++ b/src/ui/notifications.rs @@ -87,3 +87,21 @@ impl NotifyWrapper { Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::config::init; + + use super::NotifyWrapper; + + #[test] + fn create_backend() { + let dir = tempfile::tempdir().unwrap(); + + std::env::set_var("HOME", dir.path().as_os_str()); + let config = init("./test/").unwrap(); + let notify = NotifyWrapper::new(&config); + assert!(!notify.is_persistent()); + assert!(notify.unread_message(&"Butz".to_string(), 2).is_ok()); + } +} From 578421afae27d9cac188a353dcb0e5ae3ae2aabe Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Sat, 21 Dec 2024 12:07:26 +0100 Subject: [PATCH 15/16] update and rework notification test --- Cargo.toml | 2 +- src/ui/notifications.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ccf7637..1dafdee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ etcetera = "0.8.0" better-panic = "0.3.0" color-eyre = "0.6.3" human-panic = "2.0.2" -libc = "0.2.168" +libc = "0.2.169" strip-ansi-escapes = "0.2.0" tracing = "0.1.41" cfg-if = "1.0.0" diff --git a/src/ui/notifications.rs b/src/ui/notifications.rs index 0791fe6..5c76789 100644 --- a/src/ui/notifications.rs +++ b/src/ui/notifications.rs @@ -90,18 +90,21 @@ impl NotifyWrapper { #[cfg(test)] mod tests { + use crate::config::init; use super::NotifyWrapper; + /// We cannot test the actual notifications. #[test] - fn create_backend() { + fn create_notify() { let dir = tempfile::tempdir().unwrap(); std::env::set_var("HOME", dir.path().as_os_str()); let config = init("./test/").unwrap(); let notify = NotifyWrapper::new(&config); assert!(!notify.is_persistent()); - assert!(notify.unread_message(&"Butz".to_string(), 2).is_ok()); + assert!(notify.maybe_notify_new_message(None).is_ok()); + assert!(notify.maybe_notify_new_rooms(vec![]).is_ok()); } } From ffe54a6cc653c32369964692cda69b748f0250e6 Mon Sep 17 00:00:00 2001 From: Michel von Czettritz Date: Sat, 21 Dec 2024 13:05:44 +0100 Subject: [PATCH 16/16] introduce workermock and interface --- src/backend/nc_request/nc_req_worker.rs | 187 +++++++++++++++++------- src/backend/nc_request/nc_requester.rs | 24 ++- 2 files changed, 155 insertions(+), 56 deletions(-) diff --git a/src/backend/nc_request/nc_req_worker.rs b/src/backend/nc_request/nc_req_worker.rs index ec683fd..35c37bd 100644 --- a/src/backend/nc_request/nc_req_worker.rs +++ b/src/backend/nc_request/nc_req_worker.rs @@ -3,6 +3,7 @@ #![allow(dead_code)] use crate::config::Config; +use async_trait::async_trait; use base64::{prelude::BASE64_STANDARD, write::EncoderWriter}; use jzon; use reqwest::{ @@ -24,49 +25,43 @@ pub struct NCRequestWorker { json_dump_path: Option, } -impl NCRequestWorker { - pub fn new(config: &Config) -> Result> { - use std::io::Write; - - let general = &config.data.general; - - let username = general.user.clone(); - let password = Some(general.app_pw.clone()); - let base_url = general.url.clone(); - - let json_dump_path = config.get_http_dump_dir(); - let mut headers = HeaderMap::new(); - headers.insert("OCS-APIRequest", HeaderValue::from_static("true")); - headers.insert("Accept", HeaderValue::from_static("application/json")); - - let mut buf = b"Basic ".to_vec(); - { - let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD); - write!(encoder, "{username}:").expect("i/o error"); - if let Some(password) = password { - write!(encoder, "{password}").expect("i/o error"); - } - } - let mut auth_value = - HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); - auth_value.set_sensitive(true); - headers.insert(AUTHORIZATION, auth_value); - - // get a client builder - let client = reqwest::Client::builder() - .default_headers(headers.clone()) - .build()?; - - log::info!("Worker Ready {}", base_url.to_string()); +#[async_trait] +pub trait NCRequestWorkerInterface: Debug + Send + Send + Sync + Sized { + fn new(config: &Config) -> Result>; + async fn mark_chat_read(&self, token: &str, last_message: i32) -> Result<(), Box>; + async fn send_message( + &self, + message: String, + token: &Token, + ) -> Result>; + async fn fetch_autocomplete_users( + &self, + name: &str, + ) -> Result, Box>; + async fn fetch_participants( + &self, + token: &Token, + ) -> Result, Box>; + async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box>; - Ok(NCRequestWorker { - base_url: base_url.to_string(), - client, - base_headers: headers, - json_dump_path, - }) - } + async fn fetch_rooms_update( + &self, + last_timestamp: i64, + ) -> Result<(Vec, i64), Box>; + async fn fetch_chat_initial( + &self, + token: &Token, + maxMessage: i32, + ) -> Result, Box>; + async fn fetch_chat_update( + &self, + token: &Token, + maxMessage: i32, + last_message: i32, + ) -> Result, Box>; +} +impl NCRequestWorker { async fn request_rooms( &self, last_timestamp: Option, @@ -184,8 +179,53 @@ impl NCRequestWorker { } Ok(()) } +} + +#[async_trait] +impl NCRequestWorkerInterface for NCRequestWorker { + fn new(config: &Config) -> Result> { + use std::io::Write; + + let general = &config.data.general; + + let username = general.user.clone(); + let password = Some(general.app_pw.clone()); + let base_url = general.url.clone(); + + let json_dump_path = config.get_http_dump_dir(); + let mut headers = HeaderMap::new(); + headers.insert("OCS-APIRequest", HeaderValue::from_static("true")); + headers.insert("Accept", HeaderValue::from_static("application/json")); - pub async fn send_message( + let mut buf = b"Basic ".to_vec(); + { + let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD); + write!(encoder, "{username}:").expect("i/o error"); + if let Some(password) = password { + write!(encoder, "{password}").expect("i/o error"); + } + } + let mut auth_value = + HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"); + auth_value.set_sensitive(true); + headers.insert(AUTHORIZATION, auth_value); + + // get a client builder + let client = reqwest::Client::builder() + .default_headers(headers.clone()) + .build()?; + + log::info!("Worker Ready {}", base_url.to_string()); + + Ok(NCRequestWorker { + base_url: base_url.to_string(), + client, + base_headers: headers, + json_dump_path, + }) + } + + async fn send_message( &self, message: String, token: &Token, @@ -210,7 +250,7 @@ impl NCRequestWorker { } } - pub async fn fetch_autocomplete_users( + async fn fetch_autocomplete_users( &self, name: &str, ) -> Result, Box> { @@ -240,7 +280,7 @@ impl NCRequestWorker { } } - pub async fn fetch_participants( + async fn fetch_participants( &self, token: &Token, ) -> Result, Box> { @@ -273,18 +313,18 @@ impl NCRequestWorker { } } - pub async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box> { + async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box> { self.request_rooms(None).await } - pub async fn fetch_rooms_update( + async fn fetch_rooms_update( &self, last_timestamp: i64, ) -> Result<(Vec, i64), Box> { self.request_rooms(Some(last_timestamp)).await } - pub async fn fetch_chat_initial( + async fn fetch_chat_initial( &self, token: &Token, maxMessage: i32, @@ -301,7 +341,7 @@ impl NCRequestWorker { } } - pub async fn fetch_chat_update( + async fn fetch_chat_update( &self, token: &Token, maxMessage: i32, @@ -317,11 +357,7 @@ impl NCRequestWorker { } } - pub async fn mark_chat_read( - &self, - token: &str, - last_message: i32, - ) -> Result<(), Box> { + async fn mark_chat_read(&self, token: &str, last_message: i32) -> Result<(), Box> { let url_string = self.base_url.clone() + "/ocs/v2.php/apps/spreed/api/v1/chat/" + token + "/read"; let url = Url::parse(&url_string)?; @@ -339,6 +375,51 @@ impl NCRequestWorker { } } +#[cfg(test)] +use mockall::{mock, predicate::*}; + +#[cfg(test)] +mock! { + #[derive(Debug)] + pub NCRequestWorker{ + } + #[async_trait] + impl NCRequestWorkerInterface for NCRequestWorker{ + fn new(config: &Config) -> Result>; + async fn mark_chat_read(&self, token: &str, last_message: i32) -> Result<(), Box>; + async fn send_message( + &self, + message: String, + token: &Token, + ) -> Result>; + async fn fetch_autocomplete_users( + &self, + name: &str, + ) -> Result, Box>; + async fn fetch_participants( + &self, + token: &Token, + ) -> Result, Box>; + async fn fetch_rooms_initial(&self) -> Result<(Vec, i64), Box>; + + async fn fetch_rooms_update( + &self, + last_timestamp: i64, + ) -> Result<(Vec, i64), Box>; + async fn fetch_chat_initial( + &self, + token: &Token, + maxMessage: i32, + ) -> Result, Box>; + async fn fetch_chat_update( + &self, + token: &Token, + maxMessage: i32, + last_message: i32, + ) -> Result, Box>; + } +} + #[cfg(test)] mod tests { use crate::config::init; diff --git a/src/backend/nc_request/nc_requester.rs b/src/backend/nc_request/nc_requester.rs index 4396b48..7f9b2a6 100644 --- a/src/backend/nc_request/nc_requester.rs +++ b/src/backend/nc_request/nc_requester.rs @@ -13,8 +13,8 @@ use std::{fmt::Debug, sync::Arc}; use mockall::{mock, predicate::*}; use super::{ - nc_req_worker::NCRequestWorker, NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, - NCReqDataUser, Token, + nc_req_worker::{NCRequestWorker, NCRequestWorkerInterface}, + NCReqDataMessage, NCReqDataParticipants, NCReqDataRoom, NCReqDataUser, Token, }; type ApiResult = @@ -155,7 +155,7 @@ impl NCRequest { let (tx_worker, mut rx_worker) = mpsc::channel::(10); worker_queue.push(tx_worker); - let worker = NCRequestWorker::new(config).unwrap(); + let worker = NCRequestWorker::new(config).expect("Failed to create worker."); tokio::spawn(async move { loop { @@ -341,3 +341,21 @@ mock! { fn clone(&self) -> Self; } } + +#[cfg(test)] +mod tests { + + use crate::config::init; + + use super::*; + + #[tokio::test] + async fn create() { + let dir = tempfile::tempdir().unwrap(); + + std::env::set_var("HOME", dir.path().as_os_str()); + let config = init("./test/").unwrap(); + + let requester = NCRequest::new(&config); + } +}