diff --git a/src/bot/mod.rs b/src/bot/mod.rs new file mode 100644 index 0000000..30a0b4d --- /dev/null +++ b/src/bot/mod.rs @@ -0,0 +1,225 @@ +use std::collections::HashMap; +use std::error::Error; +use std::fmt::format; +use std::ops::Deref; +use std::sync::{Arc}; +use chrono::{DateTime, Duration, NaiveDateTime, Timelike, Utc}; +use lemmy_db_schema::newtypes::{CommunityId, LanguageId}; +use lemmy_db_schema::PostFeatureType; +use tokio::sync::{RwLock, RwLockWriteGuard, RwLockReadGuard}; +use crate::{jnovel, lemmy, Message, SharedData}; +use crate::config::{Config, PostBody, SeriesConfig}; +use crate::jnovel::PostInfo; +use crate::lemmy::{CustomCreatePost, Lemmy}; +use crate::post_history::SeriesHistory; +use tokio::time::sleep; + +pub(crate) async fn run(data: Arc>){ + let credentials = match lemmy::Credentials::set_credentials() { + Ok(creds) => creds, + Err(e) => panic!("{}", e.to_string()), + }; + + let mut last_reload: DateTime; + let mut lemmy: Lemmy; + let mut login_error: bool; + let communities: HashMap; + { + let mut shared_data = data.write().await; + + // Errors during bot init are likely unrecoverable and therefore should panic the bot + // Does not really matter since the bot will get restarted anyway but this way the uptime url logs a downtime + shared_data.config = match Config::load() { + Ok(data) => data, + Err(e) => panic!("{}", e), + }; + last_reload = Utc::now(); + + lemmy = match lemmy::login(&credentials, shared_data.config.instance.as_str()).await { + Ok(data) => data, + Err(e) => panic!("{}", e), + }; + login_error = false; + + communities = match lemmy.get_communities().await { + Ok(data) => data, + Err(e) => panic!("{}", e), + }; + } + + while (Utc::now().naive_local().second() != 30) { + sleep(Duration::milliseconds(100).to_std().unwrap()).await; + } + + loop { + idle(&data).await; + + let mut shared_data = data.write().await; + + shared_data.start = Utc::now(); + + if shared_data.start - last_reload > Duration::seconds(shared_data.config.config_reload_seconds as i64) { + shared_data.config = match Config::load() { + Ok(data) => data, + Err(e) => panic!("{}", e), + }; + } + + if login_error { + lemmy = match lemmy::login(&credentials, shared_data.config.instance.as_str()).await { + Ok(data) => data, + Err(e) => { + shared_data.messages.push(Message::Error(format!("{}", e))); + continue + } + }; + login_error = false; + } + + if shared_data.start - last_reload > Duration::seconds(shared_data.config.config_reload_seconds as i64) { + let communities = match lemmy.get_communities().await { + Ok(data) => data, + Err(e) => { + login_error = true; + shared_data.messages.push(Message::Error(format!("{}", e))); + continue + } + }; + last_reload = Utc::now(); + } + + shared_data.post_history = match SeriesHistory::load_history() { + Ok(data) => data, + Err(e) => { + login_error = true; + shared_data.messages.push(Message::Warning(format!("{}", e))); + continue + } + }; + + for series in shared_data.config.series.clone() { + match handle_series(&series, &communities, &lemmy, &mut shared_data).await { + Ok(data) => data, + Err(e) => { + login_error = true; + shared_data.messages.push(Message::Warning(format!("{}", e))); + continue + } + }; + } + + let down = shared_data.downgrade(); + + idle(&data).await; + } +} + +async fn idle(data: &Arc>) { + let read = data.read().await; + let mut sleep_duration = Duration::seconds(30); + if Utc::now() - read.start > sleep_duration { + sleep_duration = Duration::seconds(60); + } + + if let Some(status_url) = read.config.status_post_url.clone() { + match reqwest::get(status_url).await { + Ok(_) => {} + Err(e) => { + let mut write = data.write().await; + write.messages.push(Message::Error(format!("{}", e))) + }, + } + }; + + while Utc::now() - read.start < sleep_duration { + sleep(Duration::milliseconds(100).to_std().unwrap()).await; + } +} + +async fn handle_series<'a>( + series: &SeriesConfig, + communities: &HashMap, + lemmy: &Lemmy, + data: &mut RwLockWriteGuard<'a, SharedData>, +) -> Result<(), Box> { + let mut post_list = match jnovel::check_feed(series.slug.as_str(), series.parted).await { + Ok(data) => data, + Err(e) => panic!("{:#?}", e), + }; + + for (index, post_info) in post_list.clone().iter().enumerate() { // todo .clone() likely not needed + let post_part_info = post_info.get_part_info(); + let post_lemmy_info = post_info.get_lemmy_info(); + + if data.post_history.check_for_post(series.slug.as_str(), post_part_info.as_string().as_str(), post_lemmy_info.title.as_str()) { + data.messages.push(Message::Info(format!("Skipping '{}' since already posted", post_lemmy_info.title))); + post_list.remove(index); + continue + } + + let post_series_config = match post_info { + PostInfo::Chapter {..} => {&series.prepub_community}, + PostInfo::Volume {..} => {&series.volume_community} + }; + + let community_id = *communities + .get(post_series_config.name.as_str()) + .expect("Given community is invalid"); + + let post_body = match &post_series_config.post_body { + PostBody::None => None, + PostBody::Description => post_info.get_description(), + PostBody::Custom(text) => Some(text.clone()), + }; + + let post_data = CustomCreatePost { + name: post_lemmy_info.title, + community_id: community_id, + url: Some(post_lemmy_info.url), + body: post_body, + honeypot: None, + nsfw: None, + language_id: Some(LanguageId(37)), // TODO get this id once every few hours per API request, the ordering of IDs suggests that the EN Id might change in the future + }; + + let post_id = lemmy.post(post_data).await?; + + if post_series_config.pin_settings.pin_new_post_community { + let pinned_posts = lemmy.get_community_pinned(community_id).await?; + if pinned_posts.len() > 0 { + let community_pinned_post = &pinned_posts[0]; + lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Community).await?; + } + lemmy.pin(post_id, PostFeatureType::Community).await?; + } + + if post_series_config.pin_settings.pin_new_post_local { + let pinned_posts = lemmy.get_local_pinned().await?; + if pinned_posts.len() > 0 { + let community_pinned_post = &pinned_posts[0]; + lemmy.unpin(community_pinned_post.post.id, PostFeatureType::Local).await?; + } + lemmy.pin(post_id, PostFeatureType::Local).await?; + } + + let mut series_history = data.post_history.get_series(series.slug.as_str()); + let mut part_history = series_history.get_part(post_part_info.as_string().as_str()); + + match post_info { + PostInfo::Chapter {..} => { + part_history.chapter = post_info.get_lemmy_info().title + }, + PostInfo::Volume {..} => { + part_history.volume = post_info.get_lemmy_info().title + } + } + + series_history.set_part(post_part_info.as_string().as_str(), part_history); + data.post_history.set_series(series.slug.as_str(), series_history); + data.post_history.save_history(); + } + Ok(()) +} + + +