initial commit
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | /target | ||||||
							
								
								
									
										1180
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1180
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										16
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | [package] | ||||||
|  | name = "msgexport" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  |  | ||||||
|  | [dependencies] | ||||||
|  | serde = { version = "1.0", features = ["derive"] } | ||||||
|  | serde_json = "1.0" | ||||||
|  | clap = { version = "4.0", features = ["derive"] } | ||||||
|  | tokio = { version = "1", features = ["full"] } | ||||||
|  | dialoguer = "0.11.0" | ||||||
|  | tabled = "0.15.0" | ||||||
|  | async-trait = "0.1.48" | ||||||
|  | encoding = "0.2" | ||||||
|  | zip = "0.6" | ||||||
|  | tokio-util = "0.7" | ||||||
							
								
								
									
										63
									
								
								src/commands/add.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/commands/add.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | use std::{collections::HashMap, process::exit}; | ||||||
|  |  | ||||||
|  | use dialoguer::Select; | ||||||
|  |  | ||||||
|  | use crate::{services::{instagram::Instagram, service::Service}, types::{participant::Participant, project::Project, user::User}}; | ||||||
|  |  | ||||||
|  | pub async fn add(path: String) { | ||||||
|  |     let mut raw_project = Project::load(path).await.unwrap_or_else(|e| { | ||||||
|  |         eprintln!("Error while loading a project: {}", e); | ||||||
|  |         exit(1); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if raw_project.project.users.is_empty() { | ||||||
|  |         eprintln!("Error! You do not have any users in your project. Create at least one to import chat."); | ||||||
|  |         exit(1); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let services: Vec<Box<dyn Service>> = vec![ | ||||||
|  |         Box::new(Instagram {  | ||||||
|  |             data: None | ||||||
|  |         }),  | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     let options: Vec<&'static str> = services.iter().map(|service| service.get_name()).collect(); | ||||||
|  |  | ||||||
|  |     let selected = Select::new() | ||||||
|  |         .with_prompt("Choose a service") | ||||||
|  |         .items(&options) | ||||||
|  |         .default(0) | ||||||
|  |         .interact() | ||||||
|  |         .unwrap(); | ||||||
|  |  | ||||||
|  |     let service = services.get(selected).unwrap(); | ||||||
|  |     let mut final_service = service.setup(); | ||||||
|  |     let _ = final_service.load().await; | ||||||
|  |  | ||||||
|  |     let participants = final_service.get_participants(); | ||||||
|  |  | ||||||
|  |     let mut user_map: HashMap<Participant, User> = HashMap::new(); | ||||||
|  |  | ||||||
|  |     println!("Now select what project user is coresponding to a {} participant", service.get_name()); | ||||||
|  |     for user in raw_project.project.users.clone() { | ||||||
|  |         let parts: Vec<Participant> = participants.iter().filter(|participant| { | ||||||
|  |             !user_map.contains_key(&participant) | ||||||
|  |         }).map(|e| e.clone()).collect(); | ||||||
|  |  | ||||||
|  |         let options: Vec<String> = parts.iter().map(|part| part.name.clone()).collect(); | ||||||
|  |  | ||||||
|  |         let selected = Select::new() | ||||||
|  |             .with_prompt(format!("Select a {} participant for user '{}'", service.get_name(), user.name)) | ||||||
|  |             .items(&options) | ||||||
|  |             .default(0) | ||||||
|  |             .interact() | ||||||
|  |             .unwrap(); | ||||||
|  |  | ||||||
|  |         let selected_part = parts.get(selected).unwrap().clone(); | ||||||
|  |  | ||||||
|  |         user_map.insert(selected_part.clone(), user); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     println!("Starting to merge messages..."); | ||||||
|  |     final_service.merge_messages(&mut raw_project, &user_map) | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								src/commands/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/commands/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | pub mod users; | ||||||
|  | pub mod add; | ||||||
							
								
								
									
										88
									
								
								src/commands/users.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/commands/users.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,88 @@ | |||||||
|  | use std::process::exit; | ||||||
|  |  | ||||||
|  | use dialoguer::{Input, Select}; | ||||||
|  | use tabled::Table; | ||||||
|  |  | ||||||
|  | use crate::types::{project::Project, user::User}; | ||||||
|  |  | ||||||
|  | pub async fn users(path: String) { | ||||||
|  |     let project = Project::load(path.clone()).await.unwrap_or_else(|e| { | ||||||
|  |         eprintln!("Error while loading a project: {}", e); | ||||||
|  |         exit(1); | ||||||
|  |     }).project; | ||||||
|  |  | ||||||
|  |     menu(project, path).await; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn print_users(users: &Vec<User>) { | ||||||
|  |     if users.is_empty() { | ||||||
|  |         println!("Currently you do not have any users!"); | ||||||
|  |         return | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let users = Table::new(users).to_string(); | ||||||
|  |     println!("{}", users); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub async fn menu(mut project: Project, path: String) { | ||||||
|  |     println!("User management:"); | ||||||
|  |     print_users(&project.users); | ||||||
|  |     println!("\n"); | ||||||
|  |  | ||||||
|  |     let options = vec!["Create a new user", "Remove a user", "Save and exit"]; | ||||||
|  |  | ||||||
|  |     let option = Select::new() | ||||||
|  |         .with_prompt("Choose a option") | ||||||
|  |         .items(&options) | ||||||
|  |         .default(0) | ||||||
|  |         .interact() | ||||||
|  |         .unwrap(); | ||||||
|  |  | ||||||
|  |     match option { | ||||||
|  |         0 => { | ||||||
|  |             // Create a user | ||||||
|  |             let name: String = Input::new() | ||||||
|  |                 .with_prompt("Name") | ||||||
|  |                 .interact_text() | ||||||
|  |                 .expect("Failed to read a line"); | ||||||
|  |  | ||||||
|  |             let user = User { | ||||||
|  |                 name | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             project.users.push(user); | ||||||
|  |  | ||||||
|  |             Box::pin(menu(project, path)).await; | ||||||
|  |         }, | ||||||
|  |         1 => { | ||||||
|  |             if project.users.is_empty() { | ||||||
|  |                 println!("You do not have any users created! No users to be removed!"); | ||||||
|  |                 Box::pin(menu(project, path)).await; | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             let options_users: Vec<String> = project.users.iter().map(|user| { | ||||||
|  |                 user.name.clone() | ||||||
|  |             }).collect(); | ||||||
|  |  | ||||||
|  |             let option_remove = Select::new() | ||||||
|  |                 .with_prompt("Select user to be removed") | ||||||
|  |                 .items(&options_users) | ||||||
|  |                 .default(0) | ||||||
|  |                 .interact() | ||||||
|  |                 .unwrap(); | ||||||
|  |              | ||||||
|  |             project.users.remove(option_remove); | ||||||
|  |  | ||||||
|  |             Box::pin(menu(project, path)).await; | ||||||
|  |         }, | ||||||
|  |         2 => { | ||||||
|  |             // Save and exit | ||||||
|  |             let _ = project.save(path).await; | ||||||
|  |         }, | ||||||
|  |         _ => { | ||||||
|  |             println!("Error! Unknown option!"); | ||||||
|  |             Box::pin(menu(project, path)).await; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | |||||||
|  | mod types; | ||||||
|  | mod commands; | ||||||
|  | mod services; | ||||||
|  |  | ||||||
|  | use std::collections::HashSet; | ||||||
|  |  | ||||||
|  | use clap::{command, ArgGroup, Parser}; | ||||||
|  | use commands::{add::add, users::users}; | ||||||
|  | use types::project::Project; | ||||||
|  |  | ||||||
|  | #[derive(Parser, Debug)] | ||||||
|  | #[command( | ||||||
|  |     name = "msgexport", | ||||||
|  |     version = env!("CARGO_PKG_VERSION"), | ||||||
|  |     about = "Export and merge your messages between platforms", | ||||||
|  |     author = "Jakub Žitník", | ||||||
|  |     group = ArgGroup::new("command") | ||||||
|  |         .args(&["new", "users", "add"]) | ||||||
|  |         .multiple(false) | ||||||
|  |         .required(true) | ||||||
|  | )] | ||||||
|  | struct Cli { | ||||||
|  |     #[arg(short = 'n', long = "new", help = "Create new msgexport project.", value_name = "OUTPUT_FILE")] | ||||||
|  |     new: Option<String>, | ||||||
|  |  | ||||||
|  |     #[arg(long = "users", help = "Add and remove users from the project.", value_name = "PROJECT_FILE")] | ||||||
|  |     users: Option<String>, | ||||||
|  |  | ||||||
|  |     #[arg(long = "add", help = "Add messages from a service.", value_name = "PROJECT_FILE")] | ||||||
|  |     add: Option<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[tokio::main] | ||||||
|  | async fn main() { | ||||||
|  |     let cli = Cli::parse(); | ||||||
|  |  | ||||||
|  |     if let Some(path) = cli.new { | ||||||
|  |         let new_project = Project { | ||||||
|  |             users: Vec::new(), | ||||||
|  |             messages: Vec::new(), | ||||||
|  |             timestamps: HashSet::new(), | ||||||
|  |             media_index: 0, | ||||||
|  |         }; | ||||||
|  |         let _ = new_project.save(path).await; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if let Some(path) = cli.users { | ||||||
|  |         users(path).await; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     if let Some(path) = cli.add { | ||||||
|  |         add(path).await; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										217
									
								
								src/services/instagram.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								src/services/instagram.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,217 @@ | |||||||
|  | use std::{collections::HashMap, fs, io, path::PathBuf, process::exit}; | ||||||
|  |  | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use dialoguer::{Input, Select}; | ||||||
|  | use encoding::{all::ISO_8859_1, EncoderTrap, Encoding}; | ||||||
|  | use serde::Deserialize; | ||||||
|  | use tokio::{fs::File, io::AsyncReadExt}; | ||||||
|  |  | ||||||
|  | use crate::types::{message::{Message, Photo}, participant::Participant, project::ProjectRaw, user::User}; | ||||||
|  |  | ||||||
|  | use super::service::Service; | ||||||
|  |  | ||||||
|  | fn decode_str(input: &String) -> String { | ||||||
|  |     let vec = ISO_8859_1.encode(input.as_str(), EncoderTrap::Strict).unwrap(); | ||||||
|  |     String::from_utf8(vec).unwrap() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Deserialize, Debug)] | ||||||
|  | pub struct InstagramMessage { | ||||||
|  |     sender_name: String, | ||||||
|  |     timestamp_ms: usize, | ||||||
|  |     content: Option<String>, | ||||||
|  |     photos: Option<Vec<InstagramPhoto>> | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl InstagramMessage { | ||||||
|  |     // TODO: Take into consideration the user_map | ||||||
|  |     pub fn conv(&self, media_index: &mut usize, project_path: PathBuf, user_map: &HashMap<Participant, User>) -> Message { | ||||||
|  |         let text_content = if let Some(content) = &self.content { | ||||||
|  |             Some(decode_str(content)) | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         let photos: Vec<Photo> = if let Some(photos_list) = &self.photos { | ||||||
|  |             photos_list.iter().map(|photo| photo.conv(&text_content, media_index, project_path.clone())).collect() | ||||||
|  |         } else { | ||||||
|  |             vec![] | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         let participant = Participant { | ||||||
|  |             name: decode_str(&self.sender_name) | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         Message { | ||||||
|  |             sender_name: user_map.get(&participant).unwrap().name.clone(), | ||||||
|  |             text_content, | ||||||
|  |             timestamp: self.timestamp_ms, | ||||||
|  |             photos | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Deserialize, Debug)] | ||||||
|  | struct InstagramPhoto { | ||||||
|  |     uri: String | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl InstagramPhoto { | ||||||
|  |     pub fn conv(&self, text_content: &Option<String>, media_index: &mut usize, mut original_path: PathBuf) -> Photo { | ||||||
|  |         *media_index += 1; | ||||||
|  |         let img_path = (*media_index - 1).to_string(); | ||||||
|  |  | ||||||
|  |         original_path.pop(); | ||||||
|  |  | ||||||
|  |         original_path.push(self.uri.clone()); | ||||||
|  |  | ||||||
|  |         Photo { | ||||||
|  |             text_content: text_content.clone(), | ||||||
|  |             img_path, | ||||||
|  |             original_img_path: original_path.display().to_string() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Deserialize, Debug)] | ||||||
|  | pub struct InstagramParticipant { | ||||||
|  |     pub name: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Deserialize, Debug)] | ||||||
|  | struct InstagramDataJson { | ||||||
|  |     messages: Vec<InstagramMessage>, | ||||||
|  |     participants: Vec<InstagramParticipant> | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct InstagramData { | ||||||
|  |     pub project_path: String, | ||||||
|  |     pub participants: Vec<InstagramParticipant>, | ||||||
|  |     pub messages: Vec<InstagramMessage>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct Instagram { | ||||||
|  |     pub data: Option<InstagramData>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[async_trait] | ||||||
|  | impl Service for Instagram { | ||||||
|  |     fn get_name(&self) -> &'static str { | ||||||
|  |         "Instagram" | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn setup(&self) -> Box<dyn Service> { | ||||||
|  |         let export_path: String = Input::new() | ||||||
|  |             .with_prompt("Enter path for your instagram export. (your_instagram_activity folder)") | ||||||
|  |             .interact() | ||||||
|  |             .unwrap(); | ||||||
|  |  | ||||||
|  |         let mut path = PathBuf::from(export_path); | ||||||
|  |  | ||||||
|  |         path.push(PathBuf::from("messages/inbox")); | ||||||
|  |  | ||||||
|  |         let options: Vec<String> = fs::read_dir(&path).unwrap_or_else(|_| { | ||||||
|  |             eprintln!("Folder not found!"); | ||||||
|  |             exit(1); | ||||||
|  |         }) | ||||||
|  |             .filter_map(|entry| entry.ok()) | ||||||
|  |             .filter(|entry| entry.path().is_dir()) | ||||||
|  |             .map(|entry| { String::from(entry.file_name().to_str().unwrap()) }).collect(); | ||||||
|  |  | ||||||
|  |         let option = Select::new() | ||||||
|  |             .with_prompt("Choose a chat") | ||||||
|  |             .items(&options) | ||||||
|  |             .default(0) | ||||||
|  |             .interact() | ||||||
|  |             .unwrap(); | ||||||
|  |  | ||||||
|  |         let selected = options.get(option).unwrap().clone(); | ||||||
|  |  | ||||||
|  |         path.push(selected.clone()); | ||||||
|  |  | ||||||
|  |         Box::new(Instagram { | ||||||
|  |             data: Some(InstagramData { | ||||||
|  |                 project_path: path.display().to_string(), | ||||||
|  |                 participants: vec![], | ||||||
|  |                 messages: vec![], | ||||||
|  |             }), | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async fn load(&mut self) -> io::Result<()> { | ||||||
|  |         let data = self.data.as_mut().unwrap(); | ||||||
|  |         let path = PathBuf::from(&data.project_path); | ||||||
|  |  | ||||||
|  |         let paths: Vec<PathBuf> = fs::read_dir(&path).unwrap_or_else(|_| { | ||||||
|  |             eprintln!("Folder not found!"); | ||||||
|  |             exit(1); | ||||||
|  |         }) | ||||||
|  |             .filter_map(|entry| entry.ok()) | ||||||
|  |             .filter(|entry| entry.path().is_file()) | ||||||
|  |             .map(|entry| { entry.path() }).collect(); | ||||||
|  |  | ||||||
|  |         for (index, path) in paths.iter().enumerate() { | ||||||
|  |             if !path.exists() { | ||||||
|  |                 return Err(io::Error::new( | ||||||
|  |                     io::ErrorKind::NotFound, | ||||||
|  |                     format!( | ||||||
|  |                         "File does not exist: {}", | ||||||
|  |                         path.to_str().unwrap(), | ||||||
|  |                     ), | ||||||
|  |                 )); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             let mut file = File::open(path).await?; | ||||||
|  |             let mut contents = String::new(); | ||||||
|  |             file.read_to_string(&mut contents).await?; | ||||||
|  |  | ||||||
|  |             let jsondata: InstagramDataJson = serde_json::from_str(&contents) | ||||||
|  |                 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; | ||||||
|  |  | ||||||
|  |             if index == 0{ | ||||||
|  |                 // Load participants | ||||||
|  |                 for participant in jsondata.participants { | ||||||
|  |                     data.participants.push(participant); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             for message in jsondata.messages { | ||||||
|  |                 data.messages.push(message); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn get_participants(&self) -> Vec<Participant> { | ||||||
|  |         let data = self.data.as_ref().unwrap(); | ||||||
|  |  | ||||||
|  |         data.participants.iter().map(|participant| { | ||||||
|  |             Participant { | ||||||
|  |                 name: decode_str(&participant.name) | ||||||
|  |             } | ||||||
|  |         }).collect() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap<Participant, User>) { | ||||||
|  |         let data = self.data.as_ref().unwrap(); | ||||||
|  |         let path = PathBuf::from(data.project_path.clone()); | ||||||
|  |  | ||||||
|  |         for message in &data.messages { | ||||||
|  |             let mut media_index_clone = raw_project.project.media_index.clone(); | ||||||
|  |             let msg = message.conv(&mut media_index_clone, path.clone(), user_map); | ||||||
|  |  | ||||||
|  |             let moved = raw_project.project.push_msg(&msg); | ||||||
|  |  | ||||||
|  |             if moved { | ||||||
|  |                 // Move the file to the media folder | ||||||
|  |                  | ||||||
|  |  | ||||||
|  |                 // Update the highest media index of the project if the message was merged | ||||||
|  |                 raw_project.project.media_index = media_index_clone; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             println!("{:?}", msg); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								src/services/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/services/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | pub mod service; | ||||||
|  | pub mod instagram; | ||||||
							
								
								
									
										15
									
								
								src/services/service.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/services/service.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | use std::{collections::HashMap, io}; | ||||||
|  | use async_trait::async_trait; | ||||||
|  |  | ||||||
|  | use crate::types::{participant::Participant, project::ProjectRaw, user::User}; | ||||||
|  |  | ||||||
|  | #[async_trait] | ||||||
|  | pub trait Service { | ||||||
|  |     fn get_name(&self) -> &'static str; | ||||||
|  |  | ||||||
|  |     fn setup(&self) -> Box<dyn Service>; | ||||||
|  |     async fn load(&mut self) -> io::Result<()>; | ||||||
|  |  | ||||||
|  |     fn get_participants(&self) -> Vec<Participant>; | ||||||
|  |     fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap<Participant, User>); | ||||||
|  | } | ||||||
							
								
								
									
										18
									
								
								src/types/message.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/types/message.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  |  | ||||||
|  | #[derive(Serialize, Deserialize, Debug, Clone)] | ||||||
|  | pub struct Message { | ||||||
|  |     pub sender_name: String, | ||||||
|  |     pub timestamp: usize, | ||||||
|  |     pub text_content: Option<String>, | ||||||
|  |     pub photos: Vec<Photo> | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Serialize, Deserialize, Debug, Clone)] | ||||||
|  | pub struct Photo { | ||||||
|  |     pub text_content: Option<String>, | ||||||
|  |     pub img_path: String, | ||||||
|  |  | ||||||
|  |     #[serde(skip_serializing)] | ||||||
|  |     pub original_img_path: String, | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								src/types/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/types/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | pub mod project; | ||||||
|  | pub mod user; | ||||||
|  | pub mod participant; | ||||||
|  | pub mod message; | ||||||
							
								
								
									
										4
									
								
								src/types/participant.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/types/participant.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | #[derive(Hash, PartialEq, Eq, Debug, Clone)] | ||||||
|  | pub struct Participant { | ||||||
|  |     pub name: String | ||||||
|  | } | ||||||
							
								
								
									
										92
									
								
								src/types/project.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/types/project.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,92 @@ | |||||||
|  | use std::{collections::HashSet, io::{self, Cursor, Read, Write}, path::PathBuf, process::exit}; | ||||||
|  |  | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use tokio::{fs::File, io::{AsyncReadExt, AsyncWriteExt, BufWriter}}; | ||||||
|  | use zip::{write::FileOptions, ZipArchive, ZipWriter}; | ||||||
|  |  | ||||||
|  | use super::{message::Message, user::User}; | ||||||
|  |  | ||||||
|  | #[derive(Serialize, Deserialize, Debug, Clone)] | ||||||
|  | pub struct Project { | ||||||
|  |     pub users: Vec<User>, | ||||||
|  |     pub messages: Vec<Message>, | ||||||
|  |     pub timestamps: HashSet<usize>, | ||||||
|  |     pub media_index: usize, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub struct ProjectRaw { | ||||||
|  |     pub project: Project, | ||||||
|  |     pub zip: ZipArchive<Cursor<Vec<u8>>> | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Project { | ||||||
|  |     pub async fn load(pathstr: String) -> io::Result<ProjectRaw> { | ||||||
|  |         let path = PathBuf::from(pathstr); | ||||||
|  |  | ||||||
|  |         if !path.exists() { | ||||||
|  |             return Err(io::Error::new(io::ErrorKind::NotFound, "File does not exist!")); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let mut file = File::open(path).await?; | ||||||
|  |         let mut buffer = Vec::new(); | ||||||
|  |         file.read_to_end(&mut buffer).await?; | ||||||
|  |  | ||||||
|  |         let cursor = Cursor::new(buffer); | ||||||
|  |         let archive = ZipArchive::new(cursor)?; | ||||||
|  |  | ||||||
|  |         let mut copy_archive = archive.clone(); | ||||||
|  |         let mut project_file = copy_archive.by_name("project.json").unwrap_or_else(|_| { | ||||||
|  |             eprintln!("Invalid project format!"); | ||||||
|  |             exit(1); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         let mut json_content = String::new(); | ||||||
|  |         project_file.read_to_string(&mut json_content)?; | ||||||
|  |  | ||||||
|  |         let data: Project = serde_json::from_str(&json_content) | ||||||
|  |             .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; | ||||||
|  |  | ||||||
|  |         Ok(ProjectRaw { | ||||||
|  |             project: data, | ||||||
|  |             zip: archive | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn save(&self, file_path: String) -> io::Result<()> { | ||||||
|  |         let contents = serde_json::to_string(&self) | ||||||
|  |             .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; | ||||||
|  |  | ||||||
|  |         let file = File::create(file_path).await?; | ||||||
|  |         let mut writer = BufWriter::new(file); | ||||||
|  |  | ||||||
|  |         let mut buffer = Cursor::new(Vec::new()); | ||||||
|  |  | ||||||
|  |         { | ||||||
|  |             let mut zip = ZipWriter::new(&mut buffer); | ||||||
|  |  | ||||||
|  |             let options = FileOptions::default().unix_permissions(0o755); | ||||||
|  |             zip.add_directory("media/", options)?; | ||||||
|  |  | ||||||
|  |             zip.start_file("project.json", options)?; | ||||||
|  |             zip.write_all(contents.as_bytes())?; | ||||||
|  |  | ||||||
|  |             zip.finish()?; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         writer.write_all(&buffer.into_inner()).await?; | ||||||
|  |         writer.flush().await?; | ||||||
|  |  | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn push_msg(&mut self, message: &Message) -> bool { | ||||||
|  |         if self.timestamps.contains(&message.timestamp) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         self.timestamps.insert(message.timestamp); | ||||||
|  |         self.messages.push(message.clone()); | ||||||
|  |  | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								src/types/user.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/types/user.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use tabled::Tabled; | ||||||
|  |  | ||||||
|  | #[derive(Serialize, Deserialize, Debug, Tabled, Clone)] | ||||||
|  | pub struct User { | ||||||
|  |     pub name: String, | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user