use chrono::{Utc, DateTime, NaiveTime}; use config::{Config, PrevPost, Secrets, CommunitiesVector}; use lemmy_api_common::{ person::{Login, LoginResponse}, post::{CreatePost, GetPosts, GetPostsResponse}, sensitive::Sensitive, }; use lemmy_db_schema::{ ListingType, SortType, }; use once_cell::sync::Lazy; use reqwest::{blocking::Client, StatusCode}; use std::{thread::sleep, time, io, error::Error}; 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 }); struct Bot { secrets: Secrets, config: Config, post_history: Vec, community_ids: CommunitiesVector, auth: Sensitive, start_time: DateTime, } 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(), } } /// Get JWT Token /// /// * `return` : Returns true if token was succesfully retrieved, false otherwise #[warn(unused_results)] pub(crate) fn login(&mut self) -> Result<(), reqwest::Error> { let login_params = Login { username_or_email: self.secrets.lemmy.get_username(), password: self.secrets.lemmy.get_password(), totp_2fa_token: None, }; let res = match CLIENT .post(self.config.instance.clone() + "/api/v3/user/login") .json(&login_params) .send() { Ok(data) => data, Err(e) => return Err(e), }; if res.status() == StatusCode::OK { let data: &LoginResponse = &res.json().unwrap(); let jwt = data.jwt.clone().expect("JWT Token could not be acquired"); self.auth = jwt; return Ok(()); } else { println!("Error Code: {:?}", res.status()); return Err(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) fn post(&mut self, post_data: CreatePost) -> Result<(), reqwest::Error> { let res = match CLIENT .post(self.config.instance.clone() + "/api/v3/post") .json(&post_data) .send() { Ok(data) => data, Err(e) => return Err(e) }; // TODO: process res to get info about if post was successfuly (mostly if jwt token was valid) return Ok(()); } #[warn(unused_results)] pub(crate) fn run_once(&mut self, prev_time: &mut NaiveTime) -> Result<(), reqwest::Error> { println!("{:#<1$}", "", 30); self.start_time = Utc::now(); if self.start_time.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.time(); self.config.load(); match self.community_ids.load(&self.auth, &self.config.instance) { Ok(_) => {}, Err(e) => return Err(e) }; println!("Done!"); } // Start the polling process // Get all feed URLs (use cache) println!("Checking Feeds"); let post_queue: Vec = match self.config.check_feeds(&mut self.post_history, &self.community_ids, &self.auth) { Ok(data) => data, Err(e) => return Err(e) }; println!("Done!"); post_queue.iter().for_each(|post| { println!("Posting: {}", post.name); loop { if self.post(post.clone()).is_ok() {break}; println!("Post attempt failed, retrying"); } }); return Ok(()); } pub(crate) fn idle(&self) { let mut sleep_duration = chrono::Duration::seconds(30); if Utc::now().time() - self.start_time.time() > sleep_duration { sleep_duration = chrono::Duration::seconds(60); } while Utc::now().time() - self.start_time.time() < sleep_duration { sleep(time::Duration::from_secs(1)); } match reqwest::blocking::get("https://status.neshweb.net/api/push/7s1CjPPzrV?status=up&msg=OK&ping=") { Ok(_) => {}, Err(err) => println!("{}", err) }; } pub(crate) fn print_info(&self) { print!("\x1B[2J\x1B[1;1H"); println!("##[Ascendance of a Bookworm Bot]##"); println!("Instance: {}", &self.config.instance); println!("Ran Last: {}", &self.start_time.format("%d/%m/%Y %H:%M:%S")); println!("{:#<1$}", "", 30); self.post_history.iter().for_each(|post| { print!("{} ", post.title); print!("{:<1$}: ", "", 60 - post.title.len()); println!("{}", post.last_post_url); }) } } 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() .unwrap() .text() .unwrap(); return serde_json::from_str(&res).unwrap(); } fn run_bot() { // Get all needed auth tokens at the start let mut old = Utc::now().time(); let mut this = Bot::new(); match this.login() { Ok(_) => { this.community_ids.load(&this.auth, &this.config.instance); // Enter a loop (not for debugging) loop { println!("Start Time: {}", this.start_time.time()); println!("Previous Time: {}", old); println!("Difference Start - Old: {}", this.start_time.time() - old); println!("Difference Now - Start: {}", Utc::now().time() - this.start_time.time()); this.idle(); // 3 retries in case of connection issues let mut loop_breaker: u8 = 0; while !this.run_once(&mut old).is_ok() && loop_breaker <= 3 { println!("Unable to complete Bot cycle, retrying with fresh login credentials"); if this.login().is_ok() { this.community_ids.load(&this.auth, &this.config.instance); } sleep(time::Duration::from_secs(10)); loop_breaker += 1; }; this.print_info(); println!("Start Time: {}", this.start_time.time()); println!("Previous Time: {}", old); println!("Difference Start - Old: {}", this.start_time.time() - old); println!("Difference Now - Start: {}", Utc::now().time() - this.start_time.time()); this.idle(); } }, Err(e) => { println!("Unable to get initial login:\n {:#?}", e); } } } fn main() { run_bot(); }