feat: Fully implemented instagram service

This commit is contained in:
jzitnik-dev 2024-12-08 13:06:14 +01:00
parent 386e8abe5b
commit 7c23064afe
Signed by: jzitnik
GPG Key ID: C577A802A6AF4EF3
11 changed files with 192 additions and 24 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target /target
project.zip

39
Cargo.lock generated
View File

@ -28,6 +28,15 @@ dependencies = [
"cpufeatures", "cpufeatures",
] ]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.18"
@ -571,6 +580,7 @@ dependencies = [
"clap", "clap",
"dialoguer", "dialoguer",
"encoding", "encoding",
"regex",
"serde", "serde",
"serde_json", "serde_json",
"tabled", "tabled",
@ -732,6 +742,35 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.24" version = "0.1.24"

View File

@ -14,3 +14,4 @@ async-trait = "0.1.48"
encoding = "0.2" encoding = "0.2"
zip = "0.6" zip = "0.6"
tokio-util = "0.7" tokio-util = "0.7"
regex = "1.10"

BIN
file.zip

Binary file not shown.

View File

@ -5,7 +5,7 @@ use dialoguer::Select;
use crate::{services::{instagram::Instagram, service::Service}, types::{participant::Participant, project::Project, user::User}}; use crate::{services::{instagram::Instagram, service::Service}, types::{participant::Participant, project::Project, user::User}};
pub async fn add(path: String) { pub async fn add(path: String) {
let mut raw_project = Project::load(path).await.unwrap_or_else(|e| { let mut raw_project = Project::load(path.clone()).await.unwrap_or_else(|e| {
eprintln!("Error while loading a project: {}", e); eprintln!("Error while loading a project: {}", e);
exit(1); exit(1);
}); });
@ -59,5 +59,10 @@ pub async fn add(path: String) {
} }
println!("Starting to merge messages..."); println!("Starting to merge messages...");
final_service.merge_messages(&mut raw_project, &user_map) final_service.merge_messages(&mut raw_project, &user_map).await;
raw_project.project.sort_messages();
println!("Saving all messages to a project file.");
let _ = raw_project.save(&path).await;
} }

View File

@ -3,13 +3,13 @@ use std::process::exit;
use dialoguer::{Input, Select}; use dialoguer::{Input, Select};
use tabled::Table; use tabled::Table;
use crate::types::{project::Project, user::User}; use crate::types::{project::{Project, ProjectRaw}, user::User};
pub async fn users(path: String) { pub async fn users(path: String) {
let project = Project::load(path.clone()).await.unwrap_or_else(|e| { let project = Project::load(path.clone()).await.unwrap_or_else(|e| {
eprintln!("Error while loading a project: {}", e); eprintln!("Error while loading a project: {}", e);
exit(1); exit(1);
}).project; });
menu(project, path).await; menu(project, path).await;
} }
@ -24,12 +24,13 @@ pub fn print_users(users: &Vec<User>) {
println!("{}", users); println!("{}", users);
} }
pub async fn menu(mut project: Project, path: String) { pub async fn menu(mut project_raw: ProjectRaw, path: String) {
let project = &mut project_raw.project;
println!("User management:"); println!("User management:");
print_users(&project.users); print_users(&project.users);
println!("\n"); println!("\n");
let options = vec!["Create a new user", "Remove a user", "Save and exit"]; let options = vec!["Create a new user", "Remove a user", "Save and exit", "Exit without saving"];
let option = Select::new() let option = Select::new()
.with_prompt("Choose a option") .with_prompt("Choose a option")
@ -52,12 +53,12 @@ pub async fn menu(mut project: Project, path: String) {
project.users.push(user); project.users.push(user);
Box::pin(menu(project, path)).await; Box::pin(menu(project_raw, path)).await;
}, },
1 => { 1 => {
if project.users.is_empty() { if project.users.is_empty() {
println!("You do not have any users created! No users to be removed!"); println!("You do not have any users created! No users to be removed!");
Box::pin(menu(project, path)).await; Box::pin(menu(project_raw, path)).await;
return; return;
} }
@ -74,15 +75,18 @@ pub async fn menu(mut project: Project, path: String) {
project.users.remove(option_remove); project.users.remove(option_remove);
Box::pin(menu(project, path)).await; Box::pin(menu(project_raw, path)).await;
}, },
2 => { 2 => {
// Save and exit // Save and exit
let _ = project.save(path).await; let _ = project_raw.save(&path).await;
}, },
3 => {
// Exit without saving
}
_ => { _ => {
println!("Error! Unknown option!"); println!("Error! Unknown option!");
Box::pin(menu(project, path)).await; Box::pin(menu(project_raw, path)).await;
} }
} }
} }

