From 8c58088321b18855fa5a56db9174712057ccf886 Mon Sep 17 00:00:00 2001 From: Neshura Date: Thu, 3 Aug 2023 23:34:23 +0200 Subject: [PATCH] Automatic Post Pinning and Unpinning, Closes #2 --- src/config/mod.rs | 34 +++++------ src/main.rs | 140 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 142 insertions(+), 32 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index cff0599..3ba8dfb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,7 +1,7 @@ use std::{fs::{self, OpenOptions}, path::Path, io::Write, thread::sleep, time}; use lemmy_api_common::{sensitive::Sensitive, post::CreatePost, community::{ListCommunities, ListCommunitiesResponse}}; -use lemmy_db_schema::{newtypes::{LanguageId, CommunityId}, ListingType}; +use lemmy_db_schema::{newtypes::{LanguageId, CommunityId, PostId}, ListingType}; use serde_derive::{Deserialize, Serialize}; use url::Url; @@ -98,8 +98,8 @@ impl Config { } pub(crate) fn check_feeds(&mut self, post_history: &mut Vec - , community_ids: &CommunitiesVector, auth: &Sensitive) -> Result, reqwest::Error> { - let mut post_queue: Vec = vec![]; + , community_ids: &CommunitiesVector, auth: &Sensitive) -> Result, usize, String))>, reqwest::Error> { + let mut post_queue: Vec<(CreatePost, (Option, usize, String))> = vec![]; match self.feeds.iter().map(|feed| { let res = CLIENT @@ -135,18 +135,14 @@ impl Config { 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 { - id: feed.id, - title: data.title, - last_post_url: item.url.clone(), - }), - } + + let prev_data = ( + prev_post_idx, + feed.id, + data.title + ); + + post_queue.push((new_post, prev_data)); } sleep(time::Duration::from_millis(100)); // Should prevent dos-ing J-Novel servers return Ok(()); @@ -155,7 +151,6 @@ impl Config { Err(e) => return Err(e) } - PrevPost::save(&post_history); return Ok(post_queue); } } @@ -184,7 +179,7 @@ pub(crate) enum LemmyCommunities { aobprepub, aoblightnovel, aobmanga, - aobanime + metadiscussions } pub_struct!(FeedRedditSettings { @@ -195,6 +190,7 @@ pub_struct!(FeedRedditSettings { // Posts structs pub_struct!(PrevPost { id: usize, + post_id: PostId, title: String, last_post_url: String, }); @@ -206,13 +202,13 @@ impl PrevPost { if Path::new("posts.json").exists() { let file_contents = match fs::read_to_string("posts.json") { Ok(data) => data, - Err(e) => panic!("ERROR: secrets.json could not be read:\n\n{:#?}", e), + Err(e) => panic!("ERROR: posts.json could not be read:\n\n{:#?}", e), }; if file_contents.len() > 0 { let history_parse: Vec = match serde_json::from_str(&file_contents) { Ok(data) => data, - Err(e) => panic!("ERROR: secrets.json could not be parsed:\n\n{:#?}", e), + Err(e) => panic!("ERROR: posts.json could not be parsed:\n\n{:#?}", e), }; history = history_parse; } diff --git a/src/main.rs b/src/main.rs index dc55b0e..edf3764 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,16 @@ -use chrono::{Utc, DateTime, NaiveTime, NaiveDateTime, Timelike, NaiveDate}; -use config::{Config, PrevPost, Secrets, CommunitiesVector}; +use chrono::{Utc, NaiveDateTime}; +use config::{Config, PrevPost, Secrets, CommunitiesVector, LemmyCommunities}; use lemmy_api_common::{ person::{Login, LoginResponse}, - post::{CreatePost, GetPosts, GetPostsResponse}, - sensitive::Sensitive, + post::{CreatePost, GetPosts, GetPostsResponse, FeaturePost}, + sensitive::Sensitive, lemmy_db_views::structs::PostView, }; use lemmy_db_schema::{ - ListingType, SortType, + ListingType, SortType, PostFeatureType, newtypes::CommunityId, }; use once_cell::sync::Lazy; use reqwest::{blocking::Client, StatusCode}; -use std::{thread::sleep, time, io, error::Error}; +use std::{thread::sleep, time, collections::HashMap}; mod config; @@ -80,17 +80,102 @@ impl Bot { /// * `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> { + pub(crate) fn post(&mut self, post_data: CreatePost) -> Result { let res = CLIENT .post(self.config.instance.clone() + "/api/v3/post") .json(&post_data) .send()?; - // TODO: process res to get info about if post was successfuly (mostly if jwt token was valid) + // 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()?.as_str()).unwrap().remove("post_view").unwrap(); + return Ok(ret); + } + + #[warn(unused_results)] + pub(crate) fn pin_new(&mut self, old_post: &Option, new_post: &PostView) -> Result<(), reqwest::Error> { + 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()? + .text()?; + + 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() + }; + + let _ = self.pin(remove_local_pin); + } + } + + 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(), + }; + + let _ = self.pin(pin_new_local); return Ok(()); } + pub(crate) fn pin (&mut self, pin_data: FeaturePost) -> Result { + let res = CLIENT + .post(self.config.instance.clone() + "/api/v3/post/feature") + .json(&pin_data) + .send()?; + + let ret: PostView = serde_json::from_str::>(res.text()?.as_str()).unwrap().remove("post_view").unwrap(); + return Ok(ret.post.featured_local); + } + #[warn(unused_results)] pub(crate) fn run_once(&mut self, prev_time: &mut NaiveDateTime) -> Result<(), reqwest::Error> { println!("{:#<1$}", "", 30); @@ -107,17 +192,46 @@ impl Bot { // Start the polling process // Get all feed URLs (use cache) println!("Checking Feeds"); - let post_queue: Vec = self.config.check_feeds(&mut self.post_history, &self.community_ids, &self.auth)?; + let post_queue: Vec<(CreatePost, (Option, usize, String))> = self.config.check_feeds(&mut self.post_history, &self.community_ids, &self.auth)?; println!("Done!"); - post_queue.iter().for_each(|post| { + post_queue.iter().for_each(|(post, (prev_idx, feed_id, feed_title))| { println!("Posting: {}", post.name); + + let post_data: PostView; + loop { - if self.post(post.clone()).is_ok() {break}; + let post_res = self.post(post.clone()); + if post_res.is_ok() { + post_data = post_res.unwrap(); + break + }; println!("Post attempt failed, retrying"); } + + loop { + if self.pin_new(prev_idx, &post_data).is_ok() {break}; + println!("Pin attempt failed, retrying"); + } + + // 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(), + }), + } }); + PrevPost::save(&self.post_history); + return Ok(()); } @@ -176,7 +290,7 @@ fn run_bot() { let mut this = Bot::new(); match this.login() { Ok(_) => { - this.community_ids.load(&this.auth, &this.config.instance); + let _ = this.community_ids.load(&this.auth, &this.config.instance); // Enter a loop (not for debugging) loop { @@ -186,7 +300,7 @@ fn run_bot() { 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); + let _ = this.community_ids.load(&this.auth, &this.config.instance); } sleep(time::Duration::from_secs(10)); loop_breaker += 1;