diff --git a/Cargo.lock b/Cargo.lock index 4a23cd0..25bdb32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,18 +43,20 @@ dependencies = [ [[package]] name = "aob-lemmy-bot" -version = "2.1.6" +version = "2.2.0-rc.1" dependencies = [ "chrono", "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..527d98a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Neshura"] name = "aob-lemmy-bot" -version = "2.1.6" +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" @@ -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 diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..e9f0f81 --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,274 @@ +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 std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use tokio::time::sleep; + +pub(crate) async fn run(data: Arc>) { + let mut last_reload: DateTime; + let mut lemmy: Lemmy; + let mut login_error: bool; + let mut communities; + { + 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 = Config::load(); + last_reload = Utc::now(); + } + + { + let read = data.read().await; + lemmy = match lemmy::login(&read.config).await { + Ok(data) => data, + Err(_) => panic!(), + }; + login_error = false; + + communities = match lemmy.get_communities().await { + Ok(data) => data, + Err(_) => panic!(), + }; + } + + { + let mut write = data.write().await; + write.start = Utc::now(); + } + + let info_msg = "Bot init successful, starting normal operations".to_owned(); + write_info(info_msg); + + loop { + idle(&data).await; + + { + let mut write = data.write().await; + + write.start = Utc::now(); + + if write.start - last_reload >= Duration::seconds(write.config.config_reload_seconds as i64) { + 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(&read.config).await { + Ok(data) => data, + Err(_) => continue, + }; + login_error = false; + } + } + + { + let read = data.read().await; + if read.start - last_reload >= Duration::seconds(read.config.config_reload_seconds as i64) { + communities = match lemmy.get_communities().await { + Ok(data) => data, + Err(_) => { + login_error = true; + continue; + } + }; + let message = "Communities reloaded".to_owned(); + write_info(message); + last_reload = Utc::now(); + } + } + + { + let mut write = data.write().await; + write.post_history = SeriesHistory::load_history(); + } + + { + let read = data.read().await; + let series = read.config.series.clone(); + drop(read); + for series in series { + if handle_series(&series, &communities, &lemmy, &data) + .await + .is_err() + { + login_error = true; + continue; + }; + } + } + + idle(&data).await; + } +} + +async fn idle(data: &Arc>) { + let read = data.read().await; + let mut sleep_duration = Duration::seconds(30); + if Utc::now() - read.start > sleep_duration { + sleep_duration = Duration::seconds(60); + } + + if let Some(status_url) = read.config.status_post_url.clone() { + match reqwest::get(status_url).await { + Ok(_) => {} + Err(e) => { + let err_msg = format!("{e}"); + write_error(err_msg); + } + } + }; + + while Utc::now() - read.start < sleep_duration { + sleep(Duration::milliseconds(100).to_std().unwrap()).await; + } +} + +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 + 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(), + ) { + let message = format!("Skipping '{}' since already posted", post_lemmy_info.title); + write_info(message); + post_list.remove(index); + continue; + } + } + + let post_series_config = match post_info { + PostInfo::Chapter { .. } => &series.prepub_community, + PostInfo::Volume { .. } => &series.volume_community, + }; + + let community_id = *communities + .get(post_series_config.name.as_str()) + .expect("Given community is invalid"); + + let post_body = match &post_series_config.post_body { + PostBody::None => None, + PostBody::Description => post_info.get_description(), + PostBody::Custom(text) => Some(text.clone()), + }; + + let post_data = CreatePost { + name: post_lemmy_info.title.clone(), + community_id, + url: Some(post_lemmy_info.url), + body: post_body, + honeypot: None, + nsfw: None, + 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]; + 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 + ); + write_warn(message); + } + } + + 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 { + 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.pin(post_id, PostFeatureType::Local).await?; + } + + let mut series_history = read.post_history.get_series(series.slug.as_str()); + let mut part_history = series_history.get_part(post_part_info.as_string().as_str()); + 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, + } + + 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.save_history(); + } + Ok(()) +} diff --git a/src/bot/mod.rs b/src/bot/mod.rs deleted file mode 100644 index 50577d8..0000000 --- a/src/bot/mod.rs +++ /dev/null @@ -1,318 +0,0 @@ -use std::collections::HashMap; -use std::sync::{Arc}; -use chrono::{DateTime, Duration, Timelike, 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, Message, SharedData}; -use crate::config::{Config, PostBody, SeriesConfig}; -use crate::jnovel::PostInfo; -use crate::lemmy::{Lemmy}; -use crate::post_history::SeriesHistory; -use tokio::time::sleep; - -pub(crate) async fn run(data: Arc>) { - let mut last_reload: DateTime; - 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), - }; - 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 { - Ok(data) => data, - Err(e) => panic!("{}", e), - }; - login_error = false; - - communities = match lemmy.get_communities().await { - Ok(data) => data, - Err(e) => panic!("{}", e), - }; - } - - 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(); - } - - loop { - idle(&data).await; - - { - let mut write = data.write().await; - - 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); - } - } - - { - let read = data.read().await; - if login_error { - lemmy = match lemmy::login(&credentials, read.config.instance.as_str()).await { - Ok(data) => data, - Err(e) => { - drop(read); - let mut write = data.write().await; - write.messages.push(Message::Error(e.to_string())); - continue - } - }; - login_error = false; - } - } - - { - let read = data.read().await; - 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) => { - 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); - }; - last_reload = Utc::now(); - } - } - - { - 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 - } - }; - } - - { - let read = data.read().await; - 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 - } - }; - } - } - - 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; - } -} - -async fn idle(data: &Arc>) { - let read = data.read().await; - let mut sleep_duration = Duration::seconds(30); - if Utc::now() - read.start > sleep_duration { - sleep_duration = Duration::seconds(60); - } - - if let Some(status_url) = read.config.status_post_url.clone() { - match reqwest::get(status_url).await { - Ok(_) => {} - Err(e) => { - let mut write = data.write().await; - write.messages.push(Message::Error(e.to_string())) - }, - } - }; - - while Utc::now() - read.start < sleep_duration { - sleep(Duration::milliseconds(100).to_std().unwrap()).await; - } -} - -async fn handle_series( - series: &SeriesConfig, - communities: &HashMap, - lemmy: &Lemmy, - data: &Arc>, -) -> Result<(), String> { - - 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()); - }, - }; - - 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()) { - 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); - }; - post_list.remove(index); - continue - } - } - - let post_series_config = match post_info { - PostInfo::Chapter {..} => {&series.prepub_community}, - PostInfo::Volume {..} => {&series.volume_community} - }; - - let community_id = *communities - .get(post_series_config.name.as_str()) - .expect("Given community is invalid"); - - let post_body = match &post_series_config.post_body { - PostBody::None => None, - PostBody::Description => post_info.get_description(), - PostBody::Custom(text) => Some(text.clone()), - }; - - let post_data = CreatePost { - name: post_lemmy_info.title, - community_id, - url: Some(post_lemmy_info.url), - body: post_body, - honeypot: None, - nsfw: None, - 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 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 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.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 read = data.read().await; - if post_series_config.pin_settings.pin_new_post_local { - 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 { - let community_pinned_post = &pinned_post; - lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Local).await?; - break - } - } - } - lemmy.pin(post_id, PostFeatureType::Local).await?; - } - - let mut series_history = read.post_history.get_series(series.slug.as_str()); - let mut part_history = series_history.get_part(post_part_info.as_string().as_str()); - 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 - } - } - - 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) => return Err(format!("{}", e)) - }; - } - Ok(()) -} - - - diff --git a/src/config/mod.rs b/src/config.rs similarity index 84% rename from src/config/mod.rs rename to src/config.rs index 4eddd4b..776d868 100644 --- a/src/config/mod.rs +++ b/src/config.rs @@ -1,11 +1,12 @@ -use serde_derive::{Deserialize, Serialize}; use crate::config::PostBody::Description; +use lemmy_api_common::sensitive::Sensitive; +use serde_derive::{Deserialize, Serialize}; #[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, @@ -13,7 +14,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 +37,15 @@ impl Config { panic!("'Description' type Post Body only supported for Volumes!") } }); - Ok(cfg) + 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()) } } @@ -49,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/mod.rs b/src/jnovel.rs similarity index 72% rename from src/jnovel/mod.rs rename to src/jnovel.rs index abd3ffe..b27c88c 100644 --- a/src/jnovel/mod.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}; -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 @@ -222,23 +240,34 @@ 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() - .await { - Ok(data) => { - match data.text().await { - Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + .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) => 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 @@ -260,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; } } @@ -274,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(), @@ -309,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 @@ -330,24 +363,34 @@ 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() - .await { - Ok(data) => { - match data.text().await { - Ok(data) => data, - Err(e) => return Err(e.to_string()) + .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) => 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 @@ -356,10 +399,9 @@ async fn get_latest_prepub(volume_slug: &str) -> Result, S 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); @@ -367,7 +409,6 @@ async fn get_latest_prepub(volume_slug: &str) -> Result, S title: prepub_part.title.clone(), url: Url::parse(&post_url).unwrap(), }); - } Ok(post_details) diff --git a/src/lemmy/mod.rs b/src/lemmy.rs similarity index 51% rename from src/lemmy/mod.rs rename to src/lemmy.rs index 8265942..b37f50b 100644 --- a/src/lemmy/mod.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,117 +8,132 @@ 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; - -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 std::collections::HashMap; 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 { + .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() { 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(), - 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); + Err(()) + } } - }, - status => panic!("Unexpected HTTP Status '{}' during Login", status) + } + status => { + let err_msg = format!("Unexpected HTTP Status '{}' during Login", status); + write_error(err_msg); + 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()) .json(&post) .send() - .await { - Ok(data) => { - match data.text().await { - Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + .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) => 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()) .json(¶ms) .send() - .await { - Ok(data) => { - match data.text().await { - Ok(data) => data, - Err(e) => return Err(format!("{}", e)) + .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) => 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 +142,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 +151,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), @@ -147,30 +163,41 @@ 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) => return Err(format!("{}", e)) + .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) => 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| { - 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, String> { + pub(crate) async fn get_local_pinned(&self) -> Result, ()> { let list_params = GetPosts { type_: Some(ListingType::Local), ..Default::default() @@ -181,30 +208,41 @@ 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) => return Err(format!("{}", e)) + .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) => 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| { - 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, String> { + pub(crate) async fn get_communities(&self) -> Result, ()> { let list_params = ListCommunities { type_: Some(ListingType::Local), ..Default::default() @@ -215,19 +253,30 @@ 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) => return Err(format!("{}", e)) + .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) => 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(); diff --git a/src/main.rs b/src/main.rs index 0c9f929..b3b0fe7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,43 @@ -use chrono::{DateTime, Duration, Utc}; -use once_cell::sync::Lazy; -use reqwest::{Client}; -use std::{collections::HashMap, vec}; -use std::fmt::Debug; -use std::sync::{Arc}; -use tokio::sync::{RwLock}; -use strum_macros::Display; -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 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,49 @@ 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())); - sleep(Duration::seconds(5).to_std().expect("Conversion should always work since static")).await; + + { + 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; } } diff --git a/src/post_history.rs b/src/post_history.rs new file mode 100644 index 0000000..5e3b57e --- /dev/null +++ b/src/post_history.rs @@ -0,0 +1,79 @@ +use crate::write_error; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Serialize, Deserialize, Default, Clone, Debug)] +pub(crate) struct SeriesHistory { + pub(crate) series: HashMap, +} + +impl SeriesHistory { + 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) { + 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 { + 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; + } + } + false + } + + pub(crate) fn get_series(&self, series: &str) -> PostHistory { + match self.series.get(series) { + Some(history) => history.clone(), + None => PostHistory { + 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()) + .or_insert(data); + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub(crate) struct PostHistory { + pub(crate) parts: HashMap, +} + +impl PostHistory { + pub(crate) fn get_part(&self, part: &str) -> PostHistoryInner { + match self.parts.get(part) { + Some(history) => history.clone(), + 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()) + .or_insert(data); + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub(crate) struct PostHistoryInner { + pub(crate) volume: String, + pub(crate) chapter: String, +} diff --git a/src/post_history/mod.rs b/src/post_history/mod.rs deleted file mode 100644 index a1c8e68..0000000 --- a/src/post_history/mod.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::collections::HashMap; -use std::fs; -use std::fs::OpenOptions; -use std::io::Write; -use std::path::Path; -use serde_derive::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, 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"); - - - - match Path::new(format!("{}/history.toml", config_dir.to_str().expect("Conversion to str should not fail for a dir")).as_str()).exists() { - true => { - let file_contents: String = match fs::read_to_string("history.toml") { - Ok(data) => data, - Err(e) => return Err(format!("{}", e)), - }; - - 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) => Err(format!("{}", e)) - } - }, - false => { - Ok(SeriesHistory { - series: HashMap::new(), - }) - } - } - } - - 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 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 - } - } - false - } - - pub(crate) fn get_series(&self, series: &str) -> PostHistory { - match self.series.get(series) { - Some(history) => history.clone(), - None => PostHistory { - 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() - }) - .or_insert(data); - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub(crate) struct PostHistory { - pub(crate) parts: HashMap, -} - -impl PostHistory { - pub(crate) fn get_part(&self, part: &str) -> PostHistoryInner { - match self.parts.get(part) { - Some(history) => history.clone(), - 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() - }) - .or_insert(data); - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub(crate) struct PostHistoryInner { - pub(crate) volume: String, - pub(crate) chapter: String, -} 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