diff --git a/README.md b/README.md new file mode 100644 index 0000000..639baab --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# msgexport + +Simple tool for exporting and merging all your chats between your platforms. + +**This tool is not a chat app that has all your chats between the platforms. This tool only merges exported chat from different platforms to a unified structure** + +## Supported platforms + +- Instagram +- WhatsApp + +(more platforms will be implemented later) + +## How to use msgexport + +### Downloading msgexport + +Download latest build from releases or build it yourself. + +### Creating a project + +First you need to create a `msgexport` project. To do that you need to run: + +```bash +msgexport --new project.msge +``` + +.msge file is just a simple zip file that contains all of the messages and media. + +### Adding users to a project + +First you need to add users to the project. The users in a project are all of the participants that are in the chat. You the user count needs to have at least the amount of participants. You cannot have a participant in a chat that doesn't have a user. + +
+ Why is it this way + + Let say that on WhatsApp the person is called by his first name, lets say `Joe`. The same person can have his nickname as his name on Instagram. We need to know which user is who. This is why we create users in out project. We name them however we want and then when we start merging the messages we say that for example `LegendMaster123` is Joe. Then it will automatically assign all the messages from `LegendMaster123` to Joe. +
+ +```bash +msgexport --users project.msge +``` + +### Merging messages + +To merge a messages from a specific platform you need to run this: + +```bash +msgexport --add project.msge +``` + +Then continue to a documentation for the specific platform. + +- [Instagram](docs/services/instagram.md) +- [WhatsApp](docs/services/whatsapp.md) diff --git a/docs/services/whatsapp.md b/docs/services/whatsapp.md new file mode 100644 index 0000000..0797a54 --- /dev/null +++ b/docs/services/whatsapp.md @@ -0,0 +1,19 @@ +# WhatsApp + +## Export a chat from WhatsApp + +Go to your specified chat on your phone, click on the three dots at the right top and click **More** and **Export chat**. + +Then select **Include media** and move the exported zip to your computer and extract it. Then locate the folder and copy the relative path to it. (in the folder will be some media and one txt file) + +## Start merging + +Run + +```bash +msgexport --add project.msge +``` + +and then paste the relative path to the console. + +Then select what participant in the WhatsApp chat coresponds to a msgexport user. diff --git a/src/commands/export.rs b/src/commands/export.rs index df75b09..4aa96b6 100644 --- a/src/commands/export.rs +++ b/src/commands/export.rs @@ -1,12 +1,16 @@ +use std::io; use std::process::exit; use dialoguer::Select; +use tokio::fs; use tokio::{fs::File, io::AsyncWriteExt}; use crate::types::project::Project; +use crate::utils::html_builder::build_html; +use crate::utils::zip::extract_specific_folder; -pub async fn export(path: String) { - let raw_project = Project::load(path.clone()).await.unwrap_or_else(|e| { +pub async fn export(path: String) -> io::Result<()> { + let mut raw_project = Project::load(path.clone()).await.unwrap_or_else(|e| { eprintln!("Error while loading a project: {}", e); exit(1); }); @@ -23,12 +27,17 @@ pub async fn export(path: String) { match selected { 0 => { // HTML + let html_content = build_html(&raw_project.project); + let mut file = File::create("./export/index.html").await.expect("Error while creating a file."); + let _ = file.write_all(html_content.as_bytes()).await; + extract_specific_folder(&mut raw_project.zip, "media/", "export/media")?; } 1 => { // JSON (without media) let content = raw_project.project.to_projectjson(); let json_content = serde_json::to_string(&content).unwrap(); + fs::create_dir("export").await?; let mut file = File::create("./export.json").await.expect("Error while creating a file."); let _ = file.write_all(json_content.as_bytes()).await; } @@ -36,4 +45,6 @@ pub async fn export(path: String) { panic!("Invalid option"); } }; + + Ok(()) } diff --git a/src/main.rs b/src/main.rs index 0e1c691..ca47e5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod types; mod commands; mod services; +mod utils; use std::collections::HashSet; @@ -29,8 +30,8 @@ struct Cli { #[arg(long = "add", help = "Add messages from a service.", value_name = "PROJECT_FILE")] add: Option, - // #[arg(long = "export", help = "Export all the chats in specific format", value_name = "PROJECT_FILE")] - // export: Option, + #[arg(long = "export", help = "Export all the chats in specific format", value_name = "PROJECT_FILE")] + export: Option, } #[tokio::main] @@ -57,7 +58,9 @@ async fn main() { add(path).await; } - // if let Some(path) = cli.export { - // export(path).await; - // } + if let Some(path) = cli.export { + export(path).await.unwrap_or_else(|e| { + println!("Error: {}", e); + }); + } } diff --git a/src/services/instagram/types.rs b/src/services/instagram/types.rs index 1eee944..b95a7f6 100644 --- a/src/services/instagram/types.rs +++ b/src/services/instagram/types.rs @@ -46,7 +46,7 @@ impl InstagramMessage { }; let reactions: Vec = if let Some(reaction_list) = &self.reactions { - reaction_list.iter().map(|reaction| reaction.conv()).collect() + reaction_list.iter().map(|reaction| reaction.conv(user_map)).collect() } else { vec![] }; @@ -83,9 +83,20 @@ pub struct InstagramReaction { } impl InstagramReaction { - pub fn conv(&self) -> Reaction { + pub fn conv(&self, user_map: &HashMap) -> Reaction { + let participant = Participant { + name: decode_str(&self.actor) + }; + + let sender = user_map.get(&participant); + let sender_name = if let Some(sender_value) = sender { + sender_value.name.clone() + } else { + String::from("Unknown user") + }; + Reaction { - actor_name: decode_str(&self.actor), + actor_name: sender_name, content: decode_str(&self.reaction), timestamp: self.timestamp.unwrap_or(0) } diff --git a/src/services/whatsapp/whatsapp.rs b/src/services/whatsapp/whatsapp.rs index 9513634..902b5fd 100644 --- a/src/services/whatsapp/whatsapp.rs +++ b/src/services/whatsapp/whatsapp.rs @@ -25,7 +25,7 @@ pub fn parse_messages(input: &str) -> Vec { // Parse date as DD/MM/YY and convert to epoch let datetime_str = format!("{} {}", date, time); // DD/MM/YY HH:MM let datetime = NaiveDateTime::parse_from_str(&datetime_str, "%d/%m/%y %H:%M").unwrap(); - let epoch = datetime.and_utc().timestamp() as usize; + let epoch = (datetime.and_utc().timestamp() * 1000) as usize; // Save the previous message if there is one if let Some(msg) = current_message.take() { diff --git a/src/utils/html_builder.rs b/src/utils/html_builder.rs new file mode 100644 index 0000000..2cae50a --- /dev/null +++ b/src/utils/html_builder.rs @@ -0,0 +1,226 @@ +use crate::types::project::Project; + +pub fn build_html(project: &Project) -> String { + let users: Vec = project.users.iter().map(|u| u.name.clone()).collect(); + + let string = format!( + " + + + + + + + Chat with {} + + + +