View File

@ -41,7 +41,8 @@ async fn main() {
timestamps: HashSet::new(), timestamps: HashSet::new(),
media_index: 0, media_index: 0,
}; };
let _ = new_project.save(path).await; let _ = new_project.save_new(path).await;
println!("New project was successfully created!");
} }
if let Some(path) = cli.users { if let Some(path) = cli.users {

View File

@ -1,5 +1,6 @@
use std::{collections::HashMap, fs, io, path::PathBuf, process::exit}; use std::{collections::HashMap, fs, io, path::PathBuf, process::exit};
use regex::Regex;
use async_trait::async_trait; use async_trait::async_trait;
use dialoguer::{Input, Select}; use dialoguer::{Input, Select};
use encoding::{all::ISO_8859_1, EncoderTrap, Encoding}; use encoding::{all::ISO_8859_1, EncoderTrap, Encoding};
@ -24,7 +25,6 @@ pub struct InstagramMessage {
} }
impl InstagramMessage { 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 { 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 { let text_content = if let Some(content) = &self.content {
Some(decode_str(content)) Some(decode_str(content))
@ -61,14 +61,18 @@ impl InstagramPhoto {
*media_index += 1; *media_index += 1;
let img_path = (*media_index - 1).to_string(); let img_path = (*media_index - 1).to_string();
original_path.pop(); for _ in 0..4 {
if !original_path.pop() {
break;
}
}
original_path.push(self.uri.clone()); original_path.push(self.uri.clone());
Photo { Photo {
text_content: text_content.clone(), text_content: text_content.clone(),
img_path, img_path,
original_img_path: original_path.display().to_string() original_img_path: Some(original_path.display().to_string())
} }
} }
} }
@ -142,12 +146,15 @@ impl Service for Instagram {
let data = self.data.as_mut().unwrap(); let data = self.data.as_mut().unwrap();
let path = PathBuf::from(&data.project_path); let path = PathBuf::from(&data.project_path);
let pattern = r".*/message_.+\.json$";
let re = Regex::new(pattern).unwrap();
let paths: Vec<PathBuf> = fs::read_dir(&path).unwrap_or_else(|_| { let paths: Vec<PathBuf> = fs::read_dir(&path).unwrap_or_else(|_| {
eprintln!("Folder not found!"); eprintln!("Folder not found!");
exit(1); exit(1);
}) })
.filter_map(|entry| entry.ok()) .filter_map(|entry| entry.ok())
.filter(|entry| entry.path().is_file()) .filter(|entry| entry.path().is_file() && re.is_match(&entry.path().display().to_string()))
.map(|entry| { entry.path() }).collect(); .map(|entry| { entry.path() }).collect();
for (index, path) in paths.iter().enumerate() { for (index, path) in paths.iter().enumerate() {
@ -168,7 +175,7 @@ impl Service for Instagram {
let jsondata: InstagramDataJson = serde_json::from_str(&contents) let jsondata: InstagramDataJson = serde_json::from_str(&contents)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
if index == 0{ if index == 0 {
// Load participants // Load participants
for participant in jsondata.participants { for participant in jsondata.participants {
data.participants.push(participant); data.participants.push(participant);
@ -193,9 +200,10 @@ impl Service for Instagram {
}).collect() }).collect()
} }
fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap<Participant, User>) { async fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap<Participant, User>) {
let data = self.data.as_ref().unwrap(); let data = self.data.as_ref().unwrap();
let path = PathBuf::from(data.project_path.clone()); let path = PathBuf::from(data.project_path.clone());
let mut media_files: Vec<(String, String)> = vec![];
for message in &data.messages { for message in &data.messages {
let mut media_index_clone = raw_project.project.media_index.clone(); let mut media_index_clone = raw_project.project.media_index.clone();
@ -205,13 +213,19 @@ impl Service for Instagram {
if moved { if moved {
// Move the file to the media folder // Move the file 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);
let photovalues = (path, original_path);
media_files.push(photovalues);
}
// Update the highest media index of the project if the message was merged // Update the highest media index of the project if the message was merged
raw_project.project.media_index = media_index_clone; raw_project.project.media_index = media_index_clone;
} }
println!("{:?}", msg);
} }
let _ = raw_project.save_media_files(media_files).await;
} }
} }

View File

@ -11,5 +11,5 @@ pub trait Service {
async fn load(&mut self) -> io::Result<()>; async fn load(&mut self) -> io::Result<()>;
fn get_participants(&self) -> Vec<Participant>; fn get_participants(&self) -> Vec<Participant>;
fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap<Participant, User>); async fn merge_messages(&self, raw_project: &mut ProjectRaw, user_map: &HashMap<Participant, User>);
} }

