feat: Fully implemented instagram service
This commit is contained in:
parent
386e8abe5b
commit
7c23064afe
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
project.zip
|
||||||
|
39
Cargo.lock
generated
39
Cargo.lock
generated
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>);
|
||||||
}
|
}
|
||||||
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user