aob-lemmy-bot/src/main.rs

313 lines
11 KiB
Rust
Raw Normal View History

use chrono::{Utc, NaiveDateTime};
use config::{Config, PrevPost, Secrets, CommunitiesVector, LemmyCommunities};
use lemmy_api_common::{
person::{Login, LoginResponse},
post::{CreatePost, GetPosts, GetPostsResponse, FeaturePost},
sensitive::Sensitive, lemmy_db_views::structs::PostView,
};
use lemmy_db_schema::{
ListingType, SortType, PostFeatureType, newtypes::CommunityId,
};
use once_cell::sync::Lazy;
use reqwest::{blocking::Client, StatusCode};
use std::{thread::sleep, time, collections::HashMap, error::Error};
mod config;
2023-07-30 19:10:36 +00:00
pub static CLIENT: Lazy<Client> = 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<PrevPost>,
2023-06-19 17:21:28 +00:00
community_ids: CommunitiesVector,
auth: Sensitive<String>,
start_time: NaiveDateTime,
}
impl Bot {
pub(crate) fn new() -> Bot {
Bot {
2023-06-22 20:08:10 +00:00
secrets: Secrets::init(),
config: Config::init(),
post_history: PrevPost::load(),
2023-06-19 17:21:28 +00:00
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
2023-07-30 19:10:36 +00:00
#[warn(unused_results)]
pub(crate) fn login(&mut self) -> Result<(), Box<dyn Error>> {
let login_params = Login {
username_or_email: self.secrets.lemmy.get_username(),
password: self.secrets.lemmy.get_password(),
2023-07-09 08:18:51 +00:00
totp_2fa_token: None,
};
let res = CLIENT
2023-06-19 20:10:28 +00:00
.post(self.config.instance.clone() + "/api/v3/user/login")
.json(&login_params)
.send()?;
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(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
2023-07-30 19:10:36 +00:00
#[warn(unused_results)]
pub(crate) fn post(&mut self, post_data: CreatePost) -> Result<PostView, Box<dyn Error>> {
let res = CLIENT
2023-06-19 20:10:28 +00:00
.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)
let ret: PostView = serde_json::from_str::<HashMap<&str, PostView>>(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<usize>, new_post: &PostView) -> Result<(), Box<dyn 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(());
}
2023-06-22 20:08:10 +00:00
pub(crate) fn pin (&mut self, pin_data: FeaturePost) -> Result<bool, Box<dyn Error>> {
let res = CLIENT
.post(self.config.instance.clone() + "/api/v3/post/feature")
.json(&pin_data)
.send()?;
let ret: PostView = serde_json::from_str::<HashMap<&str, PostView>>(res.text()?.as_str()).unwrap().remove("post_view").unwrap();
return Ok(ret.post.featured_local);
}
2023-07-30 19:10:36 +00:00
#[warn(unused_results)]
pub(crate) fn run_once(&mut self, prev_time: &mut NaiveDateTime) -> Result<(), Box<dyn Error>> {
2023-07-31 17:22:28 +00:00
println!("{:#<1$}", "", 30);
self.start_time = Utc::now().naive_local();
2023-06-22 20:08:10 +00:00
if self.start_time - *prev_time > chrono::Duration::seconds(6) { // Prod should use hours, add command line switch later and read duration from config
2023-07-31 17:22:28 +00:00
println!("Reloading Config");
*prev_time = self.start_time;
2023-06-22 20:08:10 +00:00
self.config.load();
self.community_ids.load(&self.auth, &self.config.instance)?;
2023-07-31 17:22:28 +00:00
println!("Done!");
2023-06-22 20:08:10 +00:00
}
// Start the polling process
// Get all feed URLs (use cache)
2023-07-31 17:22:28 +00:00
println!("Checking Feeds");
let post_queue: Vec<(CreatePost, (Option<usize>, usize, String))> = self.config.check_feeds(&mut self.post_history, &self.community_ids, &self.auth)?;
2023-07-31 17:22:28 +00:00
println!("Done!");
2023-06-22 20:08:10 +00:00
let _ = post_queue.iter().map(|(post, (prev_idx, feed_id, feed_title))| -> Result<(), Box<dyn Error>> {
2023-06-22 20:08:10 +00:00
println!("Posting: {}", post.name);
let post_data = self.post(post.clone())?;
self.pin_new(prev_idx, &post_data)?;
// 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(),
}),
}
Ok(())
}).collect::<Vec<_>>();
2023-07-30 19:10:36 +00:00
PrevPost::save(&self.post_history);
return Ok(());
}
2023-06-22 20:08:10 +00:00
pub(crate) fn idle(&self) {
2023-07-31 20:50:48 +00:00
let mut sleep_duration = chrono::Duration::seconds(30);
if Utc::now().naive_local() - self.start_time > sleep_duration {
2023-07-31 20:50:48 +00:00
sleep_duration = chrono::Duration::seconds(60);
}
2023-07-31 21:01:52 +00:00
while Utc::now().naive_local() - self.start_time < sleep_duration {
2023-07-31 21:01:52 +00:00
sleep(time::Duration::from_secs(1));
2023-06-22 20:08:10 +00:00
}
2023-08-02 21:39:35 +00:00
match reqwest::blocking::get("https://status.neshweb.net/api/push/7s1CjPPzrV?status=up&msg=OK&ping=") {
Ok(_) => {},
Err(err) => println!("{}", err)
};
}
2023-07-29 00:32:58 +00:00
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);
})
}
}
2023-06-19 20:10:28 +00:00
fn list_posts(auth: &Sensitive<String>, base: String) -> GetPostsResponse {
let params = GetPosts {
type_: Some(ListingType::Local),
sort: Some(SortType::New),
auth: Some(auth.clone()),
..Default::default()
};
let res = CLIENT
2023-06-19 20:10:28 +00:00
.get(base + "/api/v3/post/list")
.query(&params)
.send()
.unwrap()
.text()
.unwrap();
return serde_json::from_str(&res).unwrap();
}
2023-07-29 00:32:58 +00:00
fn run_bot() {
// Get all needed auth tokens at the start
let mut old = Utc::now().naive_local();
let mut this = Bot::new();
match this.login() {
Ok(_) => {
let _ = this.community_ids.load(&this.auth, &this.config.instance);
// Enter a loop (not for debugging)
loop {
this.idle();
// 3 retries in case of connection issues
//let mut loop_breaker: u8 = 0; // DEBUG disabled for clearer crash finding
match this.run_once(&mut old) {
Ok(_) => {},
Err(e) => panic!("{:#?}", e)
};
/* 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() {
let _ = this.community_ids.load(&this.auth, &this.config.instance);
}
sleep(time::Duration::from_secs(10));
loop_breaker += 1;
}; */
this.print_info();
this.idle();
}
},
Err(e) => {
println!("Unable to get initial login:\n {:#?}", e);
}
}
2023-06-21 19:29:14 +00:00
}
fn main() {
2023-07-29 00:32:58 +00:00
run_bot();
}