View File

@ -13,6 +13,6 @@ pub struct Photo {
pub text_content: Option<String>, pub text_content: Option<String>,
pub img_path: String, pub img_path: String,
#[serde(skip_serializing)] #[serde(skip)]
pub original_img_path: String, pub original_img_path: Option<String>,
} }

View File

@ -52,7 +52,7 @@ impl Project {
}) })
} }
pub async fn save(&self, file_path: String) -> io::Result<()> { pub async fn save_new(&self, file_path: String) -> io::Result<()> {
let contents = serde_json::to_string(&self) let contents = serde_json::to_string(&self)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
@ -89,4 +89,107 @@ impl Project {
return true; return true;
} }
pub fn sort_messages(&mut self) {
self.messages.sort_by_key(|msg| msg.timestamp);
}
}
impl ProjectRaw {
/// Save multiple files to the zip archive.
///
/// # Arguments
/// * `files` - A list of tuples where each tuple contains:
/// - `target_path`: The path inside the zip archive (e.g., `media/1`).
/// - `source_path`: The path to the file on the filesystem.
pub async fn save_media_files(
&mut self,
files: Vec<(String, String)>,
) -> io::Result<()> {
let mut new_buffer = Cursor::new(Vec::new());
{
let mut zip_writer = ZipWriter::new(&mut new_buffer);
let options = FileOptions::default().unix_permissions(0o644);
let replace_files: HashSet<_> = files.iter().map(|(target, _)| target.clone()).collect();
for i in 0..self.zip.len() {
let mut file = self.zip.by_index(i)?;
let name = file.name().to_string();
if replace_files.contains(&name) {
continue;
}
zip_writer.start_file(name, options)?;
let mut file_content = Vec::new();
file.read_to_end(&mut file_content)?;
zip_writer.write_all(&file_content)?;
}
for (target_path, source_path) in files {
let mut file = File::open(source_path).await?;
let mut file_content = Vec::new();
file.read_to_end(&mut file_content).await?;
zip_writer.start_file(target_path, options)?;
zip_writer.write_all(&file_content)?;
}
zip_writer.finish()?;
}
self.zip = ZipArchive::new(new_buffer)?;
Ok(())
}
/// Save the `Project` to `project.json` and flush the zip archive to the specified file.
///
/// # Arguments
/// * `output_path` - The path to save the zip archive to.
pub async fn save(&mut self, output_path: &str) -> io::Result<()> {
let project_json = serde_json::to_string(&self.project)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let mut new_buffer = Cursor::new(Vec::new());
{
let mut zip_writer = ZipWriter::new(&mut new_buffer);
let options = FileOptions::default().unix_permissions(0o644);
let mut project_json_written = false;
for i in 0..self.zip.len() {
let mut file = self.zip.by_index(i)?;
let name = file.name();
if name == "project.json" {
// Replace the existing `project.json`.
zip_writer.start_file("project.json", options)?;
zip_writer.write_all(project_json.as_bytes())?;
project_json_written = true;
} else {
// Copy other files as-is.
zip_writer.start_file(name, options)?;
let mut file_content = Vec::new();
file.read_to_end(&mut file_content)?;
zip_writer.write_all(&file_content)?;
}
}
if !project_json_written {
zip_writer.start_file("project.json", options)?;
zip_writer.write_all(project_json.as_bytes())?;
}
zip_writer.finish()?;
}
let mut output_file = File::create(output_path).await?;
output_file.write_all(&new_buffer.into_inner()).await?;
output_file.flush().await?;
Ok(())
}
} }