Compare commits

..

4 commits
bot-v4 ... main

Author SHA1 Message Date
041590a559
Release 3.2.0
All checks were successful
Run Tests on Code / run-tests (push) Successful in 35s
Build and Release Binary File / test (push) Successful in 35s
Build and Release Binary File / build (push) Successful in 39s
Build and Release Binary File / upload-generic-package (push) Successful in 1s
Build and Release Binary File / upload-debian-package (push) Successful in 1s
Build and Release Binary File / upload-release (push) Successful in 6s
2024-10-22 16:15:19 +02:00
d6f883f890
Bump API version
All checks were successful
Run Tests on Code / run-tests (push) Successful in 35s
2024-10-22 16:14:46 +02:00
b07420e0bd Merge pull request 'Bump forgejo-release to v2' (#26) from actions-update into main
All checks were successful
Run Tests on Code / run-tests (push) Successful in 19s
Reviewed-on: https://forgejo.neshweb.net///Neshura/aob-lemmy-bot/pulls/26
2024-08-06 12:23:26 +00:00
0fc71f0a7d Bump forgejo-release to v2
All checks were successful
Run Tests on Code / run-tests (push) Successful in 1m7s
Build binary file and bundle packages / test (pull_request) Successful in 1m6s
Build binary file and bundle packages / build (pull_request) Successful in 1m10s
2024-08-06 12:19:37 +00:00
16 changed files with 332 additions and 709 deletions

View file

@ -137,7 +137,7 @@ jobs:
run: rm release_blobs/build.env run: rm release_blobs/build.env
- -
name: Release New Version name: Release New Version
uses: actions/forgejo-release@v1 uses: actions/forgejo-release@v2
with: with:
direction: upload direction: upload
url: https://forgejo.neshweb.net url: https://forgejo.neshweb.net

41
Cargo.lock generated
View file

@ -52,9 +52,9 @@ dependencies = [
[[package]] [[package]]
name = "aob-lemmy-bot" name = "aob-lemmy-bot"
version = "3.1.0" version = "3.2.0"
dependencies = [ dependencies = [
"atomic_enum", "async-trait",
"chrono", "chrono",
"confy", "confy",
"lemmy_api_common", "lemmy_api_common",
@ -62,13 +62,13 @@ dependencies = [
"log", "log",
"notify", "notify",
"once_cell", "once_cell",
"parking_lot",
"reqwest", "reqwest",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"strum_macros", "strum_macros",
"systemd-journal-logger", "systemd-journal-logger",
"tokio",
"toml", "toml",
"url", "url",
] ]
@ -95,17 +95,6 @@ dependencies = [
"syn", "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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -456,9 +445,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.30" version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@ -1210,9 +1199,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.86" version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1608,9 +1597,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.71" version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1758,9 +1747,21 @@ dependencies = [
"num_cpus", "num_cpus",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio-macros",
"windows-sys 0.48.0", "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]] [[package]]
name = "tokio-native-tls" name = "tokio-native-tls"
version = "0.3.1" version = "0.3.1"

View file

@ -1,7 +1,7 @@
[package] [package]
authors = ["Neshura"] authors = ["Neshura"]
name = "aob-lemmy-bot" name = "aob-lemmy-bot"
version = "3.1.0" version = "3.2.0"
edition = "2021" edition = "2021"
description = "Bot for automatically posting new chapters of 'Ascendance of a Bookworm' released by J-Novel Club" description = "Bot for automatically posting new chapters of 'Ascendance of a Bookworm' released by J-Novel Club"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
@ -25,11 +25,11 @@ serde = "^1.0"
serde_derive = "^1.0" serde_derive = "^1.0"
serde_json = "^1.0" serde_json = "^1.0"
strum_macros = "^0.26" strum_macros = "^0.26"
tokio = { version = "^1.37", features = ["rt", "rt-multi-thread", "macros"] }
url = "^2.5" url = "^2.5"
confy = "^0.6" confy = "^0.6"
toml = "^0.8" toml = "^0.8"
systemd-journal-logger = "^2.1.1" systemd-journal-logger = "^2.1.1"
log = "^0.4" log = "^0.4"
notify = "6.1.1" async-trait = "^0.1"
parking_lot = "0.12.3" notify = "6.1.1"
atomic_enum = "0.3.0"

View file

@ -6,6 +6,7 @@ instance = "https://lemmy.example.org"
username = "BotUserName" username = "BotUserName"
password = "BotPassword" password = "BotPassword"
status_post_url = "PostUrlForStatusMonitoring" status_post_url = "PostUrlForStatusMonitoring"
config_reload_seconds = 10800
protected_communities = [ "community_where_pins_should_stay" ] protected_communities = [ "community_where_pins_should_stay" ]

View file

@ -1,134 +1,136 @@
use std::collections::HashMap; use crate::{config::{Config}, HTTP_CLIENT};
use crate::{AtomicHealthSignal, HealthSignal, HTTP_CLIENT};
use crate::lemmy::{Lemmy}; use crate::lemmy::{Lemmy};
use crate::post_history::{SeriesHistory}; use crate::post_history::{SeriesHistory};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use std::sync::{Arc}; use std::sync::{Arc, RwLock};
use std::sync::atomic::{AtomicBool, Ordering}; use notify::{Event, EventKind, event::{AccessKind, AccessMode}, RecursiveMode, Watcher};
use std::thread::sleep; use tokio::time::sleep;
use parking_lot::RwLock; use systemd_journal_logger::connected_to_journal;
use crate::logging::Logging;
use crate::settings::{BotSettings, SeriesSettings}; 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),
}
};
}
pub(crate) struct Bot { pub(crate) struct Bot {
settings: BotSettings, shared_config: Arc<RwLock<Config>>,
settings_id: u16,
health_signal: Arc<AtomicHealthSignal>,
history: SeriesHistory, history: SeriesHistory,
login_time: Option<DateTime<Utc>>, run_start_time: DateTime<Utc>
} }
enum Wait { enum Wait {
Absolute, Absolute,
Buffer(DateTime<Utc>) Buffer
} }
impl Bot { impl Bot {
pub(crate) fn new(health_signal: Arc<AtomicHealthSignal>, settings_id: u16, settings: BotSettings) -> Self { pub(crate) fn new() -> Self {
let config = Config::load();
let shared_config: Arc<RwLock<Config>> = Arc::new(RwLock::new(config));
let shared_config_copy = shared_config.clone();
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
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);
}
}
}).expect("Watcher Error");
watcher.watch(&Config::get_path(), RecursiveMode::NonRecursive).expect("Error in watcher");
let history: SeriesHistory = SeriesHistory::load_history(); let history: SeriesHistory = SeriesHistory::load_history();
Bot { shared_config, history, run_start_time: Utc::now() }
Bot { health_signal, settings_id, settings, history, login_time: None }
} }
pub(crate) fn run( pub(crate) async fn run(&mut self) {
&mut self,
series_settings: Arc<RwLock<HashMap<u16, SeriesSettings>>>,
bot_settings: Arc<RwLock<HashMap<u16, BotSettings>>>,
update_pending: Arc<AtomicBool>
) {
let mut last_ping = Utc::now();
if let Some(interval) = self.settings.status_post_interval() {
last_ping -= interval
}
loop { loop {
match self.health_signal.load(Ordering::SeqCst) { let mut lemmy = match Lemmy::new(&self.shared_config).await {
HealthSignal::RequestAlive => { Ok(data) => data,
self.health_signal.store(HealthSignal::ConfirmAlive, Ordering::SeqCst); Err(_) => continue,
} };
HealthSignal::RequestStop => {
break;
}
_ => {}
}
if update_pending.load(Ordering::SeqCst) { lemmy.get_communities().await;
Logging::debug("Awaiting Config Update");
sleep(Duration::milliseconds(100).to_std().unwrap());
continue
}
self.settings = bot_settings.read().get(&self.settings_id).unwrap().clone();
// perform status ping
if let Some(interval) = self.settings.status_post_interval() {
if Utc::now() + Duration::seconds(1) >= last_ping + interval {
if self.ping_status() {
last_ping = Utc::now();
}
else {
continue
}
}
}
if let Some(login_time) = self.login_time {
if Utc::now() - login_time >= Duration::minutes(60) {
//self.logout();
continue
}
}
else {
//self.login();
}
self.history = SeriesHistory::load_history(); self.history = SeriesHistory::load_history();
series_settings.read().iter().for_each(|(id, series)| { let start: DateTime<Utc> = Utc::now();
//let new_entries = series.check(); while Utc::now() - start <= Duration::minutes(60) {
}); self.run_start_time = Utc::now();
for series in series_settings.read().iter() { self.ping_status().await;
//series.update(&mut self.history, &lemmy, &self.settings).await; let read_copy = self.shared_config.read().expect("Read Lock Failed").clone();
Logging::debug("Done Updating Series"); for series in read_copy.series {
self.wait(1, Wait::Absolute); 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;
} }
lemmy.logout().await;
} }
self.health_signal.store(HealthSignal::ConfirmStop, Ordering::SeqCst);
} }
fn ping_status(&self) -> bool { async fn ping_status(&self) {
if let Some(status_url) = &self.settings.status_post_url() { let read_config = &self.shared_config.read().expect("Read Lock Failed").clone();
return match HTTP_CLIENT.get(status_url).send() { if let Some(status_url) = &read_config.status_post_url {
Ok(_) => { match HTTP_CLIENT.get(status_url).send().await {
true Ok(_) => {},
},
Err(e) => { Err(e) => {
let err_msg = format!("While pinging status URL: {e}"); let err_msg = format!("While pinging status URL: {e}");
Logging::error(err_msg.as_str()); error!(err_msg);
false
} }
} }
} }
false
} }
fn wait(&self, seconds: i64, start_time: Wait) { async fn wait(&self, seconds: i64, start_time: Wait) {
let duration: Duration = Duration::seconds(seconds); let duration: Duration = Duration::seconds(seconds);
let start_time: DateTime<Utc> = match start_time { let start_time: DateTime<Utc> = match start_time {
Wait::Absolute => Utc::now(), Wait::Absolute => Utc::now(),
Wait::Buffer(time) => time, Wait::Buffer => self.run_start_time,
}; };
while Utc::now() - start_time < duration { while Utc::now() - start_time < duration {
sleep(Duration::milliseconds(100).to_std().unwrap()); sleep(Duration::milliseconds(100).to_std().unwrap()).await
} }
} }
fn login(&mut self) {
todo!();
self.login_time = Some(Utc::now());
}
fn logout(&mut self) {
self.login_time = None;
todo!();
}
} }

