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
project.zip

39
Cargo.lock generated
View File

@ -28,6 +28,15 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.18"
@ -571,6 +580,7 @@ dependencies = [
"clap",
"dialoguer",
"encoding",
"regex",
"serde",
"serde_json",
"tabled",
@ -732,6 +742,35 @@ dependencies = [
"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]]
name = "rustc-demangle"
version = "0.1.24"

View File

@ -14,3 +14,4 @@ async-trait = "0.1.48"
encoding = "0.2"
zip = "0.6"
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}};
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);
exit(1);
});
@ -59,5 +59,10 @@ pub async fn add(path: String) {
}
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 tabled::Table;
use crate::types::{project::Project, user::User};
use crate::types::{project::{Project, ProjectRaw}, 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;
}
@ -24,12 +24,13 @@ pub fn print_users(users: &Vec<User>) {
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:");
print_users(&project.users);
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()
.with_prompt("Choose a option")
@ -52,12 +53,12 @@ pub async fn menu(mut project: Project, path: String) {
project.users.push(user);
Box::pin(menu(project, path)).await;
Box::pin(menu(project_raw, 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;
Box::pin(menu(project_raw, path)).await;
return;
}
@ -74,15 +75,18 @@ pub async fn menu(mut project: Project, path: String) {
project.users.remove(option_remove);
Box::pin(menu(project, path)).await;
Box::pin(menu(project_raw, path)).await;
},
2 => {
// Save and exit
let _ = project.save(path).await;
let _ = project_raw.save(&path).await;
},
3 => {
// Exit without saving
}
_ => {
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(),
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 {

View File

@ -1,5 +1,6 @@
use std::{collections::HashMap, fs, io, path::PathBuf, process::exit};
use regex::Regex;
use async_trait::async_trait;
use dialoguer::{Input, Select};
use encoding::{all::ISO_8859_1, EncoderTrap, Encoding};
@ -24,7 +25,6 @@ pub struct 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 {
let text_content = if let Some(content) = &self.content {
Some(decode_str(content))
@ -61,14 +61,18 @@ impl InstagramPhoto {
*media_index += 1;
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());
Photo {
text_content: text_content.clone(),
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 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(|_| {
eprintln!("Folder not found!");
exit(1);
})
.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();
for (index, path) in paths.iter().enumerate() {
@ -168,7 +175,7 @@ impl Service for Instagram {
let jsondata: InstagramDataJson = serde_json::from_str(&contents)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
if index == 0{
if index == 0 {
// Load participants
for participant in jsondata.participants {
data.participants.push(participant);
@ -193,9 +200,10 @@ impl Service for Instagram {
}).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 path = PathBuf::from(data.project_path.clone());
let mut media_files: Vec<(String, String)> = vec![];
for message in &data.messages {
let mut media_index_clone = raw_project.project.media_index.clone();
@ -205,13 +213,19 @@ impl Service for Instagram {
if moved {
// 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
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<()>;
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 img_path: String,
#[serde(skip_serializing)]
pub original_img_path: String,
#[serde(skip)]
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)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
@ -89,4 +89,107 @@ impl Project {
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(())
}
}