From 05b31e426e8eaf9c10ea9c1ed84d605f8efacb9c Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Sun, 8 Dec 2024 20:04:45 +0100 Subject: [PATCH] feat: Implemented a whatsapp and minor changes --- Cargo.lock | 153 ++++++++++++++ Cargo.toml | 1 + src/commands/add.rs | 32 ++- src/commands/export.rs | 39 ++++ src/commands/mod.rs | 1 + src/commands/users.rs | 17 +- src/main.rs | 11 +- src/services/{ => instagram}/instagram.rs | 88 ++------ src/services/instagram/mod.rs | 2 + src/services/instagram/types.rs | 168 ++++++++++++++++ src/services/mod.rs | 2 + src/services/whatsapp/mod.rs | 2 + src/services/whatsapp/types.rs | 125 ++++++++++++ src/services/whatsapp/whatsapp.rs | 232 ++++++++++++++++++++++ src/types/message.rs | 37 +++- src/types/project.rs | 13 ++ 16 files changed, 840 insertions(+), 83 deletions(-) create mode 100644 src/commands/export.rs rename src/services/{ => instagram}/instagram.rs (73%) create mode 100644 src/services/instagram/mod.rs create mode 100644 src/services/instagram/types.rs create mode 100644 src/services/whatsapp/mod.rs create mode 100644 src/services/whatsapp/types.rs create mode 100644 src/services/whatsapp/whatsapp.rs diff --git a/Cargo.lock b/Cargo.lock index 1fd73ea..33a06e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -139,6 +154,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytecount" version = "0.6.8" @@ -195,6 +216,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "cipher" version = "0.4.4" @@ -270,6 +305,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.16" @@ -488,6 +529,29 @@ dependencies = [ "digest", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "inout" version = "0.1.3" @@ -518,6 +582,16 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -546,6 +620,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + [[package]] name = "memchr" version = "2.7.4" @@ -577,6 +657,7 @@ name = "msgexport" version = "0.1.0" dependencies = [ "async-trait", + "chrono", "clap", "dialoguer", "encoding", @@ -595,6 +676,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.5" @@ -1081,6 +1171,69 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.90", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 0f4a38c..b521bbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,4 @@ encoding = "0.2" zip = "0.6" tokio-util = "0.7" regex = "1.10" +chrono = "0.4.38" diff --git a/src/commands/add.rs b/src/commands/add.rs index b47fcfc..58f3330 100644 --- a/src/commands/add.rs +++ b/src/commands/add.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, process::exit}; use dialoguer::Select; -use crate::{services::{instagram::Instagram, service::Service}, types::{participant::Participant, project::Project, user::User}}; +use crate::{services::{instagram::instagram::Instagram, service::Service, whatsapp::whatsapp::WhatsApp}, types::{participant::Participant, project::Project, user::User}}; pub async fn add(path: String) { let mut raw_project = Project::load(path.clone()).await.unwrap_or_else(|e| { @@ -19,6 +19,9 @@ pub async fn add(path: String) { Box::new(Instagram { data: None }), + Box::new(WhatsApp { + data: None + }), ]; let options: Vec<&'static str> = services.iter().map(|service| service.get_name()).collect(); @@ -32,30 +35,41 @@ pub async fn add(path: String) { let service = services.get(selected).unwrap(); let mut final_service = service.setup(); - let _ = final_service.load().await; + + println!("Loading the project file"); + final_service.load().await.unwrap_or_else(|e| { + println!("Error while loading {} export: {}", final_service.get_name(), e); + exit(1); + }); let participants = final_service.get_participants(); + if participants.len() > raw_project.project.users.len() { + eprintln!("You have {} users in your project but in the {} chat there are {} participants!", raw_project.project.users.len(), final_service.get_name(), participants.len()); + println!("You need to have equal amount or more users in project that participants in chat!"); + exit(1); + } + let mut user_map: HashMap = 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 = participants.iter().filter(|participant| { - !user_map.contains_key(&participant) + for participant in participants { + let users: Vec = raw_project.project.users.iter().filter(|user| { + !user_map.values().any(|x| x.name == user.name) }).map(|e| e.clone()).collect(); - let options: Vec = parts.iter().map(|part| part.name.clone()).collect(); + let options: Vec = users.iter().map(|usr| usr.name.clone()).collect(); let selected = Select::new() - .with_prompt(format!("Select a {} participant for user '{}'", service.get_name(), user.name)) + .with_prompt(format!("Select a {} user for participant '{}'", service.get_name(), participant.name)) .items(&options) .default(0) .interact() .unwrap(); - let selected_part = parts.get(selected).unwrap().clone(); + let selected_user = users.get(selected).unwrap().clone(); - user_map.insert(selected_part.clone(), user); + user_map.insert(participant, selected_user); } println!("Starting to merge messages..."); diff --git a/src/commands/export.rs b/src/commands/export.rs new file mode 100644 index 0000000..df75b09 --- /dev/null +++ b/src/commands/export.rs @@ -0,0 +1,39 @@ +use std::process::exit; + +use dialoguer::Select; +use tokio::{fs::File, io::AsyncWriteExt}; + +use crate::types::project::Project; + +pub async fn export(path: String) { + let raw_project = Project::load(path.clone()).await.unwrap_or_else(|e| { + eprintln!("Error while loading a project: {}", e); + exit(1); + }); + + let options = vec!["HTML", "JSON (without media)"]; + + let selected = Select::new() + .with_prompt("Choose a output type") + .items(&options) + .default(0) + .interact() + .unwrap(); + + match selected { + 0 => { + // HTML + } + 1 => { + // JSON (without media) + let content = raw_project.project.to_projectjson(); + let json_content = serde_json::to_string(&content).unwrap(); + + let mut file = File::create("./export.json").await.expect("Error while creating a file."); + let _ = file.write_all(json_content.as_bytes()).await; + } + _ => { + panic!("Invalid option"); + } + }; +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f954e1c..1dae372 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,2 +1,3 @@ pub mod users; pub mod add; +pub mod export; diff --git a/src/commands/users.rs b/src/commands/users.rs index d193d9c..603906d 100644 --- a/src/commands/users.rs +++ b/src/commands/users.rs @@ -1,4 +1,4 @@ -use std::process::exit; +use std::{collections::HashSet, process::exit}; use dialoguer::{Input, Select}; use tabled::Table; @@ -72,6 +72,21 @@ pub async fn menu(mut project_raw: ProjectRaw, path: String) { .default(0) .interact() .unwrap(); + + let delete_name = options_users.get(option_remove).unwrap(); + + let mut participants_in_chat: HashSet = HashSet::new(); + for message in project.messages.clone() { + participants_in_chat.insert(message.sender_name); + + for reaction in message.reactions { + participants_in_chat.insert(reaction.actor_name); + } + } + + if participants_in_chat.contains(delete_name) { + eprintln!("This user has some messages in chat, therefore cannot be removed!") + } project.users.remove(option_remove); diff --git a/src/main.rs b/src/main.rs index 7b97371..0e1c691 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ mod services; use std::collections::HashSet; use clap::{command, ArgGroup, Parser}; -use commands::{add::add, users::users}; +use commands::{add::add, export::export, users::users}; use types::project::Project; #[derive(Parser, Debug)] @@ -15,7 +15,7 @@ use types::project::Project; about = "Export and merge your messages between platforms", author = "Jakub Žitník", group = ArgGroup::new("command") - .args(&["new", "users", "add"]) + .args(&["new", "users", "add", "export"]) .multiple(false) .required(true) )] @@ -28,6 +28,9 @@ 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, } #[tokio::main] @@ -53,4 +56,8 @@ async fn main() { if let Some(path) = cli.add { add(path).await; } + + // if let Some(path) = cli.export { + // export(path).await; + // } } diff --git a/src/services/instagram.rs b/src/services/instagram/instagram.rs similarity index 73% rename from src/services/instagram.rs rename to src/services/instagram/instagram.rs index d8f7d38..cacb9c4 100644 --- a/src/services/instagram.rs +++ b/src/services/instagram/instagram.rs @@ -7,81 +7,15 @@ 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 crate::{services::service::Service, types::{participant::Participant, project::ProjectRaw, user::User}}; -use super::service::Service; +use super::types::{InstagramMessage, InstagramParticipant}; -fn decode_str(input: &String) -> String { +pub 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, - photos: Option> -} - -impl InstagramMessage { - pub fn conv(&self, media_index: &mut usize, project_path: PathBuf, user_map: &HashMap) -> Message { - let text_content = if let Some(content) = &self.content { - Some(decode_str(content)) - } else { - None - }; - - let photos: Vec = 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, media_index: &mut usize, mut original_path: PathBuf) -> Photo { - *media_index += 1; - let img_path = (*media_index - 1).to_string(); - - for _ in 0..4 { - if !original_path.pop() { - break; - } - } - - original_path.push(self.uri.clone()); - - Photo { - text_content: text_content.clone(), - img_path, - original_img_path: Some(original_path.display().to_string()) - } - } -} - -#[derive(Deserialize, Debug)] -pub struct InstagramParticipant { - pub name: String, -} - #[derive(Deserialize, Debug)] struct InstagramDataJson { messages: Vec, @@ -212,7 +146,7 @@ impl Service for Instagram { let moved = raw_project.project.push_msg(&msg); if moved { - // Move the file to the media folder + // Move media files to the media folder for photo in msg.photos.clone() { let original_path = photo.original_img_path.unwrap().clone(); let path = format!("media/{}", photo.img_path); @@ -220,6 +154,20 @@ impl Service for Instagram { media_files.push(photovalues); } + for video in msg.videos.clone() { + let original_path = video.original_vid_path.unwrap().clone(); + let path = format!("media/{}", video.vid_path); + let photovalues = (path, original_path); + media_files.push(photovalues); + } + + for audio in msg.audio.clone() { + let original_path = audio.original_audio_path.unwrap().clone(); + let path = format!("media/{}", audio.audio_path); + let photovalues = (path, original_path); + media_files.push(photovalues); + } + // Update the highest media index of the project if the message was merged raw_project.project.media_index = media_index_clone; } diff --git a/src/services/instagram/mod.rs b/src/services/instagram/mod.rs new file mode 100644 index 0000000..2622842 --- /dev/null +++ b/src/services/instagram/mod.rs @@ -0,0 +1,2 @@ +pub mod types; +pub mod instagram; diff --git a/src/services/instagram/types.rs b/src/services/instagram/types.rs new file mode 100644 index 0000000..1eee944 --- /dev/null +++ b/src/services/instagram/types.rs @@ -0,0 +1,168 @@ +use std::{collections::HashMap, path::PathBuf}; +use serde::Deserialize; +use crate::types::{message::{Audio, Message, Photo, Reaction, Video}, participant::Participant, user::User}; +use super::instagram::decode_str; + +#[derive(Deserialize, Debug)] +pub struct InstagramParticipant { + pub name: String, +} + +#[derive(Deserialize, Debug)] +pub struct InstagramMessage { + sender_name: String, + timestamp_ms: usize, + content: Option, + photos: Option>, + videos: Option>, + audio_files: Option>, + reactions: Option> +} + +impl InstagramMessage { + pub fn conv(&self, media_index: &mut usize, project_path: PathBuf, user_map: &HashMap) -> Message { + let text_content = if let Some(content) = &self.content { + Some(decode_str(content)) + } else { + None + }; + + let photos: Vec = 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 videos: Vec