diff --git a/Cargo.lock b/Cargo.lock index e713680..9bc31b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,7 +54,7 @@ dependencies = [ name = "aob-lemmy-bot" version = "3.1.0" dependencies = [ - "async-trait", + "atomic_enum", "chrono", "confy", "lemmy_api_common", @@ -62,13 +62,13 @@ dependencies = [ "log", "notify", "once_cell", + "parking_lot", "reqwest", "serde", "serde_derive", "serde_json", "strum_macros", "systemd-journal-logger", - "tokio", "toml", "url", ] @@ -95,6 +95,17 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic_enum" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e1aca718ea7b89985790c94aad72d77533063fe00bc497bb79a7c2dae6a661" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -445,9 +456,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1199,9 +1210,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1597,9 +1608,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" dependencies = [ "proc-macro2", "quote", @@ -1747,21 +1758,9 @@ dependencies = [ "num_cpus", "pin-project-lite", "socket2", - "tokio-macros", "windows-sys 0.48.0", ] -[[package]] -name = "tokio-macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tokio-native-tls" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index fd82bf3..540786c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,11 +25,11 @@ serde = "^1.0" serde_derive = "^1.0" serde_json = "^1.0" strum_macros = "^0.26" -tokio = { version = "^1.37", features = ["rt", "rt-multi-thread", "macros"] } url = "^2.5" confy = "^0.6" toml = "^0.8" systemd-journal-logger = "^2.1.1" log = "^0.4" -async-trait = "^0.1" -notify = "6.1.1" \ No newline at end of file +notify = "6.1.1" +parking_lot = "0.12.3" +atomic_enum = "0.3.0" \ No newline at end of file diff --git a/README.md b/README.md index dd1d5c5..e75d9b1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ instance = "https://lemmy.example.org" username = "BotUserName" password = "BotPassword" status_post_url = "PostUrlForStatusMonitoring" -config_reload_seconds = 10800 protected_communities = [ "community_where_pins_should_stay" ] diff --git a/src/bot.rs b/src/bot.rs index ba0e8b9..a8b0d9c 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,136 +1,113 @@ -use crate::{config::{Config}, HTTP_CLIENT}; +use std::collections::HashMap; +use crate::{AtomicHealthSignal, HealthSignal, HTTP_CLIENT}; use crate::lemmy::{Lemmy}; use crate::post_history::{SeriesHistory}; use chrono::{DateTime, Duration, Utc}; -use std::sync::{Arc, RwLock}; -use notify::{Event, EventKind, event::{AccessKind, AccessMode}, RecursiveMode, Watcher}; -use tokio::time::sleep; -use systemd_journal_logger::connected_to_journal; - -macro_rules! debug { - ($msg:tt) => { - match connected_to_journal() { - true => log::debug!("[DEBUG] {}", $msg), - false => println!("[DEBUG] {}", $msg), - } - }; -} - -macro_rules! info { - ($msg:tt) => { - match connected_to_journal() { - true => log::info!("[INFO] {}", $msg), - false => println!("[INFO] {}", $msg), - } - }; -} - -macro_rules! error { - ($msg:tt) => { - match connected_to_journal() { - true => log::error!("[ERROR] {}", $msg), - false => eprintln!("[ERROR] {}", $msg), - } - }; -} +use std::sync::{Arc}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread::sleep; +use parking_lot::RwLock; +use crate::logging::Logging; +use crate::settings::{BotSettings, SeriesSettings}; pub(crate) struct Bot { - shared_config: Arc>, + settings: BotSettings, + health_signal: Arc, history: SeriesHistory, - run_start_time: DateTime + login_time: Option> } enum Wait { Absolute, - Buffer + Buffer(DateTime) } impl Bot { - pub(crate) fn new() -> Self { - let config = Config::load(); - let shared_config: Arc> = Arc::new(RwLock::new(config)); + pub(crate) fn new(health_signal: Arc, settings: BotSettings) -> Self { + let history: SeriesHistory = SeriesHistory::load_history(); + + Bot { health_signal, settings, history, login_time: None } + } + pub(crate) fn run(&mut self, series_settings: Arc>>, update_pending: Arc) { + loop { + match self.health_signal.load(Ordering::SeqCst) { + HealthSignal::RequestAlive => { + //self.health_signal.store(HealthSignal::ConfirmAlive, Ordering::SeqCst); + } + HealthSignal::RequestStop => { + break; + } + _ => {} + } - let shared_config_copy = shared_config.clone(); - let mut watcher = notify::recommended_watcher(move |res: Result| { - match res { - Ok(event) => { - if event.kind == EventKind::Access(AccessKind::Close(AccessMode::Write)) { - let mut write = shared_config_copy.write().expect("Write Lock Failed"); - let new_config = Config::load(); - write.series = new_config.series; - write.instance = new_config.instance; - write.protected_communities = new_config.protected_communities; - write.status_post_url = new_config.status_post_url; - info!("Reloaded Configuration"); - } - }, - Err(e) => { - let msg = format!("Error watching files: {e}"); - error!(msg); + while update_pending.load(Ordering::SeqCst) { + sleep(Duration::milliseconds(100).to_std().unwrap()); + } + + if let Some(login_time) = self.login_time { + if Utc::now() - login_time >= Duration::minutes(60) { + self.logout(); + continue } } - }).expect("Watcher Error"); - - watcher.watch(&Config::get_path(), RecursiveMode::NonRecursive).expect("Error in watcher"); - - let history: SeriesHistory = SeriesHistory::load_history(); - - Bot { shared_config, history, run_start_time: Utc::now() } - } - pub(crate) async fn run(&mut self) { - loop { - let mut lemmy = match Lemmy::new(&self.shared_config).await { - Ok(data) => data, - Err(_) => continue, - }; - - lemmy.get_communities().await; + else { + self.login(); + } self.history = SeriesHistory::load_history(); + + let run_start_time = Utc::now(); + self.ping_status(); - let start: DateTime = Utc::now(); - while Utc::now() - start <= Duration::minutes(60) { - self.run_start_time = Utc::now(); - self.ping_status().await; - let read_copy = self.shared_config.read().expect("Read Lock Failed").clone(); - for series in read_copy.series { - series.update(&mut self.history, &lemmy, &self.shared_config).await; - debug!("Done Updating Series"); - self.wait(1, Wait::Absolute).await; - } - debug!("Awaiting Timeout"); - self.wait(30, Wait::Buffer).await; - debug!("Pinging Server"); - self.ping_status().await; - debug!("Awaiting Timeout 2"); - self.wait(30, Wait::Absolute).await; + series_settings.read().iter().for_each(|(id, series)| { + let new_entries = series.check(); + }); + for series in series_settings.read().iter() { + //series.update(&mut self.history, &lemmy, &self.settings).await; + Logging::debug("Done Updating Series"); + self.wait(1, Wait::Absolute); } - - lemmy.logout().await; + Logging::debug("Awaiting Timeout"); + self.wait(30, Wait::Buffer(run_start_time)); + Logging::debug("Pinging Server"); + self.ping_status(); + Logging::debug("Awaiting Timeout 2"); + self.wait(30, Wait::Absolute); } + self.health_signal.store(HealthSignal::ConfirmStop, Ordering::SeqCst); } - async fn ping_status(&self) { - let read_config = &self.shared_config.read().expect("Read Lock Failed").clone(); - if let Some(status_url) = &read_config.status_post_url { - match HTTP_CLIENT.get(status_url).send().await { + fn ping_status(&self) { + if let Some(status_url) = &self.settings.status_post_url() { + match HTTP_CLIENT.get(status_url).send() { Ok(_) => {}, Err(e) => { let err_msg = format!("While pinging status URL: {e}"); - error!(err_msg); + Logging::error(err_msg.as_str()); } } } } - async fn wait(&self, seconds: i64, start_time: Wait) { + fn wait(&self, seconds: i64, start_time: Wait) { let duration: Duration = Duration::seconds(seconds); let start_time: DateTime = match start_time { Wait::Absolute => Utc::now(), - Wait::Buffer => self.run_start_time, + Wait::Buffer(time) => time, }; while Utc::now() - start_time < duration { - sleep(Duration::milliseconds(100).to_std().unwrap()).await + sleep(Duration::milliseconds(100).to_std().unwrap()); } } + + fn login(&mut self) { + // + self.login_time = Some(Utc::now()); + todo!() + } + + fn logout(&mut self) { + self.login_time = None; + todo!() + } } diff --git a/src/config.rs b/src/config.rs index 34627b3..76c9444 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,88 +1,47 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock}; use chrono::{Timelike, Utc}; -use crate::config::PostBody::Description; +use crate::settings::PostBody::Description; use lemmy_db_schema::PostFeatureType; use lemmy_db_schema::sensitive::SensitiveString; use serde_derive::{Deserialize, Serialize}; use crate::lemmy::{Lemmy, PartInfo, PostType}; use crate::post_history::{SeriesHistory}; -use systemd_journal_logger::connected_to_journal; use crate::fetchers::{FetcherTrait, Fetcher}; use crate::fetchers::jnovel::{JNovelFetcher}; - -macro_rules! debug { - ($msg:tt) => { - match connected_to_journal() { - true => log::debug!("[DEBUG] {}", $msg), - false => println!("[DEBUG] {}", $msg), - } - }; -} - -macro_rules! info { - ($msg:tt) => { - match connected_to_journal() { - true => log::info!("[INFO] {}", $msg), - false => println!("[INFO] {}", $msg), - } - }; -} - -macro_rules! warn { - ($msg:tt) => { - match connected_to_journal() { - true => log::warn!("[WARN] {}", $msg), - false => println!("[WARN] {}", $msg), - } - }; -} - -macro_rules! error { - ($msg:tt) => { - match connected_to_journal() { - true => log::error!("[ERROR] {}", $msg), - false => eprintln!("[ERROR] {}", $msg), - } - }; -} +use crate::logging::Logging; +use crate::settings::PostBody; #[derive(Serialize, Deserialize, Clone, Debug)] -pub(crate) struct Config { +pub(crate) struct SingleBotConfig { pub(crate) instance: String, username: SensitiveString, password: SensitiveString, pub(crate) status_post_url: Option, - pub(crate) config_reload_seconds: u32, pub(crate) protected_communities: Vec, pub(crate) series: Vec, } -impl Config { - 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}"), - }; - - if cfg.instance.is_empty() { +impl SingleBotConfig { + pub(crate) fn load(&self) -> Self { + if self.instance.is_empty() { panic!("bot instance not set!") } - if cfg.username.is_empty() { + if self.username.is_empty() { panic!("bot username not set!") } - if cfg.password.is_empty() { + if self.password.is_empty() { panic!("bot password not provided!") } - cfg.series.iter().for_each(|series| { + self.series.iter().for_each(|series| { if series.prepub_community.post_body == Description { panic!("'Description' type Post Body only supported for Volumes!") } }); - cfg + self.clone() } pub(crate) fn get_path() -> PathBuf { @@ -98,14 +57,13 @@ impl Config { } } -impl Default for Config { +impl Default for SingleBotConfig { fn default() -> Self { - Config { + SingleBotConfig { instance: "".to_owned(), username: SensitiveString::from("".to_owned()), password: SensitiveString::from("".to_owned()), status_post_url: None, - config_reload_seconds: 21600, protected_communities: vec![], series: vec![], } @@ -122,9 +80,9 @@ pub(crate) struct SeriesConfig { } impl SeriesConfig { - pub(crate) async fn update(&self, history: &mut SeriesHistory, lemmy: &Lemmy, config: &Arc>) { + pub(crate) fn update(&self, history: &mut SeriesHistory, lemmy: &Lemmy, config: &Arc>) { let info_msg = format!("Checking {} for Updates", self.slug); - info!(info_msg); + Logging::info(info_msg.as_str()); let mut fetcher: Fetcher = match &self.fetcher { Fetcher::Jnc(_) => { @@ -144,18 +102,18 @@ impl SeriesConfig { } } - let post_list = match fetcher.check_feed().await { + let post_list = match fetcher.check_feed() { Ok(data) => data, Err(_) => { let err_msg = format!("While checking feed for {}", self.slug); - error!(err_msg); + Logging::error(err_msg.as_str()); return; } }; if post_list.is_empty() && Utc::now().minute() % 10 == 0 { let info_msg = "No Updates found"; - info!(info_msg); + Logging::info(info_msg); } for post_info in post_list.iter() { @@ -174,12 +132,12 @@ impl SeriesConfig { post_info.get_info().title.as_str(), post_info.get_post_config(self).name.as_str() ); - info!(info); + Logging::info(info.as_str()); - let post_id = match lemmy.post(post_data).await { + let post_id = match lemmy.post(post_data) { Some(data) => data, None=> { - error!("Error posting chapter"); + Logging::error("Error posting chapter"); return; } }; @@ -196,19 +154,19 @@ impl SeriesConfig { post_info.get_info().title, post_info.get_post_config(self).name.as_str() ); - info!(info); - let pinned_posts = lemmy.get_community_pinned(lemmy.get_community_id(&post_info.get_post_config(self).name)).await.unwrap_or_else(|| { - error!("Pinning of Post to community failed"); + Logging::info(info.as_str()); + let pinned_posts = lemmy.get_community_pinned(lemmy.get_community_id(&post_info.get_post_config(self).name)).unwrap_or_else(|| { + Logging::error("Pinning of Post to community failed"); vec![] }); if !pinned_posts.is_empty() { let community_pinned_post = &pinned_posts[0]; - if lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Community).await.is_none() { - error!("Error un-pinning post"); + if lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Community).is_none() { + Logging::error("Error un-pinning post"); } } - if lemmy.pin(post_id, PostFeatureType::Community).await.is_none() { - error!("Error pinning post"); + if lemmy.pin(post_id, PostFeatureType::Community).is_none() { + Logging::error("Error pinning post"); } } else if read_config .protected_communities @@ -218,16 +176,16 @@ impl SeriesConfig { "Community '{}' for Series '{}' is protected. Is this intended?", &post_info.get_post_config(self).name, self.slug ); - warn!(message); + Logging::warn(message.as_str()); } if post_info.get_post_config(self).pin_settings.pin_new_post_local { let info = format!("Pinning '{}' to Instance", post_info.get_info().title); - info!(info); - let pinned_posts = match lemmy.get_local_pinned().await { + Logging::info(info.as_str()); + let pinned_posts = match lemmy.get_local_pinned() { Some(data) => {data} None => { - error!("Error fetching pinned posts"); + Logging::error("Error fetching pinned posts"); vec![] } }; @@ -241,16 +199,16 @@ impl SeriesConfig { continue; } else { let community_pinned_post = &pinned_post; - if lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Local).await.is_none() { - error!("Error pinning post"); + if lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Local).is_none() { + Logging::error("Error pinning post"); continue; } break; } } } - if lemmy.pin(post_id, PostFeatureType::Local).await.is_none() { - error!("Error pinning post"); + if lemmy.pin(post_id, PostFeatureType::Local).is_none() { + Logging::error("Error pinning post"); }; } @@ -270,7 +228,7 @@ impl SeriesConfig { series_history.set_part(post_info.get_part_info().unwrap_or(PartInfo::NoParts).as_string().as_str(), part_history); history .set_series(self.slug.as_str(), series_history); - debug!("Saving History"); + Logging::debug("Saving History"); history.save_history(); } } @@ -288,11 +246,3 @@ pub(crate) struct PinConfig { pub(crate) pin_new_post_local: bool, pub(crate) pin_new_post_community: bool, } - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(tag = "body_type", content = "body_content")] -pub(crate) enum PostBody { - None, - Description, - Custom(String), -} diff --git a/src/fetchers/jnovel.rs b/src/fetchers/jnovel.rs index 493ed07..e7b6ea5 100644 --- a/src/fetchers/jnovel.rs +++ b/src/fetchers/jnovel.rs @@ -3,29 +3,10 @@ use chrono::{DateTime, Duration, Utc}; use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; use std::ops::Sub; -use async_trait::async_trait; use crate::fetchers::{FetcherTrait}; use crate::lemmy::{PartInfo, PostInfo, PostInfoInner, PostType}; -use systemd_journal_logger::connected_to_journal; use crate::lemmy::PartInfo::{NoParts, Part}; - -macro_rules! error { - ($msg:tt) => { - match connected_to_journal() { - true => log::error!("[ERROR] {}", $msg), - false => eprintln!("[ERROR] {}", $msg), - } - }; -} - -macro_rules! info { - ($msg:tt) => { - match connected_to_journal() { - true => log::info!("[INFO] {}", $msg), - false => println!("[INFO] {}", $msg), - } - }; -} +use crate::logging::Logging; static PAST_DAYS_ELIGIBLE: u8 = 4; @@ -113,7 +94,6 @@ impl JNovelFetcher { } } -#[async_trait] impl FetcherTrait for JNovelFetcher { fn new() -> Self { JNovelFetcher { @@ -122,23 +102,22 @@ impl FetcherTrait for JNovelFetcher { } } - async fn check_feed(&self) -> Result, ()> { + fn check_feed(&self) -> Result, ()> { let response = match HTTP_CLIENT .get(api_url!() + "/series/" + self.series_slug.as_str() + "/volumes?format=json") .send() - .await { - Ok(data) => match data.text().await { + Ok(data) => match data.text() { Ok(data) => data, Err(e) => { let err_msg = format!("While checking feed: {e}"); - error!(err_msg); + Logging::error(err_msg.as_str()); return Err(()); } }, Err(e) => { let err_msg = format!("{e}"); - error!(err_msg); + Logging::error(err_msg.as_str()); return Err(()); } }; @@ -147,7 +126,7 @@ impl FetcherTrait for JNovelFetcher { Ok(data) => data, Err(e) => { let err_msg = format!("{e}"); - error!(err_msg); + Logging::error(err_msg.as_str()); return Err(()); } }; @@ -184,7 +163,7 @@ impl FetcherTrait for JNovelFetcher { match part_number { Some(number) => new_part_info = Part(number), None => { - info!("No Part found, assuming 1"); + Logging::info("No Part found, assuming 1"); new_part_info = Part(1); } } @@ -224,7 +203,7 @@ impl FetcherTrait for JNovelFetcher { .or_insert(new_post_info); } - if let Some(prepub_info) = get_latest_prepub(&volume.slug).await { + if let Some(prepub_info) = get_latest_prepub(&volume.slug) { let prepub_post_info = PostInfo { post_type: Some(PostType::Chapter), part: Some(new_part_info), @@ -252,23 +231,22 @@ impl FetcherTrait for JNovelFetcher { } -async fn get_latest_prepub(volume_slug: &str) -> Option { +fn get_latest_prepub(volume_slug: &str) -> Option { let response = match HTTP_CLIENT .get(api_url!() + "/volumes/" + volume_slug + "/parts?format=json") .send() - .await { - Ok(data) => match data.text().await { + Ok(data) => match data.text() { Ok(data) => data, Err(e) => { let err_msg = format!("While getting latest PrePub: {e}"); - error!(err_msg); + Logging::error(err_msg.as_str()); return None; } }, Err(e) => { let err_msg = format!("{e}"); - error!(err_msg); + Logging::error(err_msg.as_str()); return None; } }; @@ -277,7 +255,7 @@ async fn get_latest_prepub(volume_slug: &str) -> Option { Ok(data) => data, Err(e) => { let err_msg = format!("{e}"); - error!(err_msg); + Logging::error(err_msg.as_str()); return None; } }; diff --git a/src/fetchers/mod.rs b/src/fetchers/mod.rs index 0f2eacc..2181914 100644 --- a/src/fetchers/mod.rs +++ b/src/fetchers/mod.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use serde_derive::{Deserialize, Serialize}; use strum_macros::Display; use crate::fetchers::Fetcher::Jnc; @@ -7,16 +6,15 @@ use crate::lemmy::{PostInfo}; pub mod jnovel; -#[async_trait] pub(crate) trait FetcherTrait { fn new() -> Self where Self: Sized; - async fn check_feed(&self) -> Result, ()>; + fn check_feed(&self) -> Result, ()>; } impl Fetcher { - pub(crate) async fn check_feed(&self) -> Result, ()> { + pub(crate) fn check_feed(&self) -> Result, ()> { match self { - Jnc(fetcher) => fetcher.check_feed().await, + Jnc(fetcher) => fetcher.check_feed(), /*default => { let err_msg = format!("Fetcher {default} is not implemented"); error!(err_msg); diff --git a/src/lemmy.rs b/src/lemmy.rs index 2686778..6146945 100644 --- a/src/lemmy.rs +++ b/src/lemmy.rs @@ -1,5 +1,5 @@ use std::cmp::Ordering; -use crate::config::{Config, PostBody, PostConfig, SeriesConfig}; +use crate::config::{PostConfig, SeriesConfig}; use crate::{HTTP_CLIENT}; use lemmy_api_common::community::{ListCommunities, ListCommunitiesResponse}; use lemmy_api_common::lemmy_db_views::structs::PostView; @@ -9,28 +9,10 @@ use lemmy_db_schema::newtypes::{CommunityId, LanguageId, PostId}; use lemmy_db_schema::{ListingType, PostFeatureType}; use reqwest::StatusCode; use std::collections::HashMap; -use std::sync::{RwLock}; use lemmy_db_schema::sensitive::SensitiveString; use serde::{Deserialize, Serialize}; -use systemd_journal_logger::connected_to_journal; - -macro_rules! debug { - ($msg:tt) => { - match connected_to_journal() { - true => log::debug!("[DEBUG] {}", $msg), - false => println!("[DEBUG] {}", $msg), - } - }; -} - -macro_rules! error { - ($msg:tt) => { - match connected_to_journal() { - true => log::error!("[ERROR] {}", $msg), - false => eprintln!("[ERROR] {}", $msg), - } - }; -} +use crate::logging::Logging; +use crate::settings::{BotSettings, PostBody}; pub(crate) struct Lemmy { jwt_token: SensitiveString, @@ -107,7 +89,7 @@ impl PartialOrd for PartInfo { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub(crate) enum PostType { Chapter, Volume @@ -207,26 +189,24 @@ impl PartialOrd for PostInfo { impl Lemmy { pub(crate) fn get_community_id(&self, name: &str) -> CommunityId { - *self.communities.get(name).expect("Given community is invalid") + *self.communities.get(name).expect(format!("Community '{name}' is invalid").as_str()) } - pub(crate) async fn new(config: &RwLock) -> Result { - let read_config = config.read().expect("Read Lock Failed").clone(); + pub(crate) fn new(config: &BotSettings) -> Result { let login_params = Login { - username_or_email: read_config.get_username(), - password: read_config.get_password(), + username_or_email: config.username(), + password: config.password(), totp_2fa_token: None, }; let response = match HTTP_CLIENT - .post(read_config.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) => { let err_msg = format!("{e}"); - error!(err_msg); + Logging::error(err_msg.as_str()); return Err(()); } }; @@ -235,40 +215,41 @@ impl Lemmy { StatusCode::OK => { 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: read_config.instance.to_owned(), + instance: config.instance().to_owned(), communities: HashMap::new(), }), None => { - let err_msg = "Login did not return JWT token. Are the credentials valid?".to_owned(); - error!(err_msg); + Logging::error("Login did not return JWT token. Are the credentials valid?"); Err(()) } } } status => { let err_msg = format!("Unexpected HTTP Status '{}' during Login", status); - error!(err_msg); + Logging::error(err_msg.as_str()); Err(()) } } } - pub(crate) async fn logout(&self) { - let _ = self.post_data_json("/api/v3/user/logout", &"").await; + pub fn logout(&self) { + let _ = self.post_data_json("/api/v3/user/logout", &""); } + pub fn login(&self) { + + } - pub(crate) async fn post(&self, post: CreatePost) -> Option { - let response: String = match self.post_data_json("/api/v3/post", &post).await { + pub fn post(&self, post: CreatePost) -> Option { + let response: String = match self.post_data_json("/api/v3/post", &post) { Some(data) => data, None => return None, }; - let json_data: PostView = match self.parse_json_map(&response).await { + let json_data: PostView = match self.parse_json_map(&response) { Some(data) => data, None => return None, }; @@ -276,12 +257,12 @@ impl Lemmy { Some(json_data.post.id) } - async fn feature(&self, params: FeaturePost) -> Option { - let response: String = match self.post_data_json("/api/v3/post/feature", ¶ms).await { + fn feature(&self, params: FeaturePost) -> Option { + let response: String = match self.post_data_json("/api/v3/post/feature", ¶ms) { Some(data) => data, None => return None, }; - let json_data: PostView = match self.parse_json_map(&response).await { + let json_data: PostView = match self.parse_json_map(&response) { Some(data) => data, None => return None, }; @@ -289,36 +270,36 @@ impl Lemmy { Some(json_data) } - pub(crate) async fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Option { + pub(crate) fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Option { let pin_params = FeaturePost { post_id, featured: false, feature_type: location, }; - self.feature(pin_params).await + self.feature(pin_params) } - pub(crate) async fn pin(&self, post_id: PostId, location: PostFeatureType) -> Option { + pub(crate) fn pin(&self, post_id: PostId, location: PostFeatureType) -> Option { let pin_params = FeaturePost { post_id, featured: true, feature_type: location, }; - self.feature(pin_params).await + self.feature(pin_params) } - pub(crate) async fn get_community_pinned(&self, community: CommunityId) -> Option> { + pub(crate) fn get_community_pinned(&self, community: CommunityId) -> Option> { let list_params = GetPosts { community_id: Some(community), type_: Some(ListingType::Local), ..Default::default() }; - let response: String = match self.get_data_query("/api/v3/post/list", &list_params).await { + let response: String = match self.get_data_query("/api/v3/post/list", &list_params) { Some(data) => data, None => return None, }; - let json_data: GetPostsResponse = match self.parse_json(&response).await { + let json_data: GetPostsResponse = match self.parse_json(&response) { Some(data) => data, None => return None, }; @@ -331,17 +312,17 @@ impl Lemmy { .collect()) } - pub(crate) async fn get_local_pinned(&self) -> Option> { + pub(crate) fn get_local_pinned(&self) -> Option> { let list_params = GetPosts { type_: Some(ListingType::Local), ..Default::default() }; - let response: String = match self.get_data_query("/api/v3/post/list", &list_params).await { + let response: String = match self.get_data_query("/api/v3/post/list", &list_params) { Some(data) => data, None => return None, }; - let json_data: GetPostsResponse = match self.parse_json(&response).await { + let json_data: GetPostsResponse = match self.parse_json(&response) { Some(data) => data, None => return None, }; @@ -354,17 +335,17 @@ impl Lemmy { .collect()) } - pub(crate) async fn get_communities(&mut self) { + pub(crate) fn get_communities(&mut self) { let list_params = ListCommunities { type_: Some(ListingType::Local), ..Default::default() }; - let response: String = match self.get_data_query("/api/v3/community/list", &list_params).await { + let response: String = match self.get_data_query("/api/v3/community/list", &list_params) { Some(data) => data, None => return, }; - let json_data: ListCommunitiesResponse = match self.parse_json::(&response).await { + let json_data: ListCommunitiesResponse = match self.parse_json::(&response) { Some(data) => data, None => return, }; @@ -378,71 +359,71 @@ impl Lemmy { self.communities = communities; } - async fn post_data_json(&self, route: &str, json: &T ) -> Option { + fn post_data_json(&self, route: &str, json: &T ) -> Option { let res = HTTP_CLIENT .post(format!("{}{route}", &self.instance)) .bearer_auth(&self.jwt_token.to_string()) .json(&json) - .send() - .await; - self.extract_data(res).await + .send(); + self.extract_data(res) } - async fn get_data_query(&self, route: &str, param: &T ) -> Option { + fn get_data_query(&self, route: &str, param: &T ) -> Option { let res = HTTP_CLIENT .get(format!("{}{route}", &self.instance)) .bearer_auth(&self.jwt_token.to_string()) .query(¶m) - .send() - .await; - self.extract_data(res).await + .send(); + self.extract_data(res) } - async fn extract_data(&self, response: Result) -> Option { + fn extract_data(&self, response: Result) -> Option { match response { Ok(data) => { + let msg = format!("Status Code: '{}'", data.status().clone()); + Logging::debug(msg.as_str()); if data.status().is_success() { - match data.text().await { + match data.text() { Ok(data) => Some(data), Err(e) => { let err_msg = format!("{e}"); - error!(err_msg); + Logging::error(err_msg.as_str()); None } } } else { - let err_msg = format!("HTTP Request failed: {}", data.text().await.unwrap()); - error!(err_msg); + let err_msg = format!("HTTP Request failed: {}", data.text().unwrap()); + Logging::error(err_msg.as_str()); None } }, Err(e) => { let err_msg = format!("{e}"); - error!(err_msg); + Logging::error(err_msg.as_str()); None } } } - async fn parse_json<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option { + fn parse_json<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option { match serde_json::from_str::(response) { Ok(data) => Some(data), Err(e) => { let err_msg = format!("while parsing JSON: {e} "); - error!(err_msg); + Logging::error(err_msg.as_str()); None } } } - async fn parse_json_map<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option { - debug!(response); + fn parse_json_map<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option { + Logging::debug(response); match serde_json::from_str::>(response) { Ok(mut data) => Some(data.remove("post_view").expect("Element should be present")), Err(e) => { let err_msg = format!("while parsing JSON HashMap: {e}"); - error!(err_msg); + Logging::error(err_msg.as_str()); None } } diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..181c78c --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,80 @@ +use std::collections::VecDeque; +use chrono::{DateTime, Utc}; +use parking_lot::Mutex; +use systemd_journal_logger::connected_to_journal; + +fn mem_log(msg: Option) -> VecDeque { + static MEM_LOG: Mutex> = Mutex::new(VecDeque::new()); + + if let Some(msg) = msg { + let now = Utc::now(); + let log_event = LogEvent::new(now, msg); + let mut lock = MEM_LOG.lock(); + lock.push_back(log_event); + while lock.len() > 10 { + lock.pop_front(); + } + } + + return MEM_LOG.lock().clone(); +} + +pub struct Logging { +} + +impl Logging { + pub fn debug(msg: &str) { + let msg = format!("[DEBUG] {msg}"); + match connected_to_journal() { + true => log::debug!("{msg}"), + false => println!("{msg}"), + } + mem_log(Some(msg)); + } + + pub fn info(msg: &str) { + let msg = format!("[INFO] {msg}"); + match connected_to_journal() { + true => log::info!("{msg}"), + false => println!("{msg}"), + } + mem_log(Some(msg)); + } + + pub fn warn(msg: &str) { + let msg = format!("[WARN] {msg}"); + match connected_to_journal() { + true => log::warn!("{msg}"), + false => println!("{msg}"), + } + mem_log(Some(msg)); + } + + pub fn error(msg: &str) { + let msg = format!("[ERROR] {msg}"); + match connected_to_journal() { + true => log::error!("{msg}"), + false => eprintln!("{msg}"), + } + mem_log(Some(msg)); + } + + pub fn get_mem_log() -> VecDeque { + mem_log(None) + } +} + +#[derive(Clone)] +pub(crate) struct LogEvent { + pub date: DateTime, + pub text: String, +} + +impl LogEvent { + pub fn new(time: DateTime, message: String) -> Self { + Self { + date: time, + text: message + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index cc0e9cd..f387202 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,26 @@ +use std::env; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread::{sleep, spawn}; +use atomic_enum::atomic_enum; use chrono::{Duration}; use log::{LevelFilter}; use once_cell::sync::Lazy; -use reqwest::Client; +use parking_lot::{RwLock}; +use reqwest::blocking::Client; use systemd_journal_logger::{JournalLog}; use crate::bot::Bot; +use crate::HealthSignal::RequestAlive; +use crate::logging::Logging; +use crate::settings::ApplicationSettings; mod bot; mod config; mod lemmy; mod post_history; mod fetchers; +mod logging; +mod settings; pub static HTTP_CLIENT: Lazy = Lazy::new(|| { Client::builder() @@ -19,13 +30,13 @@ pub static HTTP_CLIENT: Lazy = Lazy::new(|| { .expect("build client") }); -#[tokio::main] -async fn main() { +fn main() { JournalLog::new() .expect("Systemd-Logger crate error") .install() .expect("Systemd-Logger crate error"); - match std::env::var("LOG_LEVEL") { + + match env::var("LOG_LEVEL") { Ok(level) => { match level.as_str() { "debug" => log::set_max_level(LevelFilter::Debug), @@ -36,6 +47,70 @@ async fn main() { _ => log::set_max_level(LevelFilter::Info), } - let mut bot = Bot::new(); - bot.run().await; + let mut settings = Arc::new(RwLock::new(ApplicationSettings::new())); + + env::set_var("USING_DB", settings.read().is_settings_source_database().to_string()); + if settings.read().is_settings_source_database() { + Logging::info("Database Usage enabled"); + } + else { + Logging::info("Operating in Single Bot Mode") + } + + let mut health_signals: Vec> = vec![]; + let mut update_pending: Arc = Arc::new(AtomicBool::new(false)); + + let series_settings = settings.read().series_settings().clone(); + settings.read().bot_settings().read().iter().for_each(|(id, bot_settings)| { + let health_signal = Arc::new(AtomicHealthSignal::new(HealthSignal::RequestAlive)); + health_signals.push(health_signal.clone()); + + let bot_settings = bot_settings.clone(); + let series_settings = series_settings.clone(); + let update_pending = update_pending.clone(); + spawn(move || { + let mut bot = Bot::new(health_signal, bot_settings); + bot.run(series_settings, update_pending); + }); + }); + + loop { + update_pending.store(true, Ordering::SeqCst); + settings.write().update(); + Logging::info("Configuration Updated"); + update_pending.store(false, Ordering::SeqCst); + + health_signals.iter().for_each(|signal| { + if signal.load(Ordering::SeqCst) == RequestAlive { + // at least one thread died, close all other threads + Logging::warn("A Bot died, restarting application."); + health_signals.iter().for_each(|signal| { + signal.store(HealthSignal::RequestStop, Ordering::SeqCst); + }); + + loop { + if health_signals.iter().all(|signal| { + signal.load(Ordering::SeqCst) == HealthSignal::ConfirmStop + }) { + Logging::info("All Threads successfully stopped"); + break; + } + else { + sleep(Duration::milliseconds(100).to_std().unwrap()); + } + } + } + }); + sleep(Duration::seconds(10).to_std().unwrap()); + } } + + +#[atomic_enum] +#[derive(PartialEq)] +enum HealthSignal { + RequestAlive, + ConfirmAlive, + RequestStop, + ConfirmStop, +} \ No newline at end of file diff --git a/src/settings/database/mod.rs b/src/settings/database/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/settings/mod.rs b/src/settings/mod.rs new file mode 100644 index 0000000..b88358a --- /dev/null +++ b/src/settings/mod.rs @@ -0,0 +1,126 @@ +use std::any::{Any, TypeId}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use chrono::{DateTime, Utc}; +use lemmy_db_schema::sensitive::SensitiveString; +use parking_lot::RwLock; +use serde_derive::{Deserialize, Serialize}; +use crate::fetchers::Fetcher; +use crate::lemmy::PostType; +use crate::settings::toml::database::DatabaseTomlSettings; +use crate::settings::toml::TomlSettings; + +pub mod database; +pub mod toml; + +pub trait SettingsSource { + fn update(&mut self); + fn bot_settings(&self) -> HashMap; + fn series_settings(&self) -> HashMap; +} + +pub struct ApplicationSettings { + settings_source: Box, + bot_settings: Arc>>, + series_settings: Arc>>, +} + +impl ApplicationSettings { + pub fn new() -> Self { + let toml_settings = TomlSettings::load(); + let settings_source: Box; + + if let Some(database) = toml_settings.database { + settings_source = Box::new(database) as Box; + } + else if let Some(single_bot) = toml_settings.single_bot { + settings_source = Box::new(single_bot) as Box; + } + else { + panic!("Invalid TOML Configuration: No valid Settings Source provided!"); + } + + let bot_settings = Arc::new(RwLock::new(settings_source.bot_settings())); + let series_settings = Arc::new(RwLock::new(settings_source.series_settings())); + ApplicationSettings { + settings_source, + bot_settings, + series_settings, + } + } + + pub fn is_settings_source_database(&self) -> bool { + self.settings_source.type_id() == TypeId::of::() + } + + pub fn update(&mut self) { + self.settings_source.update(); + *self.bot_settings.write() = self.settings_source.bot_settings(); + *self.series_settings.write() = self.settings_source.series_settings(); + } + + pub fn bot_settings(&self) -> Arc>> { + self.bot_settings.clone() + } + + pub fn series_settings(&self) -> Arc>> { + self.series_settings.clone() + } +} + +#[derive(Clone)] +pub struct BotSettings { + id: u16, + series_ids: Vec, + instance: String, + username: SensitiveString, + password: SensitiveString, + status_post_url: Option, +} +impl BotSettings { + pub fn instance(&self) -> String { + self.instance.clone() + } + pub fn username(&self) -> SensitiveString { + self.username.clone() + } + pub fn password(&self) -> SensitiveString { + self.password.clone() + } + + pub fn status_post_url(&self) -> Option { + self.status_post_url.clone() + } +} + +pub struct SeriesSettings { + id: u16, + post_settings: HashMap, + fetcher: Fetcher, + fetch_interval: Duration, + last_fetched: DateTime, +} + +impl SeriesSettings { + pub fn check(&self) { + + } +} + +pub struct PostSettings { + r#type: PostType, + community: String, + body_type: PostBody, + pin_community: bool, + pin_instance: bool +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(tag = "body_type", content = "body_content")] +pub(crate) enum PostBody { + None, + Description, + Custom(String), +} + diff --git a/src/settings/toml/bot.rs b/src/settings/toml/bot.rs new file mode 100644 index 0000000..75b06bf --- /dev/null +++ b/src/settings/toml/bot.rs @@ -0,0 +1,83 @@ +use std::collections::HashMap; +use std::time::Duration; +use chrono::Utc; +use lemmy_db_schema::sensitive::SensitiveString; +use serde_derive::{Deserialize, Serialize}; +use crate::config::SeriesConfig; +use crate::fetchers::{Fetcher, FetcherTrait}; +use crate::fetchers::jnovel::JNovelFetcher; +use crate::lemmy::PostType; +use crate::settings::{BotSettings, PostSettings, SeriesSettings, SettingsSource}; +use crate::settings::toml::TomlSettings; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SingleBotTomlSettings { + instance: String, + username: SensitiveString, + password: SensitiveString, + status_post_url: Option, + pub protected_communities: Vec, + series: Vec, + fetch_interval: usize, +} + +impl SettingsSource for SingleBotTomlSettings { + fn update(&mut self) { + let toml_settings = TomlSettings::load(); + + if let Some(single_bot) = toml_settings.single_bot { + *self = single_bot; + } + else { + panic!("No valid 'single_bot' configuration found. Hot-switching between database and single_bot mode is not currently supported."); + } + } + + fn bot_settings(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert(0, BotSettings { + id: 0, + series_ids: vec![0], + instance: self.instance.clone(), + username: self.username.clone(), + password: self.password.clone(), + status_post_url: self.status_post_url.clone(), + }); + map + } + + fn series_settings(&self) -> HashMap { + let mut map = HashMap::new(); + self.series.iter().enumerate().for_each(|(id, series)| { + let mut fetcher = JNovelFetcher::new(); + fetcher.set_series(series.slug.clone()); + fetcher.set_part_option(series.parted); + + let mut post_settings = HashMap::new(); + post_settings.insert(PostType::Chapter, PostSettings { + r#type: PostType::Chapter, + community: series.prepub_community.name.clone(), + body_type: series.prepub_community.post_body.clone(), + pin_community: series.prepub_community.pin_settings.pin_new_post_community, + pin_instance: series.prepub_community.pin_settings.pin_new_post_local, + }); + + post_settings.insert(PostType::Volume, PostSettings { + r#type: PostType::Volume, + community: series.volume_community.name.clone(), + body_type: series.volume_community.post_body.clone(), + pin_community: series.volume_community.pin_settings.pin_new_post_community, + pin_instance: series.volume_community.pin_settings.pin_new_post_local, + }); + + map.insert(id as u16, SeriesSettings { + id: id as u16, + post_settings, + fetcher: Fetcher::Jnc(fetcher), + fetch_interval: Duration::from_secs(self.fetch_interval as u64), + last_fetched: Utc::now() - Duration::from_secs(self.fetch_interval as u64), + }); + }); + map + } +} \ No newline at end of file diff --git a/src/settings/toml/database.rs b/src/settings/toml/database.rs new file mode 100644 index 0000000..14efe96 --- /dev/null +++ b/src/settings/toml/database.rs @@ -0,0 +1,22 @@ +use std::collections::HashMap; +use serde_derive::{Deserialize, Serialize}; +use crate::settings::{BotSettings, SeriesSettings, SettingsSource}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct DatabaseTomlSettings { + +} + +impl SettingsSource for DatabaseTomlSettings { + fn update(&mut self) { + todo!() + } + + fn bot_settings(&self) -> HashMap { + todo!() + } + + fn series_settings(&self) -> HashMap { + todo!() + } +} \ No newline at end of file diff --git a/src/settings/toml/mod.rs b/src/settings/toml/mod.rs new file mode 100644 index 0000000..4ea34d4 --- /dev/null +++ b/src/settings/toml/mod.rs @@ -0,0 +1,37 @@ +pub mod database; +pub mod bot; + +use std::time::Duration; +use lemmy_db_schema::sensitive::SensitiveString; +use serde_derive::{Deserialize, Serialize}; +use crate::config::SeriesConfig; +use crate::settings::BotSettings; +use crate::settings::toml::bot::SingleBotTomlSettings; +use crate::settings::toml::database::DatabaseTomlSettings; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TomlSettings { + pub database: Option, + pub single_bot: Option, +} + +impl TomlSettings { + pub(crate) fn load() -> Self { + let mut cfg: Self = match confy::load(env!("CARGO_PKG_NAME"), "config") { + Ok(data) => data, + Err(e) => panic!("config.toml not found: {e}"), + }; + + cfg + } +} + +impl Default for TomlSettings { + fn default() -> Self { + TomlSettings { + database: None, + single_bot: None, + } + } +} +