From 877c7ef32466ec5197444646dddce66c4a2588fa Mon Sep 17 00:00:00 2001 From: Neshura Date: Thu, 22 Jun 2023 22:08:10 +0200 Subject: [PATCH] Move Bot Logic to Class Methods --- src/config/mod.rs | 134 +++++++++++++++++++++++++++++++- src/main.rs | 194 +++++++++++----------------------------------- 2 files changed, 177 insertions(+), 151 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index c449ffe..6fb1519 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,7 +1,11 @@ -use std::{fs::{self, OpenOptions}, path::Path, io::Write}; +use std::{fs::{self, OpenOptions}, path::Path, io::Write, thread::sleep, time}; -use lemmy_api_common::sensitive::Sensitive; +use lemmy_api_common::{sensitive::Sensitive, post::CreatePost, community::{self, ListCommunities, ListCommunitiesResponse}}; +use lemmy_db_schema::newtypes::{LanguageId, CommunityId}; use serde_derive::{Deserialize, Serialize}; +use url::Url; + +use crate::{CLIENT}; macro_rules! pub_struct { ($name:ident {$($field:ident: $t:ty,)*}) => { @@ -19,7 +23,7 @@ pub_struct!(Secrets { }); impl Secrets { - pub(crate) fn load() -> Secrets { + pub(crate) fn init() -> Secrets { let file_contents = match fs::read_to_string("secrets.json") { Ok(data) => data, Err(e) => panic!("ERROR: secrets.json could not be read:\n\n{:#?}", e), @@ -65,7 +69,7 @@ pub_struct!(Config { }); impl Config { - pub(crate) fn load() -> Config { + pub(crate) fn init() -> Config { let file_contents = match fs::read_to_string("config.json") { Ok(data) => data, Err(e) => panic!("ERROR: config.json could not be read:\n\n{:#?}", e), @@ -77,6 +81,80 @@ impl Config { return config_parse; } + + pub(crate) fn load(&mut self) { + let file_contents = match fs::read_to_string("config.json") { + Ok(data) => data, + Err(e) => panic!("ERROR: config.json could not be read:\n\n{:#?}", e), + }; + let config_parse: Config = match serde_json::from_str(&file_contents) { + Ok(data) => data, + Err(e) => panic!("ERROR: config.json could not be parsed:\n\n{:#?}", e), + }; + + self.feeds = config_parse.feeds; + self.instance = config_parse.instance; + self.reddit_config = config_parse.reddit_config; + } + + pub(crate) fn check_feeds(&mut self, post_history: &mut Vec + , community_ids: &CommunitiesVector, auth: &Sensitive) -> Vec { + let mut post_queue: Vec = vec![]; + + self.feeds.iter().for_each(|feed| { + let res = CLIENT + .get(feed.feed_url.clone()) + .send() + .unwrap() + .text() + .unwrap(); + let data: FeedData = serde_json::from_str(&res).unwrap(); + + let mut prev_post_idx: Option = None; + let mut do_post = true; + post_history + .iter() + .enumerate() + .for_each(|(idx, post)| { + if &post.last_post_url == &data.items[0].url { + do_post = false; + } else if &post.title == &data.title { + prev_post_idx = Some(idx); + } + }); + + if do_post { + let item = &data.items[0]; + let new_post = CreatePost { + name: item.title.clone(), + community_id: community_ids.find(&feed.communities.chapter), + url: Some(Url::parse(&item.url).unwrap()), + body: Some( + "[Reddit](https://reddit.com)\n\n[Discord](https://discord.com)".into(), + ), + honeypot: None, + nsfw: Some(false), + 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 + auth: auth.clone(), + }; + post_queue.push(new_post); + match prev_post_idx { + Some(idx) => { + post_history[idx].title = data.title; + post_history[idx].last_post_url = item.url.clone(); + } + None => post_history.push(PrevPost { + title: data.title, + last_post_url: item.url.clone(), + }), + } + } + sleep(time::Duration::from_millis(100)); // Should prevent dos-ing J-Novel servers + }); + + PrevPost::save(&post_history); + return post_queue; + } } pub_struct!(RedditConfig { @@ -177,3 +255,51 @@ pub_struct!(FeedEntry { date_published: String, }); +// Bot Helper Structs +pub_struct!(CommunitiesVector { + ids: Vec<(CommunityId, String)>, +}); + +impl CommunitiesVector { + pub(crate) fn new() -> CommunitiesVector { + CommunitiesVector{ids: vec![]} + } + + pub(crate) fn load(&mut self, auth: &Sensitive, base: &String) { + let params = ListCommunities { + auth: Some(auth.clone()), + ..Default::default() + }; + + let res = CLIENT + .get(base.clone() + "/api/v3/community/list") + .query(¶ms) + .send() + .unwrap() + .text() + .unwrap(); + + let site_data: ListCommunitiesResponse = serde_json::from_str(&res).unwrap(); + + let mut ids = [].to_vec(); + + site_data.communities.iter().for_each(|entry| { + let new_id = (entry.community.id, entry.community.name.clone()); + ids.push(new_id); + }); + + self.ids = ids; + } + + pub(crate) fn find(&self, name: &LemmyCommunities) -> CommunityId { + let mut ret_id = CommunityId(0); + + self.ids.iter().for_each(|id| { + let id_name = &id.1; + if &name.to_string() == id_name { + ret_id = id.0; + } + }); + return ret_id; + } +} diff --git a/src/main.rs b/src/main.rs index 5b628ef..0b243d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,19 @@ -use chrono::Utc; -use config::{Config, PrevPost, Secrets, LemmyCommunities}; +use chrono::{Utc, DateTime, NaiveTime}; +use config::{Config, PrevPost, Secrets, CommunitiesVector}; use lemmy_api_common::{ person::{Login, LoginResponse}, post::{CreatePost, GetPosts, GetPostsResponse}, - sensitive::Sensitive, community::{ListCommunities, ListCommunitiesResponse}, + sensitive::Sensitive, }; use lemmy_db_schema::{ - newtypes::{CommunityId, LanguageId}, ListingType, SortType, }; use once_cell::sync::Lazy; use reqwest::{blocking::Client, StatusCode}; -use tui::{backend::{CrosstermBackend, Backend}, Terminal, widgets::{Widget, Block, Borders, Cell, Row, Table}, layout::{Layout, Constraint, Direction}, Frame, style::{Style, Modifier, Color}}; +use tui::{backend::{CrosstermBackend, Backend}, Terminal, widgets::{Block, Borders, Cell, Row, Table}, layout::{Layout, Constraint, Direction}, Frame, style::{Style, Modifier, Color}}; use crossterm::{event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},}; use std::{thread::{sleep, self}, time::{self, Duration}, io}; -use url::Url; -use crate::config::FeedData; mod config; pub static CLIENT: Lazy = Lazy::new(|| { @@ -34,16 +31,18 @@ struct Bot { post_history: Vec, community_ids: CommunitiesVector, auth: Sensitive, + start_time: DateTime, } impl Bot { pub(crate) fn new() -> Bot { Bot { - secrets: Secrets::load(), - config: Config::load(), + secrets: Secrets::init(), + config: Config::init(), post_history: PrevPost::load(), community_ids: CommunitiesVector::new(), auth: Sensitive::new("".to_string()), + start_time: Utc::now(), } } @@ -77,6 +76,30 @@ impl Bot { .send() .unwrap(); } + + pub(crate) fn run_once(&mut self, mut prev_time: NaiveTime) { + 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 + prev_time = self.start_time.time(); + self.config.load(); + self.community_ids.load(&self.auth, &self.config.instance); + } + + // Start the polling process + // Get all feed URLs (use cache) + let post_queue: Vec = self.config.check_feeds(&mut self.post_history, &self.community_ids, &self.auth); + + post_queue.iter().for_each(|post| { + println!("Posting: {}", post.name); + self.post(post.clone()); + }); + + while Utc::now().time() - self.start_time.time() < chrono::Duration::seconds(60) { + sleep(time::Duration::from_secs(10)); + } + + } } fn list_posts(auth: &Sensitive, base: String) -> GetPostsResponse { @@ -98,144 +121,20 @@ fn list_posts(auth: &Sensitive, base: String) -> GetPostsResponse { return serde_json::from_str(&res).unwrap(); } -struct CommunitiesVector { - ids: Vec<(CommunityId, String)>, -} - -impl CommunitiesVector { - fn new() -> CommunitiesVector { - CommunitiesVector{ids: vec![]} - } - - fn load(&mut self, auth: &Sensitive, base: &String) { - let params = ListCommunities { - auth: Some(auth.clone()), - ..Default::default() - }; - - let res = CLIENT - .get(base.clone() + "/api/v3/community/list") - .query(¶ms) - .send() - .unwrap() - .text() - .unwrap(); - - let site_data: ListCommunitiesResponse = serde_json::from_str(&res).unwrap(); - - let mut ids = [].to_vec(); - - site_data.communities.iter().for_each(|entry| { - let new_id = (entry.community.id, entry.community.name.clone()); - ids.push(new_id); - }); - - self.ids = ids; - } - - fn find(&self, name: &LemmyCommunities) -> CommunityId { - let mut ret_id = CommunityId(0); - - self.ids.iter().for_each(|id| { - let id_name = &id.1; - if &name.to_string() == id_name { - ret_id = id.0; - } - }); - return ret_id; - } -} - fn run_bot() { - // Get all needed auth tokens at the start - let mut old = Utc::now().time(); - let mut this = Bot::new(); - println!("{}", this.secrets.lemmy.username); - this.login(); - this.community_ids.load(&this.auth, &this.config.instance); - - // Create empty eTag list - println!("TODO: Etag list"); - - // Enter a loop (not for debugging) - loop { - let start = Utc::now(); - print!("\x1B[2J\x1B[1;1H"); - println!( - "Started loop at {} {}", - start.format("%H:%M:%S"), - start.timezone() - ); - - if start.time() - old > chrono::Duration::seconds(6) { - old = start.time(); - this.config = Config::load(); - this.community_ids.load(&this.auth, &this.config.instance); - } - - // Start the polling process - // Get all feed URLs (use cache) - let mut post_queue: Vec = vec![]; - this.config.feeds.iter().for_each(|feed| { - let res = CLIENT - .get(feed.feed_url.clone()) - .send() - .unwrap() - .text() - .unwrap(); - let data: FeedData = serde_json::from_str(&res).unwrap(); - - let mut prev_post_idx: Option = None; - let mut do_post = true; - this.post_history - .iter() - .enumerate() - .for_each(|(idx, post)| { - if &post.last_post_url == &data.items[0].url { - do_post = false; - } else if &post.title == &data.title { - prev_post_idx = Some(idx); - } - }); - - if do_post { - let item = &data.items[0]; - let new_post = CreatePost { - name: item.title.clone(), - community_id: this.community_ids.find(&feed.communities.chapter), - url: Some(Url::parse(&item.url).unwrap()), - body: Some( - "[Reddit](https://reddit.com)\n\n[Discord](https://discord.com)".into(), - ), - honeypot: None, - nsfw: Some(false), - 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 - auth: this.auth.clone(), - }; - post_queue.push(new_post); - match prev_post_idx { - Some(idx) => { - this.post_history[idx].title = data.title; - this.post_history[idx].last_post_url = item.url.clone(); - } - None => this.post_history.push(PrevPost { - title: data.title, - last_post_url: item.url.clone(), - }), - } - } - sleep(time::Duration::from_millis(100)); // Should prevent dos-ing J-Novel servers - }); - - PrevPost::save(&this.post_history); - post_queue.iter().for_each(|post| { - println!("Posting: {}", post.name); - this.post(post.clone()); - }); - - while Utc::now().time() - start.time() < chrono::Duration::seconds(60) { - sleep(time::Duration::from_secs(10)); - } + // Get all needed auth tokens at the start + let mut old = Utc::now().time(); + let mut this = Bot::new(); + println!("{}", this.secrets.lemmy.username); + this.login(); + this.community_ids.load(&this.auth, &this.config.instance); + + // Create empty eTag list + println!("TODO: Etag list"); + + // Enter a loop (not for debugging) + loop { + this.run_once(old); } } @@ -304,6 +203,7 @@ fn main() -> Result<(), io::Error> { )?; terminal.show_cursor()?; + + run_bot(); Ok(()) - //run_bot(); }