From a66180d817012f00754841c111b92c6d14516c34 Mon Sep 17 00:00:00 2001 From: Neshura Date: Fri, 29 Dec 2023 22:35:16 +0100 Subject: [PATCH 01/17] Remove Result from Config loading --- src/config/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 4eddd4b..7a299b6 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -13,7 +13,7 @@ pub(crate) struct Config { } impl Config { - pub(crate) fn load() -> Result { + pub(crate) fn load() -> Self { let cfg: Self = match confy::load(env!("CARGO_PKG_NAME"), "config") { Ok(data) => data, Err(e) => panic!("config.toml not found: {e}"), @@ -36,7 +36,7 @@ impl Config { panic!("'Description' type Post Body only supported for Volumes!") } }); - Ok(cfg) + cfg } } From 9cca302018a712b5bffe7a0b666e49c1f31ccd0f Mon Sep 17 00:00:00 2001 From: Neshura Date: Fri, 29 Dec 2023 22:40:07 +0100 Subject: [PATCH 02/17] Move config module out of subdirectory --- src/{config/mod.rs => config.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{config/mod.rs => config.rs} (100%) diff --git a/src/config/mod.rs b/src/config.rs similarity index 100% rename from src/config/mod.rs rename to src/config.rs From ecc05a2812563b1165e4c761f0695c8ce5373c1d Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 00:22:21 +0100 Subject: [PATCH 03/17] Make config username + password fields private --- src/config.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 7a299b6..bed8cbb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,12 @@ +use lemmy_api_common::sensitive::Sensitive; use serde_derive::{Deserialize, Serialize}; use crate::config::PostBody::Description; #[derive(Serialize, Deserialize, Clone, Debug)] pub(crate) struct Config { pub(crate) instance: String, - pub(crate) username: String, - pub(crate) password: String, + username: String, + password: String, pub(crate) status_post_url: Option, pub(crate) config_reload_seconds: u32, pub(crate) protected_communities: Vec, @@ -38,6 +39,14 @@ impl Config { }); cfg } + + pub(crate) fn get_username(&self) -> Sensitive { + Sensitive::new(self.username.clone()) + } + + pub(crate) fn get_password(&self) -> Sensitive { + Sensitive::new(self.password.clone()) + } } impl Default for Config { From c0bff031208dcd3fc9d0f777bb3f7efda97ee785 Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 00:23:01 +0100 Subject: [PATCH 04/17] Move lemmy module out of subdir, port logging from tui to println/journald --- src/{lemmy/mod.rs => lemmy.rs} | 156 ++++++++++++++++++++++----------- 1 file changed, 103 insertions(+), 53 deletions(-) rename src/{lemmy/mod.rs => lemmy.rs} (61%) diff --git a/src/lemmy/mod.rs b/src/lemmy.rs similarity index 61% rename from src/lemmy/mod.rs rename to src/lemmy.rs index 8265942..38148b2 100644 --- a/src/lemmy/mod.rs +++ b/src/lemmy.rs @@ -8,49 +8,31 @@ use lemmy_db_schema::newtypes::{CommunityId, PostId}; use lemmy_db_schema::{ListingType, PostFeatureType}; use reqwest::StatusCode; use crate::config::Config; -use crate::HTTP_CLIENT; - -pub(crate) struct Credentials { - username: String, - password: String -} - -impl Credentials { - pub(crate) fn get_username(&self) -> Sensitive { - Sensitive::new(self.username.clone()) - } - - pub(crate) fn get_password(&self) -> Sensitive { - Sensitive::new(self.password.clone()) - } - - pub(crate) fn set_credentials(config: &Config) -> Self { - Self { - username: config.username.clone(), - password: config.password.clone(), - } - } -} +use crate::{HTTP_CLIENT, write_error}; pub(crate) struct Lemmy { jwt_token: Sensitive, instance: String, } -pub(crate) async fn login(credentials: &Credentials, instance: &str) -> Result { +pub(crate) async fn login(config: &Config) -> Result { let login_params = Login { - username_or_email: credentials.get_username(), - password: credentials.get_password(), + username_or_email: config.get_username(), + password: config.get_password(), totp_2fa_token: None, }; let response = match HTTP_CLIENT - .post(instance.to_owned() + "/api/v3/user/login") + .post(config.instance.to_owned() + "/api/v3/user/login") .json(&login_params) .send() .await { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; match response.status() { @@ -59,17 +41,25 @@ pub(crate) async fn login(credentials: &Credentials, instance: &str) -> Result Ok(Lemmy { jwt_token: token.clone(), - instance: instance.to_owned(), + instance: config.instance.to_owned(), }), - None => panic!("Login did not return JWT token. Are the credentials valid?") + None => { + let err_msg = "Login did not return JWT token. Are the credentials valid?".to_owned(); + write_error(err_msg); + return Err(()) + } } }, - status => panic!("Unexpected HTTP Status '{}' during Login", status) + status => { + let err_msg = format!("Unexpected HTTP Status '{}' during Login", status); + write_error(err_msg); + return Err(()) + } } } impl Lemmy { - pub(crate) async fn post(&self, post: CreatePost) -> Result { + pub(crate) async fn post(&self, post: CreatePost) -> Result { let response = match HTTP_CLIENT .post(format!("{}/api/v3/post", &self.instance)) .bearer_auth(&self.jwt_token.to_string()) @@ -79,21 +69,33 @@ impl Lemmy { Ok(data) => { match data.text().await { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } } }, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; let json_data = match serde_json::from_str::>(&response) { Ok(mut data) => data.remove("post_view").expect("Element should be present"), - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; Ok(json_data.post.id) } - async fn feature(&self, params: FeaturePost) -> Result { + async fn feature(&self, params: FeaturePost) -> Result { let response = match HTTP_CLIENT .post(format!("{}/api/v3/post/feature", &self.instance)) .bearer_auth(&self.jwt_token.to_string()) @@ -103,21 +105,33 @@ impl Lemmy { Ok(data) => { match data.text().await { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } } }, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; let json_data = match serde_json::from_str::>(&response) { Ok(mut data) => data.remove("post_view").expect("Element should be present"), - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; Ok(json_data) } - pub(crate) async fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Result { + pub(crate) async fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Result { let pin_params = FeaturePost { post_id, featured: false, @@ -126,7 +140,7 @@ impl Lemmy { self.feature(pin_params).await } - pub(crate) async fn pin(&self, post_id: PostId, location: PostFeatureType) -> Result { + pub(crate) async fn pin(&self, post_id: PostId, location: PostFeatureType) -> Result { let pin_params = FeaturePost { post_id, featured: true, @@ -135,7 +149,7 @@ impl Lemmy { self.feature(pin_params).await } - pub(crate) async fn get_community_pinned(&self, community: CommunityId) -> Result, String> { + pub(crate) async fn get_community_pinned(&self, community: CommunityId) -> Result, ()> { let list_params = GetPosts { community_id: Some(community), type_: Some(ListingType::Local), @@ -151,15 +165,27 @@ impl Lemmy { Ok(data) => { match data.text().await { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } } }, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; let json_data: GetPostsResponse = match serde_json::from_str(&response) { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; Ok(json_data.posts.iter().filter(|post| { @@ -170,7 +196,7 @@ impl Lemmy { ) } - pub(crate) async fn get_local_pinned(&self) -> Result, String> { + pub(crate) async fn get_local_pinned(&self) -> Result, ()> { let list_params = GetPosts { type_: Some(ListingType::Local), ..Default::default() @@ -185,15 +211,27 @@ impl Lemmy { Ok(data) => { match data.text().await { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } } }, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; let json_data: GetPostsResponse = match serde_json::from_str(&response) { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; Ok(json_data.posts.iter().filter(|post| { @@ -204,7 +242,7 @@ impl Lemmy { ) } - pub(crate) async fn get_communities(&self) -> Result, String> { + pub(crate) async fn get_communities(&self) -> Result, ()> { let list_params = ListCommunities { type_: Some(ListingType::Local), ..Default::default() @@ -219,15 +257,27 @@ impl Lemmy { Ok(data) => { match data.text().await { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } } }, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; let json_data: ListCommunitiesResponse = match serde_json::from_str(&response) { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; let mut communities: HashMap = HashMap::new(); From 89b2d19c80fb97d131407f8b8fb21419c25b68fa Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 00:24:45 +0100 Subject: [PATCH 05/17] Move jnovel module out of subdir, port logging from tui to println/journald --- src/{jnovel/mod.rs => jnovel.rs} | 42 +++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) rename src/{jnovel/mod.rs => jnovel.rs} (91%) diff --git a/src/jnovel/mod.rs b/src/jnovel.rs similarity index 91% rename from src/jnovel/mod.rs rename to src/jnovel.rs index abd3ffe..f4babd0 100644 --- a/src/jnovel/mod.rs +++ b/src/jnovel.rs @@ -4,7 +4,7 @@ use std::ops::Sub; use chrono::{DateTime, Duration, Utc}; use serde_derive::{Deserialize, Serialize}; use url::Url; -use crate::{HTTP_CLIENT}; +use crate::{HTTP_CLIENT, write_error}; use crate::jnovel::PartInfo::{NoParts, Part}; use crate::jnovel::PostInfo::{Chapter, Volume}; @@ -222,7 +222,7 @@ impl PartialOrd for PostInfo { } } -pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Result, String> { +pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Result, ()> { let response = match HTTP_CLIENT .get(api_url!() + "/series/" + series_slug + "/volumes?format=json") .send() @@ -230,15 +230,27 @@ pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Res Ok(data) => { match data.text().await { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } } }, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; let mut volume_brief_data: VolumesWrapper = match serde_json::from_str(&response) { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; volume_brief_data.volumes.reverse(); // Makes breaking out of the volume loop easier @@ -330,7 +342,7 @@ pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Res Ok(result_vec) } -async fn get_latest_prepub(volume_slug: &str) -> Result, String> { +async fn get_latest_prepub(volume_slug: &str) -> Result, ()> { let response = match HTTP_CLIENT .get(api_url!() + "/volumes/" + volume_slug + "/parts?format=json") .send() @@ -338,16 +350,28 @@ async fn get_latest_prepub(volume_slug: &str) -> Result, S Ok(data) => { match data.text().await { Ok(data) => data, - Err(e) => return Err(e.to_string()) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } } }, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; let mut volume_prepub_parts_data: ChapterWrapper = match serde_json::from_str(&response) { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; volume_prepub_parts_data.parts.reverse(); // Makes breaking out of the parts loop easier From f2730a8eaf5849c2e739d7b8bc13d8fa4c25ba5c Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 00:28:10 +0100 Subject: [PATCH 06/17] Move post_history module out of subdir, port logging from tui to println/journald --- src/{post_history/mod.rs => post_history.rs} | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) rename src/{post_history/mod.rs => post_history.rs} (88%) diff --git a/src/post_history/mod.rs b/src/post_history.rs similarity index 88% rename from src/post_history/mod.rs rename to src/post_history.rs index a1c8e68..3d0e245 100644 --- a/src/post_history/mod.rs +++ b/src/post_history.rs @@ -4,6 +4,7 @@ use std::fs::OpenOptions; use std::io::Write; use std::path::Path; use serde_derive::{Deserialize, Serialize}; +use crate::write_error; #[derive(Serialize, Deserialize, Clone, Debug)] pub(crate) struct SeriesHistory { @@ -11,7 +12,7 @@ pub(crate) struct SeriesHistory { } impl SeriesHistory { - pub(crate) fn load_history() -> Result { + pub(crate) fn load_history() -> Result { let path = confy::get_configuration_file_path(env!("CARGO_PKG_NAME"), "config").expect("Something went wrong with confy"); let config_dir = path.parent().expect("Something went wrong with confy"); @@ -21,7 +22,11 @@ impl SeriesHistory { true => { let file_contents: String = match fs::read_to_string("history.toml") { Ok(data) => data, - Err(e) => return Err(format!("{}", e)), + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + }, }; let history: Result = match file_contents.len() { @@ -33,7 +38,11 @@ impl SeriesHistory { match history { Ok(data) => Ok(data), - Err(e) => Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + Err(()) + } } }, false => { From e4e17670830b7a20eecd1d45a384c7b8714b85e6 Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 00:40:12 +0100 Subject: [PATCH 07/17] Move bot module out of subdir, port logging from tui to println/journald --- src/{bot/mod.rs => bot.rs} | 122 +++++++++---------------------------- 1 file changed, 30 insertions(+), 92 deletions(-) rename src/{bot/mod.rs => bot.rs} (66%) diff --git a/src/bot/mod.rs b/src/bot.rs similarity index 66% rename from src/bot/mod.rs rename to src/bot.rs index 50577d8..4577889 100644 --- a/src/bot/mod.rs +++ b/src/bot.rs @@ -5,7 +5,7 @@ use lemmy_api_common::post::CreatePost; use lemmy_db_schema::newtypes::{CommunityId, LanguageId}; use lemmy_db_schema::PostFeatureType; use tokio::sync::{RwLock}; -use crate::{jnovel, lemmy, Message, SharedData}; +use crate::{jnovel, lemmy, SharedData, write_error, write_info, write_warn}; use crate::config::{Config, PostBody, SeriesConfig}; use crate::jnovel::PostInfo; use crate::lemmy::{Lemmy}; @@ -17,35 +17,26 @@ pub(crate) async fn run(data: Arc>) { let mut lemmy: Lemmy; let mut login_error: bool; let mut communities; - let mut credentials; { let mut write = data.write().await; // Errors during bot init are likely unrecoverable and therefore should panic the bot // Does not really matter since the bot will get restarted anyway but this way the uptime url logs a downtime - write.config = match Config::load() { - Ok(data) => data, - Err(e) => panic!("{}", e), - }; + write.config = Config::load(); last_reload = Utc::now(); } { let read = data.read().await; - credentials = lemmy::Credentials::set_credentials(&read.config); - } - - { - let read = data.read().await; - lemmy = match lemmy::login(&credentials, read.config.instance.as_str()).await { + lemmy = match lemmy::login(&read.config).await { Ok(data) => data, - Err(e) => panic!("{}", e), + Err(_) => panic!(), }; login_error = false; communities = match lemmy.get_communities().await { Ok(data) => data, - Err(e) => panic!("{}", e), + Err(_) => panic!(), }; } @@ -67,36 +58,18 @@ pub(crate) async fn run(data: Arc>) { write.start = Utc::now(); if write.start - last_reload >= Duration::seconds(write.config.config_reload_seconds as i64) { - write.config = match Config::load() { - Ok(data) => data, - Err(e) => panic!("{}", e), - }; - let message = Message::Info("Config reloaded".to_owned()); - if !write.messages.contains(&message) { - write.messages.push(message); - }; - } - } - - { - let read = data.read().await; - - if read.start - last_reload >= Duration::seconds(read.config.config_reload_seconds as i64) { - credentials = lemmy::Credentials::set_credentials(&read.config); + write.config = Config::load(); + let message = "Config reloaded".to_owned(); + write_info(message); } } { let read = data.read().await; if login_error { - lemmy = match lemmy::login(&credentials, read.config.instance.as_str()).await { + lemmy = match lemmy::login(&read.config).await { Ok(data) => data, - Err(e) => { - drop(read); - let mut write = data.write().await; - write.messages.push(Message::Error(e.to_string())); - continue - } + Err(_) => continue, }; login_error = false; } @@ -107,20 +80,13 @@ pub(crate) async fn run(data: Arc>) { if read.start - last_reload >= Duration::seconds(read.config.config_reload_seconds as i64) { communities = match lemmy.get_communities().await { Ok(data) => data, - Err(e) => { + Err(_) => { login_error = true; - drop(read); - let mut write = data.write().await; - write.messages.push(Message::Error(e.to_string())); continue } }; - let message = Message::Info("Communities reloaded".to_owned()); - if !read.messages.contains(&message) { - drop(read); - let mut write = data.write().await; - write.messages.push(message); - }; + let message = "Communities reloaded".to_owned(); + write_info(message); last_reload = Utc::now(); } } @@ -129,10 +95,7 @@ pub(crate) async fn run(data: Arc>) { let mut write = data.write().await; write.post_history = match SeriesHistory::load_history() { Ok(data) => data, - Err(e) => { - write.messages.push(Message::Warning(e.to_string())); - continue - } + Err(_) => continue, }; } @@ -141,32 +104,13 @@ pub(crate) async fn run(data: Arc>) { let series = read.config.series.clone(); drop(read); for series in series { - match handle_series(&series, &communities, &lemmy, &data).await { - Ok(data) => data, - Err(e) => { - login_error = true; - let mut write = data.write().await; - write.messages.push(Message::Warning(e.to_string())); - continue - } + if handle_series(&series, &communities, &lemmy, &data).await.is_err() { + login_error = true; + continue }; } } - let read = data.read().await; - if read.messages.len() > 15 { - let mut list = read.messages.clone(); - drop(read); - list.reverse(); - while list.len() > 15 { - list.pop(); - } - list.reverse(); - let mut write = data.write().await; - write.messages = list - } - - idle(&data).await; } } @@ -182,8 +126,8 @@ async fn idle(data: &Arc>) { match reqwest::get(status_url).await { Ok(_) => {} Err(e) => { - let mut write = data.write().await; - write.messages.push(Message::Error(e.to_string())) + let err_msg = format!("{e}"); + write_error(err_msg); }, } }; @@ -198,13 +142,11 @@ async fn handle_series( communities: &HashMap, lemmy: &Lemmy, data: &Arc>, -) -> Result<(), String> { +) -> Result<(), ()> { let mut post_list = match jnovel::check_feed(series.slug.as_str(), series.parted).await { Ok(data) => data, - Err(e) => { - return Err(e.to_string()); - }, + Err(_) => return Err(()), }; for (index, post_info) in post_list.clone().iter().enumerate() { // todo .clone() likely not needed @@ -215,12 +157,8 @@ async fn handle_series( { let read = data.read().await; if read.post_history.check_for_post(series.slug.as_str(), post_part_info.as_string().as_str(), post_lemmy_info.title.as_str()) { - let message = Message::Info(format!("Skipping '{}' since already posted", post_lemmy_info.title)); - if !read.messages.contains(&message) { - drop(read); - let mut write = data.write().await; - write.messages.push(message); - }; + let message = format!("Skipping '{}' since already posted", post_lemmy_info.title); + write_info(message); post_list.remove(index); continue } @@ -263,12 +201,8 @@ async fn handle_series( } lemmy.pin(post_id, PostFeatureType::Community).await?; } else if read.config.protected_communities.contains(&post_series_config.name) { - let message = Message::Warning(format!("Community '{}' for Series '{}' is protected. Is this intended?", &post_series_config.name, series.slug)); - if !read.messages.contains(&message) { - drop(read); - let mut write = data.write().await; - write.messages.push(message); - } + let message = format!("Community '{}' for Series '{}' is protected. Is this intended?", &post_series_config.name, series.slug); + write_warn(message); } } @@ -308,7 +242,11 @@ async fn handle_series( write.post_history.set_series(series.slug.as_str(), series_history); let _ = match write.post_history.save_history() { Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()) + } }; } Ok(()) From 1db4cb19bd17ba799bb9a90c89f301d804c154a5 Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 00:41:17 +0100 Subject: [PATCH 08/17] Delete TUI module --- src/tui/mod.rs | 100 ------------------------------------------------- 1 file changed, 100 deletions(-) delete mode 100644 src/tui/mod.rs diff --git a/src/tui/mod.rs b/src/tui/mod.rs deleted file mode 100644 index 07fa649..0000000 --- a/src/tui/mod.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::ops::Deref; -use std::sync::{Arc}; -use chrono::{Duration, Local}; -use tokio::sync::{RwLock}; -use tokio::time::sleep; -use crate::{SharedData}; -use crate::post_history::{PostHistory, PostHistoryInner}; - -pub(crate) async fn run(shared_data: Arc>) { - let mut min_len_series: u32 = 0; - let mut min_len_slug: u32 = 0; - println!("TUI restarted"); - loop { - sleep(Duration::milliseconds(250).to_std().unwrap()).await; - print_info(&shared_data, &mut min_len_series, &mut min_len_slug).await; - } -} - -async fn print_info(arc_data: &Arc>, min_len_series: &mut u32, min_len_slug: &mut u32) { - let data = arc_data.read().await; - let mut local_min_len_series = *min_len_series.deref() as usize; - let mut local_min_len_slug = *min_len_slug.deref() as usize; - let separator_width = local_min_len_slug + local_min_len_series + 44; // 44 should account for length of every other string - - print!("\x1B[2J\x1B[1;1H"); - println!( - "##[Ascendance of a Bookworm Bot]## | Time: {}", - Local::now().naive_local().format("%H:%M:%S") - ); - println!("Instance: {}", data.config.instance); - println!( - "Ran Last: {} | Config Reload Interval: {}", - data - .start - .with_timezone(&Local) - .naive_local() - .format("%d/%m/%Y %H:%M:%S"), - data.config.config_reload_seconds - ); - println!("{:#<1$}", "", separator_width); - let mut sorted_series: Vec<(&String, &PostHistory)> = data.post_history.series.iter().collect(); - sorted_series.sort_by(|(a, _), (b, _)| a.cmp(b)); - for (series, post_history) in sorted_series.iter() { - - if series.len() > local_min_len_series { - local_min_len_series = series.len() + 1; - *min_len_series = local_min_len_series as u32; - } - - let series_config = match data.config.series - .iter() - .find(|ser| { - &&ser.slug == series - }) { - Some(data) => data, - None => continue, - }; - - let mut sorted_parts: Vec<(&String, &PostHistoryInner)> = post_history.parts.iter().collect(); - sorted_parts.sort_by(|(a, _), (b, _)| a.cmp(b)); - for (part, part_history) in sorted_parts.iter() { - - if part_history.volume.len() > local_min_len_slug { - local_min_len_slug = part_history.chapter.len() + 1; - *min_len_slug = local_min_len_slug as u32; - } - - print!("{series}"); - print!("{:<1$}| ", "", local_min_len_series - series.len()); - print!("Part {part}"); - print!("{:<1$}| Volume | ", "", 2-part.len()); - print!("{}", part_history.volume); - print!("{:<1$}| ", "", local_min_len_slug - part_history.volume.len()); - print!("{}", series_config.volume_community.name); - println!("{:<1$}|", "", 20 - series_config.volume_community.name.len()); - - if part_history.chapter.len() > local_min_len_slug { - local_min_len_slug = part_history.chapter.len() + 1; - *min_len_slug = local_min_len_slug as u32; - } - - print!("{series}"); - print!("{:<1$}| ", "", local_min_len_series - series.len()); - print!("Part {part}"); - print!("{:<1$}| Chapter | ", "", 2-part.len()); - print!("{}", part_history.chapter); - print!("{:<1$}| ", "", local_min_len_slug - part_history.chapter.len()); - print!("{}", series_config.prepub_community.name); - println!("{:<1$}|", "", 20 - series_config.prepub_community.name.len()); - } - } - println!("{:#<1$}", "", separator_width); - for error in data.get_messages(true, true, false).iter() { - println!("{}", error.content()); - } - println!("{:#<1$}", "", separator_width); - for message in data.get_messages(false, false, true).iter() { - println!("{}", message.content()); - } -} \ No newline at end of file From c355bc141f38f5d3218a526666b4fee509fd37c4 Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 00:41:29 +0100 Subject: [PATCH 09/17] Adapt module changes to main.rs --- src/main.rs | 84 ++++++++++++++++++++++++----------------------------- 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0c9f929..be977d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,12 @@ use chrono::{DateTime, Duration, Utc}; use once_cell::sync::Lazy; use reqwest::{Client}; -use std::{collections::HashMap, vec}; +use std::{collections::HashMap}; use std::fmt::Debug; use std::sync::{Arc}; +use log::{error, warn, info, LevelFilter}; use tokio::sync::{RwLock}; -use strum_macros::Display; +use systemd_journal_logger::{connected_to_journal, JournalLog}; use tokio::time::sleep; use crate::config::Config; use crate::post_history::{SeriesHistory}; @@ -14,9 +15,29 @@ mod config; mod jnovel; mod bot; mod lemmy; -mod tui; mod post_history; +pub (crate) fn write_error(err_msg: String) { + match connected_to_journal() { + true => error!("[ERROR] {err_msg}"), + false => println!("[ERROR] {err_msg}"), + } +} + +pub (crate) fn write_warn(warn_msg: String) { + match connected_to_journal() { + true => warn!("[WARN] {warn_msg}"), + false => println!("[WARN] {warn_msg}"), + } +} + +pub (crate) fn write_info(info_msg: String) { + match connected_to_journal() { + true => info!("[INFO] {info_msg}"), + false => println!("[INFO] {info_msg}"), + } +} + pub static HTTP_CLIENT: Lazy = Lazy::new(|| { Client::builder() .timeout(Duration::seconds(30).to_std().unwrap()) @@ -27,7 +48,6 @@ pub static HTTP_CLIENT: Lazy = Lazy::new(|| { #[derive(Clone, Debug)] pub(crate) struct SharedData { - messages: Vec, config: Config, post_history: SeriesHistory, start: DateTime, @@ -36,69 +56,41 @@ pub(crate) struct SharedData { impl SharedData { pub(crate) fn new() -> Self { SharedData { - messages: vec![], - config: Config { - instance: "".to_owned(), - username: "".to_owned(), - password: "".to_owned(), - status_post_url: None, - config_reload_seconds: 0, - protected_communities: vec![], - series: vec![], - }, + config: Config::default(), post_history: SeriesHistory { series: HashMap::new(), }, start: Utc::now(), } } - - pub(crate) fn get_messages(&self, errors: bool, warnings: bool, infos: bool) -> Vec { - self.messages.iter().filter(|msg| { - match msg { - Message::Error(_) => errors, - Message::Warning(_) => warnings, - Message::Info(_) => infos, - } - }).cloned().collect() - } -} - -#[derive(Clone, Debug, Display, PartialEq)] -pub(crate) enum Message { - Info(String), - Warning(String), - Error(String), -} - -impl Message { - pub(crate) fn content(&self) -> String { - match self { - Message::Info(msg) => msg.clone(), - Message::Warning(msg) => msg.clone(), - Message::Error(msg) => msg.clone(), - } - } } #[tokio::main] async fn main() { - let mut data = SharedData::new(); + JournalLog::new().expect("Systemd-Logger crate error").install().expect("Systemd-Logger crate error"); + log::set_max_level(LevelFilter::Info); + let mut data = SharedData::new(); loop { let write_data = Arc::new(RwLock::new(data.clone())); - let read_data = write_data.clone(); + //let read_data = write_data.clone(); let persistent_data = write_data.clone(); - let tui_thread = tokio::spawn(async move { tui::run(read_data).await }); let bot_thread = tokio::spawn(async move { bot::run(write_data).await }); let _ = bot_thread.await; - tui_thread.abort(); data = persistent_data.read().await.clone(); - data.messages.push(Message::Error("Bot crashed due to unknown Error, restarting thread after wait...".to_owned())); + + { + let err_msg = "Bot crashed due to unknown Error, restarting thread after wait..."; + match connected_to_journal() { + true => error!("[ERROR] {err_msg}"), + false => println!("[ERROR] {err_msg}") + } + } + sleep(Duration::seconds(5).to_std().expect("Conversion should always work since static")).await; } } From 4a4dff8c225dc3d17fafb5c8aa60fb638f884083 Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 00:41:40 +0100 Subject: [PATCH 10/17] Add logging dependencies --- Cargo.lock | 21 +++++++++++++++++++++ Cargo.toml | 6 ++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a23cd0..620e36d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,12 +49,14 @@ dependencies = [ "confy", "lemmy_api_common", "lemmy_db_schema", + "log", "once_cell", "reqwest", "serde", "serde_derive", "serde_json", "strum_macros", + "systemd-journal-logger", "tokio", "toml 0.8.8", "url", @@ -715,6 +717,9 @@ name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] [[package]] name = "memchr" @@ -1209,6 +1214,16 @@ dependencies = [ "libc", ] +[[package]] +name = "systemd-journal-logger" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f3848dd723f2a54ac1d96da793b32923b52de8dfcced8722516dac312a5b2a" +dependencies = [ + "log", + "rustix", +] + [[package]] name = "tempfile" version = "3.8.1" @@ -1487,6 +1502,12 @@ dependencies = [ "serde", ] +[[package]] +name = "value-bag" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 49d915d..32ecc63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,5 +27,7 @@ serde_json = "^1.0.97" strum_macros = "^0.25.0" tokio = { version = "^1.32.0", features = ["rt", "rt-multi-thread", "macros"] } url = "^2.4.0" -confy = "0.5.1" -toml = "0.8.8" +confy = "^0.5.1" +toml = "^0.8.8" +systemd-journal-logger = "^2.1.1" +log = "^0.4.20" \ No newline at end of file From 9c15ea791ff0b8e4410bf7f28df233b2bc898e62 Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 00:42:50 +0100 Subject: [PATCH 11/17] Release Candidate 2.1.7-rc.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 620e36d..16388ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,7 +43,7 @@ dependencies = [ [[package]] name = "aob-lemmy-bot" -version = "2.1.6" +version = "2.1.7-rc.1" dependencies = [ "chrono", "confy", diff --git a/Cargo.toml b/Cargo.toml index 32ecc63..6bcec23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Neshura"] name = "aob-lemmy-bot" -version = "2.1.6" +version = "2.1.7-rc.1" edition = "2021" description = "Bot for automatically posting new chapters of 'Ascendance of a Bookworm' released by J-Novel Club" license = "GPL-3.0-or-later" From 0f1de67db6d79a1d7c46193f15248621362f1612 Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 00:53:00 +0100 Subject: [PATCH 12/17] Bugfix for moved history.toml --- src/bot.rs | 3 +++ src/lemmy.rs | 4 ++-- src/post_history.rs | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index 4577889..35c9902 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -49,6 +49,9 @@ pub(crate) async fn run(data: Arc>) { write.start = Utc::now(); } + let info_msg = "Bot init successful, starting normal operations".to_owned(); + write_info(info_msg); + loop { idle(&data).await; diff --git a/src/lemmy.rs b/src/lemmy.rs index 38148b2..b641577 100644 --- a/src/lemmy.rs +++ b/src/lemmy.rs @@ -46,14 +46,14 @@ pub(crate) async fn login(config: &Config) -> Result { None => { let err_msg = "Login did not return JWT token. Are the credentials valid?".to_owned(); write_error(err_msg); - return Err(()) + Err(()) } } }, status => { let err_msg = format!("Unexpected HTTP Status '{}' during Login", status); write_error(err_msg); - return Err(()) + Err(()) } } } diff --git a/src/post_history.rs b/src/post_history.rs index 3d0e245..7f8e1a3 100644 --- a/src/post_history.rs +++ b/src/post_history.rs @@ -17,10 +17,10 @@ impl SeriesHistory { let config_dir = path.parent().expect("Something went wrong with confy"); - - match Path::new(format!("{}/history.toml", config_dir.to_str().expect("Conversion to str should not fail for a dir")).as_str()).exists() { + let path = format!("{}/history.toml", config_dir.to_str().expect("Conversion to str should not fail for a dir")); + match Path::new(path.as_str()).exists() { true => { - let file_contents: String = match fs::read_to_string("history.toml") { + let file_contents: String = match fs::read_to_string(path.as_str()) { Ok(data) => data, Err(e) => { let err_msg = format!("{e}"); From f08e60a89da8d2ce54a9b3d44eabf37e57ed2b6b Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 00:53:15 +0100 Subject: [PATCH 13/17] Release Candidate 2.2.0-rc.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16388ab..25bdb32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,7 +43,7 @@ dependencies = [ [[package]] name = "aob-lemmy-bot" -version = "2.1.7-rc.1" +version = "2.2.0-rc.1" dependencies = [ "chrono", "confy", diff --git a/Cargo.toml b/Cargo.toml index 6bcec23..527d98a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Neshura"] name = "aob-lemmy-bot" -version = "2.1.7-rc.1" +version = "2.2.0-rc.1" edition = "2021" description = "Bot for automatically posting new chapters of 'Ascendance of a Bookworm' released by J-Novel Club" license = "GPL-3.0-or-later" From cbb50e5d533afebc85044dd91425bdf67273ed92 Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 01:02:20 +0100 Subject: [PATCH 14/17] Utilize confy for reading and saving history --- src/bot.rs | 14 ++-------- src/post_history.rs | 64 +++++++-------------------------------------- 2 files changed, 12 insertions(+), 66 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index 35c9902..8b0a61a 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -96,10 +96,7 @@ pub(crate) async fn run(data: Arc>) { { let mut write = data.write().await; - write.post_history = match SeriesHistory::load_history() { - Ok(data) => data, - Err(_) => continue, - }; + write.post_history = SeriesHistory::load_history(); } { @@ -243,14 +240,7 @@ async fn handle_series( series_history.set_part(post_part_info.as_string().as_str(), part_history); let mut write = data.write().await; write.post_history.set_series(series.slug.as_str(), series_history); - let _ = match write.post_history.save_history() { - Ok(data) => data, - Err(e) => { - let err_msg = format!("{e}"); - write_error(err_msg); - return Err(()) - } - }; + write.post_history.save_history(); } Ok(()) } diff --git a/src/post_history.rs b/src/post_history.rs index 7f8e1a3..364f444 100644 --- a/src/post_history.rs +++ b/src/post_history.rs @@ -1,69 +1,25 @@ use std::collections::HashMap; -use std::fs; -use std::fs::OpenOptions; -use std::io::Write; -use std::path::Path; use serde_derive::{Deserialize, Serialize}; use crate::write_error; -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Default, Clone, Debug)] pub(crate) struct SeriesHistory { pub(crate) series: HashMap, } impl SeriesHistory { - pub(crate) fn load_history() -> Result { - let path = confy::get_configuration_file_path(env!("CARGO_PKG_NAME"), "config").expect("Something went wrong with confy"); - let config_dir = path.parent().expect("Something went wrong with confy"); - - - let path = format!("{}/history.toml", config_dir.to_str().expect("Conversion to str should not fail for a dir")); - match Path::new(path.as_str()).exists() { - true => { - let file_contents: String = match fs::read_to_string(path.as_str()) { - Ok(data) => data, - Err(e) => { - let err_msg = format!("{e}"); - write_error(err_msg); - return Err(()) - }, - }; - - let history: Result = match file_contents.len() { - 0 => return Ok(SeriesHistory { - series: HashMap::new(), - }), - _ => toml::from_str(file_contents.as_str()), - }; - - match history { - Ok(data) => Ok(data), - Err(e) => { - let err_msg = format!("{e}"); - write_error(err_msg); - Err(()) - } - } - }, - false => { - Ok(SeriesHistory { - series: HashMap::new(), - }) - } + pub(crate) fn load_history() -> Self { + match confy::load(env!("CARGO_PKG_NAME"), "history") { + Ok(data) => data, + Err(e) => panic!("history.toml not found: {e}"), } } - pub(crate) fn save_history(&self) -> std::io::Result { - let mut file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .open("history.toml") - .unwrap(); - - let json_data = toml::to_string_pretty(&self).unwrap(); - - file.write(json_data.as_bytes()) + pub(crate) fn save_history(&self) { + if let Err(e) = confy::store(env!("CARGO_PKG_NAME"), "history", self) { + let err_msg = format!("Unexpected error saving to history.toml: {e}"); + write_error(err_msg); + } } pub(crate) fn check_for_post(&self, series: &str, part: &str, title: &str) -> bool { From c3ff578c57aab01b692c3990c63f3f9594ae9c57 Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 01:22:04 +0100 Subject: [PATCH 15/17] Clean up code --- src/bot.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index 8b0a61a..5ca1567 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::sync::{Arc}; -use chrono::{DateTime, Duration, Timelike, Utc}; +use chrono::{DateTime, Duration, Utc}; use lemmy_api_common::post::CreatePost; use lemmy_db_schema::newtypes::{CommunityId, LanguageId}; use lemmy_db_schema::PostFeatureType; @@ -40,10 +40,6 @@ pub(crate) async fn run(data: Arc>) { }; } - while Utc::now().naive_local().second() != 30 { - sleep(Duration::milliseconds(100).to_std().unwrap()).await; - } - { let mut write = data.write().await; write.start = Utc::now(); From 8be74585a0b3704720fa63ebb94c79f0453c954a Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 01:22:13 +0100 Subject: [PATCH 16/17] Extend logging for new posts --- src/bot.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/bot.rs b/src/bot.rs index 5ca1567..eb67a4b 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -176,7 +176,7 @@ async fn handle_series( }; let post_data = CreatePost { - name: post_lemmy_info.title, + name: post_lemmy_info.title.clone(), community_id, url: Some(post_lemmy_info.url), body: post_body, @@ -185,11 +185,15 @@ async fn handle_series( language_id: Some(LanguageId(37)), // TODO get this id once every few hours per API request, the ordering of IDs suggests that the EN Id might change in the future }; + let info = format!("Posting '{}' to {}", post_lemmy_info.title.as_str(), post_series_config.name.as_str()); + write_info(info); let post_id = lemmy.post(post_data).await?; { let read = data.read().await; if post_series_config.pin_settings.pin_new_post_community && !read.config.protected_communities.contains(&post_series_config.name) { + let info = format!("Pinning '{}' to {}", post_lemmy_info.title, post_series_config.name.as_str()); + write_info(info); let pinned_posts = lemmy.get_community_pinned(community_id).await?; if !pinned_posts.is_empty() { let community_pinned_post = &pinned_posts[0]; @@ -204,6 +208,8 @@ async fn handle_series( let read = data.read().await; if post_series_config.pin_settings.pin_new_post_local { + let info = format!("Pinning '{}' to Instance", post_lemmy_info.title); + write_info(info); let pinned_posts = lemmy.get_local_pinned().await?; if !pinned_posts.is_empty() { for pinned_post in pinned_posts { From 8c1da63e0cae6919d088794ae3994e7e2adcb5df Mon Sep 17 00:00:00 2001 From: Neshura Date: Sat, 30 Dec 2023 01:27:11 +0100 Subject: [PATCH 17/17] rustfmt --- src/bot.rs | 117 +++++++++++++++++++-------------- src/config.rs | 4 +- src/jnovel.rs | 155 ++++++++++++++++++++++++-------------------- src/lemmy.rs | 143 ++++++++++++++++++++-------------------- src/main.rs | 44 ++++++++----- src/post_history.rs | 24 +++---- 6 files changed, 267 insertions(+), 220 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index eb67a4b..e9f0f81 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,15 +1,15 @@ -use std::collections::HashMap; -use std::sync::{Arc}; +use crate::config::{Config, PostBody, SeriesConfig}; +use crate::jnovel::PostInfo; +use crate::lemmy::Lemmy; +use crate::post_history::SeriesHistory; +use crate::{jnovel, lemmy, write_error, write_info, write_warn, SharedData}; use chrono::{DateTime, Duration, Utc}; use lemmy_api_common::post::CreatePost; use lemmy_db_schema::newtypes::{CommunityId, LanguageId}; use lemmy_db_schema::PostFeatureType; -use tokio::sync::{RwLock}; -use crate::{jnovel, lemmy, SharedData, write_error, write_info, write_warn}; -use crate::config::{Config, PostBody, SeriesConfig}; -use crate::jnovel::PostInfo; -use crate::lemmy::{Lemmy}; -use crate::post_history::SeriesHistory; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; use tokio::time::sleep; pub(crate) async fn run(data: Arc>) { @@ -81,7 +81,7 @@ pub(crate) async fn run(data: Arc>) { Ok(data) => data, Err(_) => { login_error = true; - continue + continue; } }; let message = "Communities reloaded".to_owned(); @@ -100,9 +100,12 @@ pub(crate) async fn run(data: Arc>) { let series = read.config.series.clone(); drop(read); for series in series { - if handle_series(&series, &communities, &lemmy, &data).await.is_err() { + if handle_series(&series, &communities, &lemmy, &data) + .await + .is_err() + { login_error = true; - continue + continue; }; } } @@ -124,7 +127,7 @@ async fn idle(data: &Arc>) { Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - }, + } } }; @@ -133,36 +136,34 @@ async fn idle(data: &Arc>) { } } -async fn handle_series( - series: &SeriesConfig, - communities: &HashMap, - lemmy: &Lemmy, - data: &Arc>, -) -> Result<(), ()> { - +async fn handle_series(series: &SeriesConfig, communities: &HashMap, lemmy: &Lemmy, data: &Arc>) -> Result<(), ()> { let mut post_list = match jnovel::check_feed(series.slug.as_str(), series.parted).await { Ok(data) => data, Err(_) => return Err(()), }; - for (index, post_info) in post_list.clone().iter().enumerate() { // todo .clone() likely not needed + for (index, post_info) in post_list.clone().iter().enumerate() { + // todo .clone() likely not needed let post_part_info = post_info.get_part_info(); let post_lemmy_info = post_info.get_lemmy_info(); - { let read = data.read().await; - if read.post_history.check_for_post(series.slug.as_str(), post_part_info.as_string().as_str(), post_lemmy_info.title.as_str()) { + if read.post_history.check_for_post( + series.slug.as_str(), + post_part_info.as_string().as_str(), + post_lemmy_info.title.as_str(), + ) { let message = format!("Skipping '{}' since already posted", post_lemmy_info.title); write_info(message); post_list.remove(index); - continue + continue; } } let post_series_config = match post_info { - PostInfo::Chapter {..} => {&series.prepub_community}, - PostInfo::Volume {..} => {&series.volume_community} + PostInfo::Chapter { .. } => &series.prepub_community, + PostInfo::Volume { .. } => &series.volume_community, }; let community_id = *communities @@ -185,23 +186,45 @@ async fn handle_series( language_id: Some(LanguageId(37)), // TODO get this id once every few hours per API request, the ordering of IDs suggests that the EN Id might change in the future }; - let info = format!("Posting '{}' to {}", post_lemmy_info.title.as_str(), post_series_config.name.as_str()); + let info = format!( + "Posting '{}' to {}", + post_lemmy_info.title.as_str(), + post_series_config.name.as_str() + ); write_info(info); let post_id = lemmy.post(post_data).await?; { let read = data.read().await; - if post_series_config.pin_settings.pin_new_post_community && !read.config.protected_communities.contains(&post_series_config.name) { - let info = format!("Pinning '{}' to {}", post_lemmy_info.title, post_series_config.name.as_str()); + if post_series_config.pin_settings.pin_new_post_community + && !read + .config + .protected_communities + .contains(&post_series_config.name) + { + let info = format!( + "Pinning '{}' to {}", + post_lemmy_info.title, + post_series_config.name.as_str() + ); write_info(info); let pinned_posts = lemmy.get_community_pinned(community_id).await?; if !pinned_posts.is_empty() { let community_pinned_post = &pinned_posts[0]; - lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Community).await?; + lemmy + .unpin(community_pinned_post.post.id, PostFeatureType::Community) + .await?; } lemmy.pin(post_id, PostFeatureType::Community).await?; - } else if read.config.protected_communities.contains(&post_series_config.name) { - let message = format!("Community '{}' for Series '{}' is protected. Is this intended?", &post_series_config.name, series.slug); + } else if read + .config + .protected_communities + .contains(&post_series_config.name) + { + let message = format!( + "Community '{}' for Series '{}' is protected. Is this intended?", + &post_series_config.name, series.slug + ); write_warn(message); } } @@ -213,13 +236,18 @@ async fn handle_series( let pinned_posts = lemmy.get_local_pinned().await?; if !pinned_posts.is_empty() { for pinned_post in pinned_posts { - if read.config.protected_communities.contains(&pinned_post.community.name) { - continue - } - else { + if read + .config + .protected_communities + .contains(&pinned_post.community.name) + { + continue; + } else { let community_pinned_post = &pinned_post; - lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Local).await?; - break + lemmy + .unpin(community_pinned_post.post.id, PostFeatureType::Local) + .await?; + break; } } } @@ -231,21 +259,16 @@ async fn handle_series( drop(read); match post_info { - PostInfo::Chapter {..} => { - part_history.chapter = post_info.get_lemmy_info().title - }, - PostInfo::Volume {..} => { - part_history.volume = post_info.get_lemmy_info().title - } + PostInfo::Chapter { .. } => part_history.chapter = post_info.get_lemmy_info().title, + PostInfo::Volume { .. } => part_history.volume = post_info.get_lemmy_info().title, } series_history.set_part(post_part_info.as_string().as_str(), part_history); let mut write = data.write().await; - write.post_history.set_series(series.slug.as_str(), series_history); + write + .post_history + .set_series(series.slug.as_str(), series_history); write.post_history.save_history(); } Ok(()) } - - - diff --git a/src/config.rs b/src/config.rs index bed8cbb..776d868 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ +use crate::config::PostBody::Description; use lemmy_api_common::sensitive::Sensitive; use serde_derive::{Deserialize, Serialize}; -use crate::config::PostBody::Description; #[derive(Serialize, Deserialize, Clone, Debug)] pub(crate) struct Config { @@ -58,7 +58,7 @@ impl Default for Config { status_post_url: None, config_reload_seconds: 21600, protected_communities: vec![], - series: vec![] + series: vec![], } } } diff --git a/src/jnovel.rs b/src/jnovel.rs index f4babd0..b27c88c 100644 --- a/src/jnovel.rs +++ b/src/jnovel.rs @@ -1,12 +1,12 @@ +use crate::jnovel::PartInfo::{NoParts, Part}; +use crate::jnovel::PostInfo::{Chapter, Volume}; +use crate::{write_error, HTTP_CLIENT}; +use chrono::{DateTime, Duration, Utc}; +use serde_derive::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::HashMap; use std::ops::Sub; -use chrono::{DateTime, Duration, Utc}; -use serde_derive::{Deserialize, Serialize}; use url::Url; -use crate::{HTTP_CLIENT, write_error}; -use crate::jnovel::PartInfo::{NoParts, Part}; -use crate::jnovel::PostInfo::{Chapter, Volume}; static PAST_DAYS_ELIGIBLE: u8 = 4; @@ -61,7 +61,6 @@ pub(crate) struct VolumeDetail { pub(crate) cover: Cover, } - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub(crate) struct ChapterDetail { pub(crate) title: String, @@ -105,9 +104,13 @@ impl PartialEq for PartInfo { impl PartialOrd for PartInfo { fn partial_cmp(&self, other: &Self) -> Option { - if self.gt(other) { Some(Ordering::Greater) } - else if self.eq(other) { Some(Ordering::Equal) } - else { Some(Ordering::Less) } + if self.gt(other) { + Some(Ordering::Greater) + } else if self.eq(other) { + Some(Ordering::Equal) + } else { + Some(Ordering::Less) + } } fn lt(&self, other: &Self) -> bool { @@ -135,29 +138,40 @@ impl PartialOrd for PartInfo { #[derive(Debug, Clone)] pub(crate) enum PostInfo { - Chapter { part: PartInfo, lemmy_info: LemmyPostInfo }, - Volume { part: PartInfo, description: String, lemmy_info: LemmyPostInfo }, + Chapter { + part: PartInfo, + lemmy_info: LemmyPostInfo, + }, + Volume { + part: PartInfo, + description: String, + lemmy_info: LemmyPostInfo, + }, } impl PostInfo { pub(crate) fn get_part_info(&self) -> PartInfo { match self { - Chapter {part: part_info, ..} => *part_info, - Volume {part: part_info, ..} => *part_info + Chapter { + part: part_info, .. + } => *part_info, + Volume { + part: part_info, .. + } => *part_info, } } pub(crate) fn get_lemmy_info(&self) -> LemmyPostInfo { match self { - Chapter {lemmy_info, ..} => lemmy_info.clone(), - Volume {lemmy_info, ..} => lemmy_info.clone() + Chapter { lemmy_info, .. } => lemmy_info.clone(), + Volume { lemmy_info, .. } => lemmy_info.clone(), } } pub(crate) fn get_description(&self) -> Option { match self { - Chapter {..} => None, - Volume {description, ..} => Some(description.clone()), + Chapter { .. } => None, + Volume { description, .. } => Some(description.clone()), } } } @@ -165,13 +179,13 @@ impl PostInfo { impl PartialEq for PostInfo { fn eq(&self, other: &Self) -> bool { let self_part = match self { - Chapter {part, ..} => part, - Volume {part, ..} => part, + Chapter { part, .. } => part, + Volume { part, .. } => part, }; let other_part = match other { - Chapter {part, ..} => part, - Volume {part, ..} => part, + Chapter { part, .. } => part, + Volume { part, .. } => part, }; self_part.eq(other_part) @@ -180,20 +194,24 @@ impl PartialEq for PostInfo { impl PartialOrd for PostInfo { fn partial_cmp(&self, other: &Self) -> Option { - if self.gt(other) { Some(Ordering::Greater) } - else if self.eq(other) { Some(Ordering::Equal) } - else { Some(Ordering::Less) } + if self.gt(other) { + Some(Ordering::Greater) + } else if self.eq(other) { + Some(Ordering::Equal) + } else { + Some(Ordering::Less) + } } fn lt(&self, other: &Self) -> bool { let self_part = match self { - Chapter {part, ..} => part, - Volume {part, ..} => part, + Chapter { part, .. } => part, + Volume { part, .. } => part, }; let other_part = match other { - Chapter {part, ..} => part, - Volume {part, ..} => part, + Chapter { part, .. } => part, + Volume { part, .. } => part, }; self_part < other_part @@ -205,13 +223,13 @@ impl PartialOrd for PostInfo { fn gt(&self, other: &Self) -> bool { let self_part = match self { - Chapter {part, ..} => part, - Volume {part, ..} => part, + Chapter { part, .. } => part, + Volume { part, .. } => part, }; let other_part = match other { - Chapter {part, ..} => part, - Volume {part, ..} => part, + Chapter { part, .. } => part, + Volume { part, .. } => part, }; self_part > other_part @@ -226,21 +244,20 @@ pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Res let response = match HTTP_CLIENT .get(api_url!() + "/series/" + series_slug + "/volumes?format=json") .send() - .await { - Ok(data) => { - match data.text().await { - Ok(data) => data, - Err(e) => { - let err_msg = format!("{e}"); - write_error(err_msg); - return Err(()) - } + .await + { + Ok(data) => match data.text().await { + Ok(data) => data, + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()); } }, Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; @@ -249,7 +266,7 @@ pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Res Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; volume_brief_data.volumes.reverse(); // Makes breaking out of the volume loop easier @@ -272,11 +289,12 @@ pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Res let mut part_number: Option = None; let splits: Vec<&str> = volume.slug.split('-').collect(); for (index, split) in splits.clone().into_iter().enumerate() { - if split == "part" { - part_number = Some(splits[index +1] - .parse::() - .expect("Split Element after 'Part' should always be a number")); + part_number = Some( + splits[index + 1] + .parse::() + .expect("Split Element after 'Part' should always be a number"), + ); break; } } @@ -286,14 +304,17 @@ pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Res None => { println!("No Part found, assuming 1"); new_part_info = Part(1); - }, + } } - } - else { + } else { new_part_info = NoParts; } - let post_url = format!("{}/series/{series_slug}#volume-{}", jnc_base_url!(), volume.number); + let post_url = format!( + "{}/series/{series_slug}#volume-{}", + jnc_base_url!(), + volume.number + ); let post_details = LemmyPostInfo { title: volume.title.clone(), url: Url::parse(&post_url).unwrap(), @@ -321,7 +342,7 @@ pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Res if let Some(prepub_info) = get_latest_prepub(&volume.slug).await? { let prepub_post_info = Chapter { part: new_part_info, - lemmy_info: prepub_info + lemmy_info: prepub_info, }; prepub_map @@ -346,31 +367,29 @@ async fn get_latest_prepub(volume_slug: &str) -> Result, ( let response = match HTTP_CLIENT .get(api_url!() + "/volumes/" + volume_slug + "/parts?format=json") .send() - .await { - Ok(data) => { - match data.text().await { - Ok(data) => data, - Err(e) => { - let err_msg = format!("{e}"); - write_error(err_msg); - return Err(()) - } + .await + { + Ok(data) => match data.text().await { + Ok(data) => data, + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()); } }, Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; - let mut volume_prepub_parts_data: ChapterWrapper = match serde_json::from_str(&response) { Ok(data) => data, Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; volume_prepub_parts_data.parts.reverse(); // Makes breaking out of the parts loop easier @@ -380,10 +399,9 @@ async fn get_latest_prepub(volume_slug: &str) -> Result, ( for prepub_part in volume_prepub_parts_data.parts.iter() { let publishing_date = DateTime::parse_from_rfc3339(&prepub_part.launch).unwrap(); if publishing_date > Utc::now() { - break - } - else if publishing_date < Utc::now().sub(Duration::days(PAST_DAYS_ELIGIBLE as i64)) { - continue + break; + } else if publishing_date < Utc::now().sub(Duration::days(PAST_DAYS_ELIGIBLE as i64)) { + continue; } let post_url = format!("{}/read/{}", jnc_base_url!(), prepub_part.slug); @@ -391,7 +409,6 @@ async fn get_latest_prepub(volume_slug: &str) -> Result, ( title: prepub_part.title.clone(), url: Url::parse(&post_url).unwrap(), }); - } Ok(post_details) diff --git a/src/lemmy.rs b/src/lemmy.rs index b641577..b37f50b 100644 --- a/src/lemmy.rs +++ b/src/lemmy.rs @@ -1,4 +1,5 @@ -use std::collections::HashMap; +use crate::config::Config; +use crate::{write_error, HTTP_CLIENT}; use lemmy_api_common::community::{ListCommunities, ListCommunitiesResponse}; use lemmy_api_common::lemmy_db_views::structs::PostView; use lemmy_api_common::person::{Login, LoginResponse}; @@ -7,8 +8,7 @@ use lemmy_api_common::sensitive::Sensitive; use lemmy_db_schema::newtypes::{CommunityId, PostId}; use lemmy_db_schema::{ListingType, PostFeatureType}; use reqwest::StatusCode; -use crate::config::Config; -use crate::{HTTP_CLIENT, write_error}; +use std::collections::HashMap; pub(crate) struct Lemmy { jwt_token: Sensitive, @@ -26,18 +26,22 @@ pub(crate) async fn login(config: &Config) -> Result { .post(config.instance.to_owned() + "/api/v3/user/login") .json(&login_params) .send() - .await { + .await + { Ok(data) => data, Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; match response.status() { StatusCode::OK => { - let data: LoginResponse = response.json().await.expect("Successful Login Request should return JSON"); + let data: LoginResponse = response + .json() + .await + .expect("Successful Login Request should return JSON"); match data.jwt { Some(token) => Ok(Lemmy { jwt_token: token.clone(), @@ -49,7 +53,7 @@ pub(crate) async fn login(config: &Config) -> Result { Err(()) } } - }, + } status => { let err_msg = format!("Unexpected HTTP Status '{}' during Login", status); write_error(err_msg); @@ -65,21 +69,20 @@ impl Lemmy { .bearer_auth(&self.jwt_token.to_string()) .json(&post) .send() - .await { - Ok(data) => { - match data.text().await { - Ok(data) => data, - Err(e) => { - let err_msg = format!("{e}"); - write_error(err_msg); - return Err(()) - } + .await + { + Ok(data) => match data.text().await { + Ok(data) => data, + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()); } }, Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; @@ -88,7 +91,7 @@ impl Lemmy { Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; @@ -101,21 +104,20 @@ impl Lemmy { .bearer_auth(&self.jwt_token.to_string()) .json(¶ms) .send() - .await { - Ok(data) => { - match data.text().await { - Ok(data) => data, - Err(e) => { - let err_msg = format!("{e}"); - write_error(err_msg); - return Err(()) - } + .await + { + Ok(data) => match data.text().await { + Ok(data) => data, + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()); } }, Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; @@ -124,7 +126,7 @@ impl Lemmy { Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; @@ -161,21 +163,20 @@ impl Lemmy { .bearer_auth(&self.jwt_token.to_string()) .query(&list_params) .send() - .await { - Ok(data) => { - match data.text().await { - Ok(data) => data, - Err(e) => { - let err_msg = format!("{e}"); - write_error(err_msg); - return Err(()) - } + .await + { + Ok(data) => match data.text().await { + Ok(data) => data, + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()); } }, Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; @@ -184,16 +185,16 @@ impl Lemmy { Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; - Ok(json_data.posts.iter().filter(|post| { - post.post.featured_community - }) + Ok(json_data + .posts + .iter() + .filter(|post| post.post.featured_community) .cloned() - .collect() - ) + .collect()) } pub(crate) async fn get_local_pinned(&self) -> Result, ()> { @@ -207,21 +208,20 @@ impl Lemmy { .bearer_auth(&self.jwt_token.to_string()) .query(&list_params) .send() - .await { - Ok(data) => { - match data.text().await { - Ok(data) => data, - Err(e) => { - let err_msg = format!("{e}"); - write_error(err_msg); - return Err(()) - } + .await + { + Ok(data) => match data.text().await { + Ok(data) => data, + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()); } }, Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; @@ -230,16 +230,16 @@ impl Lemmy { Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; - Ok(json_data.posts.iter().filter(|post| { - post.post.featured_local - }) + Ok(json_data + .posts + .iter() + .filter(|post| post.post.featured_local) .cloned() - .collect() - ) + .collect()) } pub(crate) async fn get_communities(&self) -> Result, ()> { @@ -253,21 +253,20 @@ impl Lemmy { .bearer_auth(&self.jwt_token.to_string()) .query(&list_params) .send() - .await { - Ok(data) => { - match data.text().await { - Ok(data) => data, - Err(e) => { - let err_msg = format!("{e}"); - write_error(err_msg); - return Err(()) - } + .await + { + Ok(data) => match data.text().await { + Ok(data) => data, + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + return Err(()); } }, Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; @@ -276,7 +275,7 @@ impl Lemmy { Err(e) => { let err_msg = format!("{e}"); write_error(err_msg); - return Err(()) + return Err(()); } }; diff --git a/src/main.rs b/src/main.rs index be977d5..b3b0fe7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,37 +1,37 @@ -use chrono::{DateTime, Duration, Utc}; -use once_cell::sync::Lazy; -use reqwest::{Client}; -use std::{collections::HashMap}; -use std::fmt::Debug; -use std::sync::{Arc}; -use log::{error, warn, info, LevelFilter}; -use tokio::sync::{RwLock}; -use systemd_journal_logger::{connected_to_journal, JournalLog}; -use tokio::time::sleep; use crate::config::Config; -use crate::post_history::{SeriesHistory}; +use crate::post_history::SeriesHistory; +use chrono::{DateTime, Duration, Utc}; +use log::{error, info, warn, LevelFilter}; +use once_cell::sync::Lazy; +use reqwest::Client; +use std::collections::HashMap; +use std::fmt::Debug; +use std::sync::Arc; +use systemd_journal_logger::{connected_to_journal, JournalLog}; +use tokio::sync::RwLock; +use tokio::time::sleep; +mod bot; mod config; mod jnovel; -mod bot; mod lemmy; mod post_history; -pub (crate) fn write_error(err_msg: String) { +pub(crate) fn write_error(err_msg: String) { match connected_to_journal() { true => error!("[ERROR] {err_msg}"), false => println!("[ERROR] {err_msg}"), } } -pub (crate) fn write_warn(warn_msg: String) { +pub(crate) fn write_warn(warn_msg: String) { match connected_to_journal() { true => warn!("[WARN] {warn_msg}"), false => println!("[WARN] {warn_msg}"), } } -pub (crate) fn write_info(info_msg: String) { +pub(crate) fn write_info(info_msg: String) { match connected_to_journal() { true => info!("[INFO] {info_msg}"), false => println!("[INFO] {info_msg}"), @@ -67,7 +67,10 @@ impl SharedData { #[tokio::main] async fn main() { - JournalLog::new().expect("Systemd-Logger crate error").install().expect("Systemd-Logger crate error"); + JournalLog::new() + .expect("Systemd-Logger crate error") + .install() + .expect("Systemd-Logger crate error"); log::set_max_level(LevelFilter::Info); let mut data = SharedData::new(); @@ -87,10 +90,15 @@ async fn main() { let err_msg = "Bot crashed due to unknown Error, restarting thread after wait..."; match connected_to_journal() { true => error!("[ERROR] {err_msg}"), - false => println!("[ERROR] {err_msg}") + false => println!("[ERROR] {err_msg}"), } } - sleep(Duration::seconds(5).to_std().expect("Conversion should always work since static")).await; + sleep( + Duration::seconds(5) + .to_std() + .expect("Conversion should always work since static"), + ) + .await; } } diff --git a/src/post_history.rs b/src/post_history.rs index 364f444..5e3b57e 100644 --- a/src/post_history.rs +++ b/src/post_history.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; -use serde_derive::{Deserialize, Serialize}; use crate::write_error; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Default, Clone, Debug)] pub(crate) struct SeriesHistory { @@ -25,7 +25,7 @@ impl SeriesHistory { pub(crate) fn check_for_post(&self, series: &str, part: &str, title: &str) -> bool { if let Some(series_map) = self.series.get(series) { if let Some(part_info) = series_map.parts.get(part) { - return part_info.volume == title || part_info.chapter == title + return part_info.volume == title || part_info.chapter == title; } } false @@ -35,15 +35,15 @@ impl SeriesHistory { match self.series.get(series) { Some(history) => history.clone(), None => PostHistory { - parts: HashMap::new() - } + parts: HashMap::new(), + }, } } pub(crate) fn set_series(&mut self, series: &str, data: PostHistory) { - self.series.entry(series.to_owned()).and_modify(|val| { - *val = data.clone() - }) + self.series + .entry(series.to_owned()) + .and_modify(|val| *val = data.clone()) .or_insert(data); } } @@ -60,14 +60,14 @@ impl PostHistory { None => PostHistoryInner { volume: "".to_owned(), chapter: "".to_owned(), - } + }, } } pub(crate) fn set_part(&mut self, part: &str, data: PostHistoryInner) { - self.parts.entry(part.to_owned()).and_modify(|val| { - *val = data.clone() - }) + self.parts + .entry(part.to_owned()) + .and_modify(|val| *val = data.clone()) .or_insert(data); } }