(Re-)introduce Threading, Add support for different Config (Settings) Sources, Remove Async leftovers
All checks were successful
Run Tests on Code / run-tests (push) Successful in 40s
All checks were successful
Run Tests on Code / run-tests (push) Successful in 40s
This commit is contained in:
parent
2ae6468ad8
commit
5976bd59a7
15 changed files with 634 additions and 329 deletions
39
Cargo.lock
generated
39
Cargo.lock
generated
|
@ -54,7 +54,7 @@ dependencies = [
|
||||||
name = "aob-lemmy-bot"
|
name = "aob-lemmy-bot"
|
||||||
version = "3.1.0"
|
version = "3.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"atomic_enum",
|
||||||
"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,6 +95,17 @@ 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"
|
||||||
|
@ -445,9 +456,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.29"
|
version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
|
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
|
@ -1199,9 +1210,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.76"
|
version = "1.0.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
|
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
@ -1597,9 +1608,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.48"
|
version = "2.0.71"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1747,21 +1758,9 @@ 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"
|
||||||
|
|
|
@ -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"
|
||||||
async-trait = "^0.1"
|
|
||||||
notify = "6.1.1"
|
notify = "6.1.1"
|
||||||
|
parking_lot = "0.12.3"
|
||||||
|
atomic_enum = "0.3.0"
|
|
@ -6,7 +6,6 @@ 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" ]
|
||||||
|
|
||||||
|
|
167
src/bot.rs
167
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::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, RwLock};
|
use std::sync::{Arc};
|
||||||
use notify::{Event, EventKind, event::{AccessKind, AccessMode}, RecursiveMode, Watcher};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use tokio::time::sleep;
|
use std::thread::sleep;
|
||||||
use systemd_journal_logger::connected_to_journal;
|
use parking_lot::RwLock;
|
||||||
|
use crate::logging::Logging;
|
||||||
macro_rules! debug {
|
use crate::settings::{BotSettings, SeriesSettings};
|
||||||
($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 {
|
||||||
shared_config: Arc<RwLock<Config>>,
|
settings: BotSettings,
|
||||||
|
health_signal: Arc<AtomicHealthSignal>,
|
||||||
history: SeriesHistory,
|
history: SeriesHistory,
|
||||||
run_start_time: DateTime<Utc>
|
login_time: Option<DateTime<Utc>>
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Wait {
|
enum Wait {
|
||||||
Absolute,
|
Absolute,
|
||||||
Buffer
|
Buffer(DateTime<Utc>)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bot {
|
impl Bot {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new(health_signal: Arc<AtomicHealthSignal>, settings: BotSettings) -> 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, history, login_time: None }
|
||||||
}
|
}
|
||||||
pub(crate) async fn run(&mut self) {
|
pub(crate) fn run(&mut self, series_settings: Arc<RwLock<HashMap<u16, SeriesSettings>>>, update_pending: Arc<AtomicBool>) {
|
||||||
loop {
|
loop {
|
||||||
let mut lemmy = match Lemmy::new(&self.shared_config).await {
|
match self.health_signal.load(Ordering::SeqCst) {
|
||||||
Ok(data) => data,
|
HealthSignal::RequestAlive => {
|
||||||
Err(_) => continue,
|
//self.health_signal.store(HealthSignal::ConfirmAlive, Ordering::SeqCst);
|
||||||
};
|
}
|
||||||
|
HealthSignal::RequestStop => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
lemmy.get_communities().await;
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.login();
|
||||||
|
}
|
||||||
|
|
||||||
self.history = SeriesHistory::load_history();
|
self.history = SeriesHistory::load_history();
|
||||||
|
|
||||||
let start: DateTime<Utc> = Utc::now();
|
let run_start_time = Utc::now();
|
||||||
while Utc::now() - start <= Duration::minutes(60) {
|
self.ping_status();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
lemmy.logout().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);
|
||||||
|
}
|
||||||
|
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) {
|
fn ping_status(&self) {
|
||||||
let read_config = &self.shared_config.read().expect("Read Lock Failed").clone();
|
if let Some(status_url) = &self.settings.status_post_url() {
|
||||||
if let Some(status_url) = &read_config.status_post_url {
|
match HTTP_CLIENT.get(status_url).send() {
|
||||||
match HTTP_CLIENT.get(status_url).send().await {
|
|
||||||
Ok(_) => {},
|
Ok(_) => {},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let err_msg = format!("While pinging status URL: {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 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 => self.run_start_time,
|
Wait::Buffer(time) => time,
|
||||||
};
|
};
|
||||||
while Utc::now() - start_time < duration {
|
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!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
124
src/config.rs
124
src/config.rs
|
@ -1,88 +1,47 @@
|
||||||
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::config::PostBody::Description;
|
use crate::settings::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;
|
||||||
macro_rules! debug {
|
use crate::settings::PostBody;
|
||||||
($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 Config {
|
pub(crate) struct SingleBotConfig {
|
||||||
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 Config {
|
impl SingleBotConfig {
|
||||||
pub(crate) fn load() -> Self {
|
pub(crate) fn load(&self) -> Self {
|
||||||
let cfg: Self = match confy::load(env!("CARGO_PKG_NAME"), "config") {
|
if self.instance.is_empty() {
|
||||||
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 cfg.username.is_empty() {
|
if self.username.is_empty() {
|
||||||
panic!("bot username not set!")
|
panic!("bot username not set!")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.password.is_empty() {
|
if self.password.is_empty() {
|
||||||
panic!("bot password not provided!")
|
panic!("bot password not provided!")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.series.iter().for_each(|series| {
|
self.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!")
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cfg
|
self.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_path() -> PathBuf {
|
pub(crate) fn get_path() -> PathBuf {
|
||||||
|
@ -98,14 +57,13 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for SingleBotConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Config {
|
SingleBotConfig {
|
||||||
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![],
|
||||||
}
|
}
|
||||||
|
@ -122,9 +80,9 @@ pub(crate) struct SeriesConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SeriesConfig {
|
impl SeriesConfig {
|
||||||
pub(crate) async fn update(&self, history: &mut SeriesHistory, lemmy: &Lemmy, config: &Arc<RwLock<Config>>) {
|
pub(crate) fn update(&self, history: &mut SeriesHistory, lemmy: &Lemmy, config: &Arc<RwLock<SingleBotConfig>>) {
|
||||||
let info_msg = format!("Checking {} for Updates", self.slug);
|
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 {
|
let mut fetcher: Fetcher = match &self.fetcher {
|
||||||
Fetcher::Jnc(_) => {
|
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,
|
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);
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
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";
|
||||||
info!(info_msg);
|
Logging::info(info_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
for post_info in post_list.iter() {
|
for post_info in post_list.iter() {
|
||||||
|
@ -174,12 +132,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()
|
||||||
);
|
);
|
||||||
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,
|
Some(data) => data,
|
||||||
None=> {
|
None=> {
|
||||||
error!("Error posting chapter");
|
Logging::error("Error posting chapter");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -196,19 +154,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()
|
||||||
);
|
);
|
||||||
info!(info);
|
Logging::info(info.as_str());
|
||||||
let pinned_posts = lemmy.get_community_pinned(lemmy.get_community_id(&post_info.get_post_config(self).name)).await.unwrap_or_else(|| {
|
let pinned_posts = lemmy.get_community_pinned(lemmy.get_community_id(&post_info.get_post_config(self).name)).unwrap_or_else(|| {
|
||||||
error!("Pinning of Post to community failed");
|
Logging::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).await.is_none() {
|
if lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Community).is_none() {
|
||||||
error!("Error un-pinning post");
|
Logging::error("Error un-pinning post");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lemmy.pin(post_id, PostFeatureType::Community).await.is_none() {
|
if lemmy.pin(post_id, PostFeatureType::Community).is_none() {
|
||||||
error!("Error pinning post");
|
Logging::error("Error pinning post");
|
||||||
}
|
}
|
||||||
} else if read_config
|
} else if read_config
|
||||||
.protected_communities
|
.protected_communities
|
||||||
|
@ -218,16 +176,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
|
||||||
);
|
);
|
||||||
warn!(message);
|
Logging::warn(message.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
info!(info);
|
Logging::info(info.as_str());
|
||||||
let pinned_posts = match lemmy.get_local_pinned().await {
|
let pinned_posts = match lemmy.get_local_pinned() {
|
||||||
Some(data) => {data}
|
Some(data) => {data}
|
||||||
None => {
|
None => {
|
||||||
error!("Error fetching pinned posts");
|
Logging::error("Error fetching pinned posts");
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -241,16 +199,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).await.is_none() {
|
if lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Local).is_none() {
|
||||||
error!("Error pinning post");
|
Logging::error("Error pinning post");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lemmy.pin(post_id, PostFeatureType::Local).await.is_none() {
|
if lemmy.pin(post_id, PostFeatureType::Local).is_none() {
|
||||||
error!("Error pinning post");
|
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);
|
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);
|
||||||
debug!("Saving History");
|
Logging::debug("Saving History");
|
||||||
history.save_history();
|
history.save_history();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -288,11 +246,3 @@ 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),
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,29 +3,10 @@ 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;
|
||||||
|
|
||||||
|
@ -113,7 +94,6 @@ impl JNovelFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl FetcherTrait for JNovelFetcher {
|
impl FetcherTrait for JNovelFetcher {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
JNovelFetcher {
|
JNovelFetcher {
|
||||||
|
@ -122,23 +102,22 @@ impl FetcherTrait for JNovelFetcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_feed(&self) -> Result<Vec<PostInfo>, ()> {
|
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().await {
|
Ok(data) => match data.text() {
|
||||||
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}");
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let err_msg = format!("{e}");
|
let err_msg = format!("{e}");
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -147,7 +126,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}");
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -184,7 +163,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 => {
|
||||||
info!("No Part found, assuming 1");
|
Logging::info("No Part found, assuming 1");
|
||||||
new_part_info = Part(1);
|
new_part_info = Part(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,7 +203,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).await {
|
if let Some(prepub_info) = get_latest_prepub(&volume.slug) {
|
||||||
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),
|
||||||
|
@ -252,23 +231,22 @@ impl FetcherTrait for JNovelFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn get_latest_prepub(volume_slug: &str) -> Option<PostInfoInner> {
|
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().await {
|
Ok(data) => match data.text() {
|
||||||
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}");
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let err_msg = format!("{e}");
|
let err_msg = format!("{e}");
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -277,7 +255,7 @@ async 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}");
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
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;
|
||||||
|
@ -7,16 +6,15 @@ 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;
|
||||||
async fn check_feed(&self) -> Result<Vec<PostInfo>, ()>;
|
fn check_feed(&self) -> Result<Vec<PostInfo>, ()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fetcher {
|
impl Fetcher {
|
||||||
pub(crate) async fn check_feed(&self) -> Result<Vec<PostInfo>, ()> {
|
pub(crate) fn check_feed(&self) -> Result<Vec<PostInfo>, ()> {
|
||||||
match self {
|
match self {
|
||||||
Jnc(fetcher) => fetcher.check_feed().await,
|
Jnc(fetcher) => fetcher.check_feed(),
|
||||||
/*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);
|
||||||
|
|
131
src/lemmy.rs
131
src/lemmy.rs
|
@ -1,5 +1,5 @@
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use crate::config::{Config, PostBody, PostConfig, SeriesConfig};
|
use crate::config::{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,28 +9,10 @@ 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 systemd_journal_logger::connected_to_journal;
|
use crate::logging::Logging;
|
||||||
|
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,
|
||||||
|
@ -107,7 +89,7 @@ impl PartialOrd for PartInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub(crate) enum PostType {
|
pub(crate) enum PostType {
|
||||||
Chapter,
|
Chapter,
|
||||||
Volume
|
Volume
|
||||||
|
@ -207,26 +189,24 @@ 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("Given community is invalid")
|
*self.communities.get(name).expect(format!("Community '{name}' is invalid").as_str())
|
||||||
}
|
}
|
||||||
pub(crate) async fn new(config: &RwLock<Config>) -> Result<Self, ()> {
|
pub(crate) fn new(config: &BotSettings) -> Result<Self, ()> {
|
||||||
let read_config = config.read().expect("Read Lock Failed").clone();
|
|
||||||
let login_params = Login {
|
let login_params = Login {
|
||||||
username_or_email: read_config.get_username(),
|
username_or_email: config.username(),
|
||||||
password: read_config.get_password(),
|
password: config.password(),
|
||||||
totp_2fa_token: None,
|
totp_2fa_token: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = match HTTP_CLIENT
|
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)
|
.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}");
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -235,40 +215,41 @@ 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: read_config.instance.to_owned(),
|
instance: config.instance().to_owned(),
|
||||||
communities: HashMap::new(),
|
communities: HashMap::new(),
|
||||||
}),
|
}),
|
||||||
None => {
|
None => {
|
||||||
let err_msg = "Login did not return JWT token. Are the credentials valid?".to_owned();
|
Logging::error("Login did not return JWT token. Are the credentials valid?");
|
||||||
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);
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
Err(())
|
Err(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn logout(&self) {
|
pub fn logout(&self) {
|
||||||
let _ = self.post_data_json("/api/v3/user/logout", &"").await;
|
let _ = self.post_data_json("/api/v3/user/logout", &"");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn login(&self) {
|
||||||
|
|
||||||
pub(crate) async fn post(&self, post: CreatePost) -> Option<PostId> {
|
}
|
||||||
let response: String = match self.post_data_json("/api/v3/post", &post).await {
|
|
||||||
|
pub fn post(&self, post: CreatePost) -> Option<PostId> {
|
||||||
|
let response: String = match self.post_data_json("/api/v3/post", &post) {
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => return None,
|
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,
|
Some(data) => data,
|
||||||
None => return None,
|
None => return None,
|
||||||
};
|
};
|
||||||
|
@ -276,12 +257,12 @@ impl Lemmy {
|
||||||
Some(json_data.post.id)
|
Some(json_data.post.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn feature(&self, params: FeaturePost) -> Option<PostView> {
|
fn feature(&self, params: FeaturePost) -> Option<PostView> {
|
||||||
let response: String = match self.post_data_json("/api/v3/post/feature", ¶ms).await {
|
let response: String = match self.post_data_json("/api/v3/post/feature", ¶ms) {
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => return None,
|
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,
|
Some(data) => data,
|
||||||
None => return None,
|
None => return None,
|
||||||
};
|
};
|
||||||
|
@ -289,36 +270,36 @@ impl Lemmy {
|
||||||
Some(json_data)
|
Some(json_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Option<PostView> {
|
pub(crate) 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).await
|
self.feature(pin_params)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn pin(&self, post_id: PostId, location: PostFeatureType) -> Option<PostView> {
|
pub(crate) 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).await
|
self.feature(pin_params)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_community_pinned(&self, community: CommunityId) -> Option<Vec<PostView>> {
|
pub(crate) 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).await {
|
let response: String = match self.get_data_query("/api/v3/post/list", &list_params) {
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => return None,
|
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,
|
Some(data) => data,
|
||||||
None => return None,
|
None => return None,
|
||||||
};
|
};
|
||||||
|
@ -331,17 +312,17 @@ impl Lemmy {
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_local_pinned(&self) -> Option<Vec<PostView>> {
|
pub(crate) 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).await {
|
let response: String = match self.get_data_query("/api/v3/post/list", &list_params) {
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => return None,
|
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,
|
Some(data) => data,
|
||||||
None => return None,
|
None => return None,
|
||||||
};
|
};
|
||||||
|
@ -354,17 +335,17 @@ impl Lemmy {
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_communities(&mut self) {
|
pub(crate) 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).await {
|
let response: String = match self.get_data_query("/api/v3/community/list", &list_params) {
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
let json_data: ListCommunitiesResponse = match self.parse_json::<ListCommunitiesResponse>(&response).await {
|
let json_data: ListCommunitiesResponse = match self.parse_json::<ListCommunitiesResponse>(&response) {
|
||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
@ -378,71 +359,71 @@ impl Lemmy {
|
||||||
self.communities = communities;
|
self.communities = communities;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_data_json<T: Serialize>(&self, route: &str, json: &T ) -> Option<String> {
|
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();
|
||||||
.await;
|
self.extract_data(res)
|
||||||
self.extract_data(res).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_data_query<T: Serialize>(&self, route: &str, param: &T ) -> Option<String> {
|
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(¶m)
|
.query(¶m)
|
||||||
.send()
|
.send();
|
||||||
.await;
|
self.extract_data(res)
|
||||||
self.extract_data(res).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn extract_data(&self, response: Result<reqwest::Response, reqwest::Error>) -> Option<String> {
|
fn extract_data(&self, response: Result<reqwest::blocking::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().await {
|
match data.text() {
|
||||||
Ok(data) => Some(data),
|
Ok(data) => Some(data),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let err_msg = format!("{e}");
|
let err_msg = format!("{e}");
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let err_msg = format!("HTTP Request failed: {}", data.text().await.unwrap());
|
let err_msg = format!("HTTP Request failed: {}", data.text().unwrap());
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let err_msg = format!("{e}");
|
let err_msg = format!("{e}");
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn parse_json<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option<T> {
|
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} ");
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn parse_json_map<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option<T> {
|
fn parse_json_map<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Option<T> {
|
||||||
debug!(response);
|
Logging::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}");
|
||||||
error!(err_msg);
|
Logging::error(err_msg.as_str());
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
80
src/logging.rs
Normal file
80
src/logging.rs
Normal file
|
@ -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<String>) -> VecDeque<LogEvent> {
|
||||||
|
static MEM_LOG: Mutex<VecDeque<LogEvent>> = 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<LogEvent> {
|
||||||
|
mem_log(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
87
src/main.rs
87
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 chrono::{Duration};
|
||||||
use log::{LevelFilter};
|
use log::{LevelFilter};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use reqwest::Client;
|
use parking_lot::{RwLock};
|
||||||
|
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()
|
||||||
|
@ -19,13 +30,13 @@ pub static HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||||
.expect("build client")
|
.expect("build client")
|
||||||
});
|
});
|
||||||
|
|
||||||
#[tokio::main]
|
fn 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),
|
||||||
|
@ -36,6 +47,70 @@ async fn main() {
|
||||||
_ => log::set_max_level(LevelFilter::Info),
|
_ => log::set_max_level(LevelFilter::Info),
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut bot = Bot::new();
|
let mut settings = Arc::new(RwLock::new(ApplicationSettings::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));
|
||||||
|
|
||||||
|
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,
|
||||||
}
|
}
|
0
src/settings/database/mod.rs
Normal file
0
src/settings/database/mod.rs
Normal file
126
src/settings/mod.rs
Normal file
126
src/settings/mod.rs
Normal file
|
@ -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<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 BotSettings {
|
||||||
|
id: u16,
|
||||||
|
series_ids: Vec<u16>,
|
||||||
|
instance: String,
|
||||||
|
username: SensitiveString,
|
||||||
|
password: SensitiveString,
|
||||||
|
status_post_url: Option<String>,
|
||||||
|
}
|
||||||
|
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> {
|
||||||
|
self.status_post_url.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
83
src/settings/toml/bot.rs
Normal file
83
src/settings/toml/bot.rs
Normal file
|
@ -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<String>,
|
||||||
|
pub protected_communities: Vec<String>,
|
||||||
|
series: Vec<SeriesConfig>,
|
||||||
|
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<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_url: self.status_post_url.clone(),
|
||||||
|
});
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
22
src/settings/toml/database.rs
Normal file
22
src/settings/toml/database.rs
Normal file
|
@ -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<u16, BotSettings> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn series_settings(&self) -> HashMap<u16, SeriesSettings> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
37
src/settings/toml/mod.rs
Normal file
37
src/settings/toml/mod.rs
Normal file
|
@ -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<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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue