use chrono::{NaiveDateTime, Utc}; use config::{CommunitiesVector, Config, LemmyCommunities, PrevPost, Secrets}; use lemmy_api_common::{ lemmy_db_views::structs::PostView, person::{Login, LoginResponse}, post::{CreatePost, FeaturePost, GetPosts, GetPostsResponse}, sensitive::Sensitive, }; use lemmy_db_schema::{newtypes::CommunityId, ListingType, PostFeatureType, SortType}; use once_cell::sync::Lazy; use reqwest::{Client, StatusCode}; use std::{ collections::HashMap, error::Error, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, thread::{self, sleep}, time::{self, Duration}, }; use tokio::sync::Mutex; mod config; pub static CLIENT: Lazy = Lazy::new(|| { let client = Client::builder() .timeout(time::Duration::from_secs(30)) .connect_timeout(time::Duration::from_secs(30)) .build() .expect("build client"); client }); #[derive(Clone)] struct Bot { secrets: Secrets, config: Config, post_history: Vec, community_ids: CommunitiesVector, auth: Sensitive, start_time: NaiveDateTime, } impl Bot { pub(crate) fn new() -> Bot { Bot { secrets: Secrets::init(), config: Config::init(), post_history: PrevPost::load(), community_ids: CommunitiesVector::new(), auth: Sensitive::new("".to_string()), start_time: Utc::now().naive_local(), } } /// Get JWT Token /// /// * `return` : Returns true if token was succesfully retrieved, false otherwise #[warn(unused_results)] pub(crate) async fn login(&mut self) -> Result, Box> { let login_params = Login { username_or_email: self.secrets.lemmy.get_username(), password: self.secrets.lemmy.get_password(), totp_2fa_token: None, }; let res = CLIENT .post(self.config.instance.clone() + "/api/v3/user/login") .json(&login_params) .send().await?; if res.status() == StatusCode::OK { let data: &LoginResponse = &res.json().await.unwrap(); let jwt = data.jwt.clone().expect("JWT Token could not be acquired"); self.auth = jwt.clone(); return Ok(jwt); } else { println!("Error Code: {:?}", res.status()); return Err(Box::new(res.error_for_status().unwrap_err())); } } /// Make Post to Lemmy Instance /// /// * `post_data` : Object of type [CreatePost] containing post info /// * `return` : Returns true if Post was succesful, false otherwise #[warn(unused_results)] pub(crate) async fn post(&mut self, post_data: CreatePost) -> Result> { let res = CLIENT .post(self.config.instance.clone() + "/api/v3/post") .json(&post_data) .send().await?; // TODO: process res to get info about if post was successfuly (mostly if jwt token was valid) let ret: PostView = serde_json::from_str::>(res.text().await?.as_str()) .unwrap() .remove("post_view") .unwrap(); return Ok(ret); } #[warn(unused_results)] pub(crate) async fn pin_new( &mut self, old_post: &Option, new_post: &PostView, ) -> Result<(), Box> { match old_post { Some(id) => { let remove_community_pin = FeaturePost { post_id: self.post_history[*id].post_id, featured: false, feature_type: PostFeatureType::Community, auth: self.auth.clone(), }; let _ = self.pin(remove_community_pin); } None => { println!("Unable to unpin old post, please do so manually"); } } // Unpin the other chapter post on local // Get all local pinned posts first // Filter out any post made in the meta community (Community ID) let get_params = GetPosts { auth: Some(self.auth.clone()), ..Default::default() }; let post_list_json = CLIENT .get(self.config.instance.clone() + "/api/v3/post/list") .query(&get_params) .send().await? .text().await?; let post_list: GetPostsResponse = serde_json::from_str(post_list_json.as_str()).unwrap(); let mut meta_community: CommunityId = CommunityId(15); self.community_ids.ids.iter().for_each(|(id, name)| { if name == &LemmyCommunities::metadiscussions.to_string() { meta_community = id.clone(); } }); for post_view in post_list.posts { if post_view.community.id != meta_community && post_view.post.featured_local { let remove_local_pin = FeaturePost { post_id: post_view.post.id, featured: false, feature_type: PostFeatureType::Local, auth: self.auth.clone(), }; match self.pin(remove_local_pin).await { Ok(_) => {} Err(e) => println!("Error Unpinning Post: {:#?}", e), }; } } let pin_new_community = FeaturePost { post_id: new_post.post.id, featured: true, feature_type: PostFeatureType::Community, auth: self.auth.clone(), }; let _ = self.pin(pin_new_community); let pin_new_local = FeaturePost { post_id: new_post.post.id, featured: true, feature_type: PostFeatureType::Local, auth: self.auth.clone(), }; match self.pin(pin_new_local).await { Ok(_) => {} Err(e) => println!("Error Pinning Post: {:#?}", e), }; return Ok(()); } pub(crate) async fn pin(&mut self, pin_data: FeaturePost) -> Result> { let res = CLIENT .post(self.config.instance.clone() + "/api/v3/post/feature") .json(&pin_data) .send().await?; let ret: PostView = serde_json::from_str::>(res.text().await?.as_str()) .unwrap() .remove("post_view") .unwrap(); return Ok(ret.post.featured_local); } #[warn(unused_results)] pub(crate) async fn run_once(&mut self, prev_time: &mut NaiveDateTime) -> Result<(), Box> { println!("{:#<1$}", "", 30); self.start_time = Utc::now().naive_local(); if self.start_time - *prev_time > chrono::Duration::seconds(6) { // Prod should use hours, add command line switch later and read duration from config println!("Reloading Config"); *prev_time = self.start_time; self.config.load(); self.community_ids.load(&self.auth, &self.config.instance).await?; println!("Done!"); } // Start the polling process // Get all feed URLs (use cache) println!("Checking Feeds"); let post_queue: Vec<(CreatePost, (Option, usize, String))> = self .config .check_feeds(&mut self.post_history, &self.community_ids, &self.auth).await?; println!("Done!"); let mut i = 0; while i < post_queue.len() { let (post, (prev_idx, feed_id, feed_title)) = &post_queue[i]; println!("Posting: {}", post.name); let post_data = self.post(post.clone()).await?; self.pin_new(&prev_idx, &post_data).await?; // Move current post to old post list match prev_idx { Some(idx) => { self.post_history[*idx].title = feed_title.clone(); self.post_history[*idx].post_id = post_data.post.id; self.post_history[*idx].last_post_url = post.url.clone().unwrap().to_string(); } None => self.post_history.push(PrevPost { id: feed_id.clone(), post_id: post_data.post.id, title: feed_title.clone(), last_post_url: post.url.clone().unwrap().to_string(), }), } i += 1; } PrevPost::save(&self.post_history); return Ok(()); } pub(crate) async fn idle(&self) { let mut sleep_duration = chrono::Duration::seconds(30); if Utc::now().naive_local() - self.start_time > sleep_duration { sleep_duration = chrono::Duration::seconds(60); } while Utc::now().naive_local() - self.start_time < sleep_duration { sleep(time::Duration::from_secs(1)); } match reqwest::get( "https://status.neshweb.net/api/push/7s1CjPPzrV?status=up&msg=OK&ping=", ).await { Ok(_) => {} Err(err) => println!("{}", err), }; } } async fn list_posts(auth: &Sensitive, base: String) -> GetPostsResponse { let params = GetPosts { type_: Some(ListingType::Local), sort: Some(SortType::New), auth: Some(auth.clone()), ..Default::default() }; let res = CLIENT .get(base + "/api/v3/post/list") .query(¶ms) .send().await .unwrap() .text().await .unwrap(); return serde_json::from_str(&res).unwrap(); } async fn run_bot(bot: Arc>) { // TODO this currently does not update the bot Mutex when run // Get all needed auth tokens at the start let mut old = Utc::now().naive_local(); let mut this = bot.lock().await.clone(); match this.login().await { Ok(_) => { println!("Login successful"); }, Err(e) => { println!("Unable to get initial login:\n {:#?}", e); } }; let _ = this.community_ids.load(&this.auth, &this.config.instance).await; loop { this.idle().await; println!("Debug A"); // DEBUG match this.run_once(&mut old).await { Ok(_) => {} Err(e) => panic!("Crashed due to Error: {:#?}", e), }; *bot.lock().await = this.clone(); this.idle().await; } } async fn print_info(shutdown: Arc, bot: Arc>) { while !shutdown.load(Ordering::Relaxed) { let bot: tokio::sync::MutexGuard<'_, Bot> = bot.lock().await; thread::sleep(Duration::from_millis(500)); print!("\x1B[2J\x1B[1;1H"); println!( "##[Ascendance of a Bookworm Bot]## | Time: {}", Utc::now().naive_local().format("%H:%M:%S") ); println!("Instance: {}", bot.config.instance); println!("Ran Last: {}", bot.start_time.format("%d/%m/%Y %H:%M:%S")); println!("{:#<1$}", "", 30); bot.post_history.iter().for_each(|post| { print!("| -- |"); print!("{} ", post.title); print!("{:<1$}| ", "", 60 - post.title.len()); println!("{}", post.last_post_url); }) } } #[tokio::main] async fn main() { let shutdown = Arc::new(AtomicBool::new(false)); loop { println!("Starting AoB Bot..."); let shutdown_clone = shutdown.clone(); let bot = Arc::new(Mutex::new(Bot::new())); let bot_clone = bot.clone(); let bot_thread = tokio::spawn(async move { run_bot(bot).await }); let tui_thread = tokio::spawn(async move { print_info(shutdown_clone, bot_clone).await }); let _ = bot_thread.await; tui_thread.abort(); println!("Bot crashed due to unknown Error, restarting thread after wait..."); sleep(Duration::from_secs(30)); } }