Chat with {}

+
+ + + + +", + env!("CARGO_PKG_VERSION"), + users.join(", "), + users.join(", "), + serde_json::to_string(project).unwrap() + ); + + string +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..047682c --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod html_builder; +pub mod zip; diff --git a/src/utils/zip.rs b/src/utils/zip.rs new file mode 100644 index 0000000..4176e91 --- /dev/null +++ b/src/utils/zip.rs @@ -0,0 +1,39 @@ +use std::{fs::File, io::{self, Read}, path::PathBuf}; + +use zip::ZipArchive; + +pub fn extract_specific_folder( + zip_archive: &mut ZipArchive, + source_folder: &str, + destination_folder: &str, +) -> io::Result<()> { + for i in 0..zip_archive.len() { + let mut file = zip_archive.by_index(i)?; + let file_name = file.name(); + + // Check if the file is in the desired folder + if file_name.starts_with(source_folder) { + // Remove the source folder prefix + let relative_path = file_name.strip_prefix(source_folder).unwrap_or(file_name); + + // Create the destination path + let mut dest_path = PathBuf::from(destination_folder); + dest_path.push(relative_path); + + if file.is_dir() { + // Create the directory + std::fs::create_dir_all(&dest_path)?; + } else { + // Create the destination directory if it doesn't exist + if let Some(parent) = dest_path.parent() { + std::fs::create_dir_all(parent)?; + } + + // Write the file to the destination + let mut outfile = File::create(&dest_path)?; + io::copy(&mut file, &mut outfile)?; + } + } + } + Ok(()) +}