View file

@ -1,47 +1,88 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use chrono::{Timelike, Utc}; use chrono::{Timelike, Utc};
use crate::settings::PostBody::Description; use crate::config::PostBody::Description;
use lemmy_db_schema::PostFeatureType; use lemmy_db_schema::PostFeatureType;
use lemmy_db_schema::sensitive::SensitiveString; use lemmy_db_schema::sensitive::SensitiveString;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use crate::lemmy::{Lemmy, PartInfo, PostType}; use crate::lemmy::{Lemmy, PartInfo, PostType};
use crate::post_history::{SeriesHistory}; use crate::post_history::{SeriesHistory};
use systemd_journal_logger::connected_to_journal;
use crate::fetchers::{FetcherTrait, Fetcher}; use crate::fetchers::{FetcherTrait, Fetcher};
use crate::fetchers::jnovel::{JNovelFetcher}; use crate::fetchers::jnovel::{JNovelFetcher};
use crate::logging::Logging;
use crate::settings::PostBody; 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),
}
};
}
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub(crate) struct SingleBotConfig { pub(crate) struct Config {
pub(crate) instance: String, pub(crate) instance: String,
username: SensitiveString, username: SensitiveString,
password: SensitiveString, password: SensitiveString,
pub(crate) status_post_url: Option<String>, pub(crate) status_post_url: Option<String>,
pub(crate) config_reload_seconds: u32,
pub(crate) protected_communities: Vec<String>, pub(crate) protected_communities: Vec<String>,
pub(crate) series: Vec<SeriesConfig>, pub(crate) series: Vec<SeriesConfig>,
} }
impl SingleBotConfig { impl Config {
pub(crate) fn load(&self) -> Self { pub(crate) fn load() -> Self {
if self.instance.is_empty() { 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() {
panic!("bot instance not set!") panic!("bot instance not set!")
} }
if self.username.is_empty() { if cfg.username.is_empty() {
panic!("bot username not set!") panic!("bot username not set!")
} }
if self.password.is_empty() { if cfg.password.is_empty() {
panic!("bot password not provided!") panic!("bot password not provided!")
} }
self.series.iter().for_each(|series| { cfg.series.iter().for_each(|series| {
if series.prepub_community.post_body == Description { if series.prepub_community.post_body == Description {
panic!("'Description' type Post Body only supported for Volumes!") panic!("'Description' type Post Body only supported for Volumes!")
} }
}); });
self.clone() cfg
} }
pub(crate) fn get_path() -> PathBuf { pub(crate) fn get_path() -> PathBuf {
@ -57,13 +98,14 @@ impl SingleBotConfig {
} }
} }
impl Default for SingleBotConfig { impl Default for Config {
fn default() -> Self { fn default() -> Self {
SingleBotConfig { Config {
instance: "".to_owned(), instance: "".to_owned(),
username: SensitiveString::from("".to_owned()), username: SensitiveString::from("".to_owned()),
password: SensitiveString::from("".to_owned()), password: SensitiveString::from("".to_owned()),
status_post_url: None, status_post_url: None,
config_reload_seconds: 21600,
protected_communities: vec![], protected_communities: vec![],
series: vec![], series: vec![],
} }
@ -80,9 +122,9 @@ pub(crate) struct SeriesConfig {
} }
impl SeriesConfig { impl SeriesConfig {
pub(crate) fn update(&self, history: &mut SeriesHistory, lemmy: &Lemmy, config: &Arc<RwLock<SingleBotConfig>>) { pub(crate) async fn update(&self, history: &mut SeriesHistory, lemmy: &Lemmy, config: &Arc<RwLock<Config>>) {
let info_msg = format!("Checking {} for Updates", self.slug); let info_msg = format!("Checking {} for Updates", self.slug);
Logging::info(info_msg.as_str()); info!(info_msg);
let mut fetcher: Fetcher = match &self.fetcher { let mut fetcher: Fetcher = match &self.fetcher {
Fetcher::Jnc(_) => { Fetcher::Jnc(_) => {
@ -102,18 +144,18 @@ impl SeriesConfig {
} }
} }
let post_list = match fetcher.check_feed() { let post_list = match fetcher.check_feed().await {
Ok(data) => data, Ok(data) => data,
Err(_) => { Err(_) => {
let err_msg = format!("While checking feed for {}", self.slug); let err_msg = format!("While checking feed for {}", self.slug);
Logging::error(err_msg.as_str()); error!(err_msg);
return; return;
} }
}; };
if post_list.is_empty() && Utc::now().minute() % 10 == 0 { if post_list.is_empty() && Utc::now().minute() % 10 == 0 {
let info_msg = "No Updates found"; let info_msg = "No Updates found";
Logging::info(info_msg); info!(info_msg);
} }
for post_info in post_list.iter() { for post_info in post_list.iter() {
@ -132,12 +174,12 @@ impl SeriesConfig {
post_info.get_info().title.as_str(), post_info.get_info().title.as_str(),
post_info.get_post_config(self).name.as_str() post_info.get_post_config(self).name.as_str()
); );
Logging::info(info.as_str()); info!(info);
let post_id = match lemmy.post(post_data) { let post_id = match lemmy.post(post_data).await {
Some(data) => data, Some(data) => data,
None=> { None=> {
Logging::error("Error posting chapter"); error!("Error posting chapter");
return; return;
} }
}; };
@ -154,19 +196,19 @@ impl SeriesConfig {
post_info.get_info().title, post_info.get_info().title,
post_info.get_post_config(self).name.as_str() post_info.get_post_config(self).name.as_str()
); );
Logging::info(info.as_str()); info!(info);
let pinned_posts = lemmy.get_community_pinned(lemmy.get_community_id(&post_info.get_post_config(self).name)).unwrap_or_else(|| { let pinned_posts = lemmy.get_community_pinned(lemmy.get_community_id(&post_info.get_post_config(self).name)).await.unwrap_or_else(|| {
Logging::error("Pinning of Post to community failed"); error!("Pinning of Post to community failed");
vec![] vec![]
}); });
if !pinned_posts.is_empty() { if !pinned_posts.is_empty() {
let community_pinned_post = &pinned_posts[0]; let community_pinned_post = &pinned_posts[0];
if lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Community).is_none() { if lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Community).await.is_none() {
Logging::error("Error un-pinning post"); error!("Error un-pinning post");
} }
} }
if lemmy.pin(post_id, PostFeatureType::Community).is_none() { if lemmy.pin(post_id, PostFeatureType::Community).await.is_none() {
Logging::error("Error pinning post"); error!("Error pinning post");
} }
} else if read_config } else if read_config
.protected_communities .protected_communities
@ -176,16 +218,16 @@ impl SeriesConfig {
"Community '{}' for Series '{}' is protected. Is this intended?", "Community '{}' for Series '{}' is protected. Is this intended?",
&post_info.get_post_config(self).name, self.slug &post_info.get_post_config(self).name, self.slug
); );
Logging::warn(message.as_str()); warn!(message);
} }
if post_info.get_post_config(self).pin_settings.pin_new_post_local { if post_info.get_post_config(self).pin_settings.pin_new_post_local {
let info = format!("Pinning '{}' to Instance", post_info.get_info().title); let info = format!("Pinning '{}' to Instance", post_info.get_info().title);
Logging::info(info.as_str()); info!(info);
let pinned_posts = match lemmy.get_local_pinned() { let pinned_posts = match lemmy.get_local_pinned().await {
Some(data) => {data} Some(data) => {data}
None => { None => {
Logging::error("Error fetching pinned posts"); error!("Error fetching pinned posts");
vec![] vec![]
} }
}; };
@ -199,16 +241,16 @@ impl SeriesConfig {
continue; continue;
} else { } else {
let community_pinned_post = &pinned_post; let community_pinned_post = &pinned_post;
if lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Local).is_none() { if lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Local).await.is_none() {
Logging::error("Error pinning post"); error!("Error pinning post");
continue; continue;
} }
break; break;
} }
} }
} }
if lemmy.pin(post_id, PostFeatureType::Local).is_none() { if lemmy.pin(post_id, PostFeatureType::Local).await.is_none() {
Logging::error("Error pinning post"); error!("Error pinning post");
}; };
} }
@ -228,7 +270,7 @@ impl SeriesConfig {
series_history.set_part(post_info.get_part_info().unwrap_or(PartInfo::NoParts).as_string().as_str(), part_history); series_history.set_part(post_info.get_part_info().unwrap_or(PartInfo::NoParts).as_string().as_str(), part_history);
history history
.set_series(self.slug.as_str(), series_history); .set_series(self.slug.as_str(), series_history);
Logging::debug("Saving History"); debug!("Saving History");
history.save_history(); history.save_history();
} }
} }
@ -246,3 +288,11 @@ pub(crate) struct PinConfig {
pub(crate) pin_new_post_local: bool, pub(crate) pin_new_post_local: bool,
pub(crate) pin_new_post_community: 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),
}

View file

@ -3,16 +3,35 @@ use chrono::{DateTime, Duration, Utc};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Sub; use std::ops::Sub;
use async_trait::async_trait;
use crate::fetchers::{FetcherTrait}; use crate::fetchers::{FetcherTrait};
use crate::lemmy::{PartInfo, PostInfo, PostInfoInner, PostType}; use crate::lemmy::{PartInfo, PostInfo, PostInfoInner, PostType};
use systemd_journal_logger::connected_to_journal;
use crate::lemmy::PartInfo::{NoParts, Part}; use crate::lemmy::PartInfo::{NoParts, Part};
use crate::logging::Logging;
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),
}
};
}
static PAST_DAYS_ELIGIBLE: u8 = 4; static PAST_DAYS_ELIGIBLE: u8 = 4;
macro_rules! api_url { macro_rules! api_url {
() => { () => {
"https://labs.j-novel.club/app/v1".to_owned() "https://labs.j-novel.club/app/v2".to_owned()
}; };
} }
@ -94,6 +113,7 @@ impl JNovelFetcher {
} }
} }
#[async_trait]
impl FetcherTrait for JNovelFetcher { impl FetcherTrait for JNovelFetcher {
fn new() -> Self { fn new() -> Self {
JNovelFetcher { JNovelFetcher {
@ -102,22 +122,23 @@ impl FetcherTrait for JNovelFetcher {
} }
} }
fn check_feed(&self) -> Result<Vec<PostInfo>, ()> { async fn check_feed(&self) -> Result<Vec<PostInfo>, ()> {
let response = match HTTP_CLIENT let response = match HTTP_CLIENT
.get(api_url!() + "/series/" + self.series_slug.as_str() + "/volumes?format=json") .get(api_url!() + "/series/" + self.series_slug.as_str() + "/volumes?format=json")
.send() .send()
.await
{ {
Ok(data) => match data.text() { Ok(data) => match data.text().await {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
let err_msg = format!("While checking feed: {e}"); let err_msg = format!("While checking feed: {e}");
Logging::error(err_msg.as_str()); error!(err_msg);
return Err(()); return Err(());
} }
}, },
Err(e) => { Err(e) => {
let err_msg = format!("{e}"); let err_msg = format!("{e}");
Logging::error(err_msg.as_str()); error!(err_msg);
return Err(()); return Err(());
} }
}; };
@ -126,7 +147,7 @@ impl FetcherTrait for JNovelFetcher {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
let err_msg = format!("{e}"); let err_msg = format!("{e}");
Logging::error(err_msg.as_str()); error!(err_msg);
return Err(()); return Err(());
} }
}; };
@ -163,7 +184,7 @@ impl FetcherTrait for JNovelFetcher {
match part_number { match part_number {
Some(number) => new_part_info = Part(number), Some(number) => new_part_info = Part(number),
None => { None => {
Logging::info("No Part found, assuming 1"); info!("No Part found, assuming 1");
new_part_info = Part(1); new_part_info = Part(1);
} }
} }
@ -203,7 +224,7 @@ impl FetcherTrait for JNovelFetcher {
.or_insert(new_post_info); .or_insert(new_post_info);
} }
if let Some(prepub_info) = get_latest_prepub(&volume.slug) { if let Some(prepub_info) = get_latest_prepub(&volume.slug).await {
let prepub_post_info = PostInfo { let prepub_post_info = PostInfo {
post_type: Some(PostType::Chapter), post_type: Some(PostType::Chapter),
part: Some(new_part_info), part: Some(new_part_info),
@ -231,22 +252,23 @@ impl FetcherTrait for JNovelFetcher {
} }
fn get_latest_prepub(volume_slug: &str) -> Option<PostInfoInner> { async fn get_latest_prepub(volume_slug: &str) -> Option<PostInfoInner> {
let response = match HTTP_CLIENT let response = match HTTP_CLIENT
.get(api_url!() + "/volumes/" + volume_slug + "/parts?format=json") .get(api_url!() + "/volumes/" + volume_slug + "/parts?format=json")
.send() .send()
.await
{ {
Ok(data) => match data.text() { Ok(data) => match data.text().await {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
let err_msg = format!("While getting latest PrePub: {e}"); let err_msg = format!("While getting latest PrePub: {e}");
Logging::error(err_msg.as_str()); error!(err_msg);
return None; return None;
} }
}, },
Err(e) => { Err(e) => {
let err_msg = format!("{e}"); let err_msg = format!("{e}");
Logging::error(err_msg.as_str()); error!(err_msg);
return None; return None;
} }
}; };
@ -255,7 +277,7 @@ fn get_latest_prepub(volume_slug: &str) -> Option<PostInfoInner> {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
let err_msg = format!("{e}"); let err_msg = format!("{e}");
Logging::error(err_msg.as_str()); error!(err_msg);
return None; return None;
} }
}; };

View file

@ -1,3 +1,4 @@
use async_trait::async_trait;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use strum_macros::Display; use strum_macros::Display;
use crate::fetchers::Fetcher::Jnc; use crate::fetchers::Fetcher::Jnc;
@ -6,15 +7,16 @@ use crate::lemmy::{PostInfo};
pub mod jnovel; pub mod jnovel;
#[async_trait]
pub(crate) trait FetcherTrait { pub(crate) trait FetcherTrait {
fn new() -> Self where Self: Sized; fn new() -> Self where Self: Sized;
fn check_feed(&self) -> Result<Vec<PostInfo>, ()>; async fn check_feed(&self) -> Result<Vec<PostInfo>, ()>;
} }
impl Fetcher { impl Fetcher {
pub(crate) fn check_feed(&self) -> Result<Vec<PostInfo>, ()> { pub(crate) async fn check_feed(&self) -> Result<Vec<PostInfo>, ()> {
match self { match self {
Jnc(fetcher) => fetcher.check_feed(), Jnc(fetcher) => fetcher.check_feed().await,
/*default => { /*default => {
let err_msg = format!("Fetcher {default} is not implemented"); let err_msg = format!("Fetcher {default} is not implemented");
error!(err_msg); error!(err_msg);

View file

@ -1,5 +1,5 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use crate::config::{PostConfig, SeriesConfig}; use crate::config::{Config, PostBody, PostConfig, SeriesConfig};
use crate::{HTTP_CLIENT}; use crate::{HTTP_CLIENT};
use lemmy_api_common::community::{ListCommunities, ListCommunitiesResponse}; use lemmy_api_common::community::{ListCommunities, ListCommunitiesResponse};
use lemmy_api_common::lemmy_db_views::structs::PostView; use lemmy_api_common::lemmy_db_views::structs::PostView;
@ -9,10 +9,28 @@ use lemmy_db_schema::newtypes::{CommunityId, LanguageId, PostId};
use lemmy_db_schema::{ListingType, PostFeatureType}; use lemmy_db_schema::{ListingType, PostFeatureType};
use reqwest::StatusCode; use reqwest::StatusCode;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{RwLock};
use lemmy_db_schema::sensitive::SensitiveString; use lemmy_db_schema::sensitive::SensitiveString;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::logging::Logging; use systemd_journal_logger::connected_to_journal;
use crate::settings::{BotSettings, PostBody};
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),
}
};
}
pub(crate) struct Lemmy { pub(crate) struct Lemmy {
jwt_token: SensitiveString, jwt_token: SensitiveString,
@ -89,7 +107,7 @@ impl PartialOrd for PartInfo {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy)]
pub(crate) enum PostType { pub(crate) enum PostType {
Chapter, Chapter,
Volume Volume
@ -189,24 +207,26 @@ impl PartialOrd for PostInfo {
impl Lemmy { impl Lemmy {
pub(crate) fn get_community_id(&self, name: &str) -> CommunityId { pub(crate) fn get_community_id(&self, name: &str) -> CommunityId {
*self.communities.get(name).expect(format!("Community '{name}' is invalid").as_str()) *self.communities.get(name).expect("Given community is invalid")
} }
pub(crate) fn new(config: &BotSettings) -> Result<Self, ()> { pub(crate) async fn new(config: &RwLock<Config>) -> Result<Self, ()> {
let read_config = config.read().expect("Read Lock Failed").clone();
let login_params = Login { let login_params = Login {
username_or_email: config.username(), username_or_email: read_config.get_username(),
password: config.password(), password: read_config.get_password(),
totp_2fa_token: None, totp_2fa_token: None,
}; };
let response = match HTTP_CLIENT let response = match HTTP_CLIENT
.post(config.instance().to_owned() + "/api/v3/user/login") .post(read_config.instance.to_owned() + "/api/v3/user/login")
.json(&login_params) .json(&login_params)
.send() .send()
.await
{ {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
let err_msg = format!("{e}"); let err_msg = format!("{e}");
Logging::error(err_msg.as_str()); error!(err_msg);
return Err(()); return Err(());
} }
}; };
@ -215,41 +235,40 @@ impl Lemmy {
StatusCode::OK => { StatusCode::OK => {
let data: LoginResponse = response let data: LoginResponse = response
.json() .json()
.await
.expect("Successful Login Request should return JSON"); .expect("Successful Login Request should return JSON");
match data.jwt { match data.jwt {
Some(token) => Ok(Lemmy { Some(token) => Ok(Lemmy {
jwt_token: token.clone(), jwt_token: token.clone(),
instance: config.instance().to_owned(), instance: read_config.instance.to_owned(),
communities: HashMap::new(), communities: HashMap::new(),
}), }),
None => { None => {
Logging::error("Login did not return JWT token. Are the credentials valid?"); let err_msg = "Login did not return JWT token. Are the credentials valid?".to_owned();
error!(err_msg);
Err(()) Err(())
} }
} }
} }
status => { status => {
let err_msg = format!("Unexpected HTTP Status '{}' during Login", status); let err_msg = format!("Unexpected HTTP Status '{}' during Login", status);
Logging::error(err_msg.as_str()); error!(err_msg);
Err(()) Err(())
} }
} }
} }
pub fn logout(&self) { pub(crate) async fn logout(&self) {
let _ = self.post_data_json("/api/v3/user/logout", &""); let _ = self.post_data_json("/api/v3/user/logout", &"").await;
} }
pub fn login(&self) {
}
pub fn post(&self, post: CreatePost) -> Option<PostId> { pub(crate) async fn post(&self, post: CreatePost) -> Option<PostId> {
let response: String = match self.post_data_json("/api/v3/post", &post) { let response: String = match self.post_data_json("/api/v3/post", &post).await {
Some(data) => data, Some(data) => data,
None => return None, None => return None,
}; };
let json_data: PostView = match self.parse_json_map(&response) { let json_data: PostView = match self.parse_json_map(&response).await {
Some(data) => data, Some(data) => data,
None => return None, None => return None,
}; };
@ -257,12 +276,12 @@ impl Lemmy {
Some(json_data.post.id) Some(json_data.post.id)
} }
fn feature(&self, params: FeaturePost) -> Option<PostView> { async fn feature(&self, params: FeaturePost) -> Option<PostView> {
let response: String = match self.post_data_json("/api/v3/post/feature", &params) { let response: String = match self.post_data_json("/api/v3/post/feature", &params).await {
Some(data) => data, Some(data) => data,
None => return None, None => return None,
}; };
let json_data: PostView = match self.parse_json_map(&response) { let json_data: PostView = match self.parse_json_map(&response).await {
Some(data) => data, Some(data) => data,
None => return None, None => return None,
}; };
@ -270,36 +289,36 @@ impl Lemmy {
Some(json_data) Some(json_data)
} }
pub(crate) fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Option<PostView> { pub(crate) async fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Option<PostView> {
let pin_params = FeaturePost { let pin_params = FeaturePost {
post_id, post_id,
featured: false, featured: false,
feature_type: location, feature_type: location,
}; };
self.feature(pin_params) self.feature(pin_params).await
} }
pub(crate) fn pin(&self, post_id: PostId, location: PostFeatureType) -> Option<PostView> { pub(crate) async fn pin(&self, post_id: PostId, location: PostFeatureType) -> Option<PostView> {
let pin_params = FeaturePost { let pin_params = FeaturePost {
post_id, post_id,
featured: true, featured: true,
feature_type: location, feature_type: location,
}; };
self.feature(pin_params) self.feature(pin_params).await
} }
pub(crate) fn get_community_pinned(&self, community: CommunityId) -> Option<Vec<PostView>> { pub(crate) async fn get_community_pinned(&self, community: CommunityId) -> Option<Vec<PostView>> {
let list_params = GetPosts { let list_params = GetPosts {
community_id: Some(community), community_id: Some(community),
type_: Some(ListingType::Local), type_: Some(ListingType::Local),
..Default::default() ..Default::default()
}; };
let response: String = match self.get_data_query("/api/v3/post/list", &list_params) { let response: String = match self.get_data_query("/api/v3/post/list", &list_params).await {
Some(data) => data, Some(data) => data,
None => return None, None => return None,
}; };
let json_data: GetPostsResponse = match self.parse_json(&response) { let json_data: GetPostsResponse = match self.parse_json(&response).await {
Some(data) => data, Some(data) => data,
None => return None, None => return None,
}; };
@ -312,17 +331,17 @@ impl Lemmy {
.collect()) .collect())
} }
pub(crate) fn get_local_pinned(&self) -> Option<Vec<PostView>> { pub(crate) async fn get_local_pinned(&self) -> Option<Vec<PostView>> {
let list_params = GetPosts { let list_params = GetPosts {
type_: Some(ListingType::Local), type_: Some(ListingType::Local),
..Default::default() ..Default::default()
}; };
let response: String = match self.get_data_query("/api/v3/post/list", &list_params) { let response: String = match self.get_data_query("/api/v3/post/list", &list_params).await {
Some(data) => data, Some(data) => data,
None => return None, None => return None,
}; };
let json_data: GetPostsResponse = match self.parse_json(&response) { let json_data: GetPostsResponse = match self.parse_json(&response).await {
Some(data) => data, Some(data) => data,
None => return None, None => return None,
}; };
@ -335,17 +354,17 @@ impl Lemmy {
.collect()) .collect())
} }
pub(crate) fn get_communities(&mut self) { pub(crate) async fn get_communities(&mut self) {
let list_params = ListCommunities { let list_params = ListCommunities {
type_: Some(ListingType::Local), type_: Some(ListingType::Local),
..Default::default() ..Default::default()
}; };
let response: String = match self.get_data_query("/api/v3/community/list", &list_params) { let response: String = match self.get_data_query("/api/v3/community/list", &list_params).await {
Some(data) => data, Some(data) => data,
None => return, None => return,
}; };
let json_data: ListCommunitiesResponse = match self.parse_json::<ListCommunitiesResponse>(&response) { let json_data: ListCommunitiesResponse = match self.parse_json::<ListCommunitiesResponse>(&response).await {
Some(data) => data, Some(data) => data,
None => return, None => return,
}; };
@ -359,71 +378,71 @@ impl Lemmy {
self.communities = communities; self.communities = communities;
} }
fn post_data_json<T: Serialize>(&self, route: &str, json: &T ) -> Option<String> { async fn post_data_json<T: Serialize>(&self, route: &str, json: &T ) -> Option<String> {
let res = HTTP_CLIENT let res = HTTP_CLIENT
.post(format!("{}{route}", &self.instance)) .post(format!("{}{route}", &self.instance))
.bearer_auth(&self.jwt_token.to_string()) .bearer_auth(&self.jwt_token.to_string())
.json(&json) .json(&json)
.send(); .send()
self.extract_data(res) .await;
self.extract_data(res).await
} }
fn get_data_query<T: Serialize>(&self, route: &str, param: &T ) -> Option<String> { async fn get_data_query<T: Serialize>(&self, route: &str, param: &T ) -> Option<String> {
let res = HTTP_CLIENT let res = HTTP_CLIENT
.get(format!("{}{route}", &self.instance)) .get(format!("{}{route}", &self.instance))
.bearer_auth(&self.jwt_token.to_string()) .bearer_auth(&self.jwt_token.to_string())
.query(&param) .query(&param)
.send(); .send()
self.extract_data(res) .await;
self.extract_data(res).await
} }
fn extract_data(&self, response: Result<reqwest::blocking::Response, reqwest::Error>) -> Option<String> { async fn extract_data(&self, response: Result<reqwest::Response, reqwest::Error>) -> Option<String> {
match response { match response {
Ok(data) => { Ok(data) => {
let msg = format!("Status Code: '{}'", data.status().clone());
Logging::debug(msg.as_str());
if data.status().is_success() { if data.status().is_success() {
match data.text() { match data.text().await {
Ok(data) => Some(data), Ok(data) => Some(data),
Err(e) => { Err(e) => {
let err_msg = format!("{e}"); let err_msg = format!("{e}");
Logging::error(err_msg.as_str()); error!(err_msg);
None None
} }
} }
} }
else { else {
let err_msg = format!("HTTP Request failed: {}", data.text().unwrap()); let err_msg = format!("HTTP Request failed: {}", data.text().await.unwrap());
Logging::error(err_msg.as_str()); error!(err_msg);
None None
} }
}, },
Err(e) => { Err(e) => {
let err_msg = format!("{e}"); let err_msg = format!("{e}");
Logging::error(err_msg.as_str()); error!(err_msg);
None None
} }
} }
} }
fn parse_json<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option<T> { async fn parse_json<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option<T> {
match serde_json::from_str::<T>(response) { match serde_json::from_str::<T>(response) {
Ok(data) => Some(data), Ok(data) => Some(data),
Err(e) => { Err(e) => {
let err_msg = format!("while parsing JSON: {e} "); let err_msg = format!("while parsing JSON: {e} ");
Logging::error(err_msg.as_str()); error!(err_msg);
None None
} }
} }
} }
fn parse_json_map<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option<T> { async fn parse_json_map<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option<T> {
Logging::debug(response); debug!(response);
match serde_json::from_str::<HashMap<&str, T>>(response) { match serde_json::from_str::<HashMap<&str, T>>(response) {
Ok(mut data) => Some(data.remove("post_view").expect("Element should be present")), Ok(mut data) => Some(data.remove("post_view").expect("Element should be present")),
Err(e) => { Err(e) => {
let err_msg = format!("while parsing JSON HashMap: {e}"); let err_msg = format!("while parsing JSON HashMap: {e}");
Logging::error(err_msg.as_str()); error!(err_msg);
None None
} }
} }

View file

@ -1,110 +0,0 @@
use std::collections::VecDeque;
use chrono::{DateTime, Utc};
use log::Level;
use parking_lot::Mutex;
use systemd_journal_logger::connected_to_journal;
fn mem_log(level: Level, msg: Option<String>) -> VecDeque<LogEvent> {
static MEM_LOG_DEBUG: Mutex<VecDeque<LogEvent>> = Mutex::new(VecDeque::new());
static MEM_LOG_INFO: Mutex<VecDeque<LogEvent>> = Mutex::new(VecDeque::new());
static MEM_LOG_WARN: Mutex<VecDeque<LogEvent>> = Mutex::new(VecDeque::new());
static MEM_LOG_ERROR: Mutex<VecDeque<LogEvent>> = Mutex::new(VecDeque::new());
let max_len: i8;
let mut list = match level {
Level::Debug => {
max_len = 10;
MEM_LOG_DEBUG.lock()
},
Level::Info => {
max_len = 20;
MEM_LOG_INFO.lock()
},
Level::Warn => {
max_len = 40;
MEM_LOG_WARN.lock()
},
Level::Error => {
max_len = -1;
MEM_LOG_ERROR.lock()
},
Level::Trace => {
max_len = 10;
MEM_LOG_DEBUG.lock()
}
};
if let Some(msg) = msg {
let now = Utc::now();
let log_event = LogEvent::new(now, msg);
list.push_back(log_event);
if max_len != -1 {
while list.len() > max_len as usize {
list.pop_front();
}
}
}
list.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(Level::Debug, 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(Level::Info, 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(Level::Warn, 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(Level::Error, Some(msg));
}
pub fn get_mem_log(level: Level) -> VecDeque<LogEvent> {
mem_log(level, None)
}
}
#[derive(Clone)]
pub(crate) struct LogEvent {
pub date: DateTime<Utc>,
pub text: String,
}
impl LogEvent {
pub fn new(time: DateTime<Utc>, message: String) -> Self {
Self {
date: time,
text: message
}
}
}

View file

@ -1,26 +1,15 @@
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 chrono::{Duration};
use log::{LevelFilter}; use log::{LevelFilter};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use parking_lot::{RwLock}; use reqwest::Client;
use reqwest::blocking::Client;
use systemd_journal_logger::{JournalLog}; use systemd_journal_logger::{JournalLog};
use crate::bot::Bot; use crate::bot::Bot;
use crate::HealthSignal::RequestAlive;
use crate::logging::Logging;
use crate::settings::ApplicationSettings;
mod bot; mod bot;
mod config; mod config;
mod lemmy; mod lemmy;
mod post_history; mod post_history;
mod fetchers; mod fetchers;
mod logging;
mod settings;
pub static HTTP_CLIENT: Lazy<Client> = Lazy::new(|| { pub static HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
Client::builder() Client::builder()
@ -30,13 +19,13 @@ pub static HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
.expect("build client") .expect("build client")
}); });
fn main() { #[tokio::main]
async fn main() {
JournalLog::new() JournalLog::new()
.expect("Systemd-Logger crate error") .expect("Systemd-Logger crate error")
.install() .install()
.expect("Systemd-Logger crate error"); .expect("Systemd-Logger crate error");
match std::env::var("LOG_LEVEL") {
match env::var("LOG_LEVEL") {
Ok(level) => { Ok(level) => {
match level.as_str() { match level.as_str() {
"debug" => log::set_max_level(LevelFilter::Debug), "debug" => log::set_max_level(LevelFilter::Debug),
@ -47,71 +36,6 @@ fn main() {
_ => log::set_max_level(LevelFilter::Info), _ => log::set_max_level(LevelFilter::Info),
} }
let mut settings = Arc::new(RwLock::new(ApplicationSettings::new())); let mut bot = Bot::new();
bot.run().await;
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<Arc<AtomicHealthSignal>> = vec![];
let mut update_pending: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
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 bot_id = *id;
let series_settings = settings.read().series_settings().clone();
let bot_settings_map = settings.read().bot_settings().clone();
let update_pending = update_pending.clone();
spawn(move || {
let mut bot = Bot::new(health_signal, bot_id, bot_settings);
bot.run(series_settings, bot_settings_map, 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,
}

View file

@ -1,142 +0,0 @@
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<u16, BotSettings>;
fn series_settings(&self) -> HashMap<u16, SeriesSettings>;
}
pub struct ApplicationSettings {
settings_source: Box<dyn SettingsSource>,
bot_settings: Arc<RwLock<HashMap<u16, BotSettings>>>,
series_settings: Arc<RwLock<HashMap<u16, SeriesSettings>>>,
}
impl ApplicationSettings {
pub fn new() -> Self {
let toml_settings = TomlSettings::load();
let settings_source: Box<dyn SettingsSource>;
if let Some(database) = toml_settings.database {
settings_source = Box::new(database) as Box<dyn SettingsSource>;
}
else if let Some(single_bot) = toml_settings.single_bot {
settings_source = Box::new(single_bot) as Box<dyn SettingsSource>;
}
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::<DatabaseTomlSettings>()
}
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<RwLock<HashMap<u16, BotSettings>>> {
self.bot_settings.clone()
}
pub fn series_settings(&self) -> Arc<RwLock<HashMap<u16, SeriesSettings>>> {
self.series_settings.clone()
}
}
#[derive(Clone)]
pub struct StatusPostSettings {
url: String,
interval: Duration,
}
#[derive(Clone)]
pub struct BotSettings {
id: u16,
series_ids: Vec<u16>,
instance: String,
username: SensitiveString,
password: SensitiveString,
status_post_settings: Option<StatusPostSettings>,
}
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<String> {
match &self.status_post_settings {
Some(settings) => Some(settings.url.clone()),
None => None
}
}
pub fn status_post_interval(&self) -> Option<Duration> {
match &self.status_post_settings {
Some(settings) => Some(settings.interval),
None => None
}
}
}
pub struct SeriesSettings {
id: u16,
post_settings: HashMap<PostType, PostSettings>,
fetcher: Fetcher,
fetch_interval: Duration,
last_fetched: DateTime<Utc>,
}
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),
}

View file

@ -1,87 +0,0 @@
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, StatusPostSettings};
use crate::settings::toml::TomlSettings;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SingleBotTomlSettings {
instance: String,
username: SensitiveString,
password: SensitiveString,
status_post_url: Option<String>,
status_post_interval: Option<u32>,
pub protected_communities: Vec<String>,
series: Vec<SeriesConfig>,
fetch_interval: u32,
}
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<u16, BotSettings> {
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_settings: Some(StatusPostSettings {
url: self.status_post_url.clone().unwrap(),
interval: Duration::from_secs(self.status_post_interval.clone().unwrap() as u64)
}),
});
map
}
fn series_settings(&self) -> HashMap<u16, SeriesSettings> {
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
}
}

View file

@ -1,22 +0,0 @@
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<u16, BotSettings> {
todo!()
}
fn series_settings(&self) -> HashMap<u16, SeriesSettings> {
todo!()
}
}

View file

@ -1,37 +0,0 @@
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<DatabaseTomlSettings>,
pub single_bot: Option<SingleBotTomlSettings>,
}
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,
}
}
}