2023-08-31 21:36:37 +00:00
|
|
|
use chrono::{NaiveDateTime, Utc};
|
|
|
|
use config::{CommunitiesVector, Config, LemmyCommunities, PrevPost, Secrets};
|
2023-06-18 22:26:50 +00:00
|
|
|
use lemmy_api_common::{
|
2023-08-31 21:36:37 +00:00
|
|
|
lemmy_db_views::structs::PostView,
|
2023-06-18 22:26:50 +00:00
|
|
|
person::{Login, LoginResponse},
|
2023-08-31 21:36:37 +00:00
|
|
|
post::{CreatePost, FeaturePost, GetPosts, GetPostsResponse},
|
|
|
|
sensitive::Sensitive,
|
2023-06-18 22:26:50 +00:00
|
|
|
};
|
2023-08-31 21:36:37 +00:00
|
|
|
use lemmy_db_schema::{newtypes::CommunityId, ListingType, PostFeatureType, SortType};
|
2023-06-18 22:26:50 +00:00
|
|
|
use once_cell::sync::Lazy;
|
2023-08-31 21:36:37 +00:00
|
|
|
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;
|
2023-06-18 22:26:50 +00:00
|
|
|
|
|
|
|
mod config;
|
|
|
|
|
|
|
|
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
|
|
|
|
});
|
|
|
|
|
2023-08-31 21:36:37 +00:00
|
|
|
#[derive(Clone)]
|
2023-06-18 22:26:50 +00:00
|
|
|
struct Bot {
|
|
|
|
secrets: Secrets,
|
|
|
|
config: Config,
|
|
|
|
post_history: Vec<PrevPost>,
|
2023-06-19 17:21:28 +00:00
|
|
|
community_ids: CommunitiesVector,
|
2023-06-18 22:26:50 +00:00
|
|
|
auth: Sensitive<String>,
|
2023-08-03 19:27:39 +00:00
|
|
|
start_time: NaiveDateTime,
|
2023-06-18 22:26:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Bot {
|
|
|
|
pub(crate) fn new() -> Bot {
|
|
|
|
Bot {
|
2023-06-22 20:08:10 +00:00
|
|
|
secrets: Secrets::init(),
|
|
|
|
config: Config::init(),
|
2023-06-18 22:26:50 +00:00
|
|
|
post_history: PrevPost::load(),
|
2023-06-19 17:21:28 +00:00
|
|
|
community_ids: CommunitiesVector::new(),
|
2023-06-18 22:26:50 +00:00
|
|
|
auth: Sensitive::new("".to_string()),
|
2023-08-03 19:27:39 +00:00
|
|
|
start_time: Utc::now().naive_local(),
|
2023-06-18 22:26:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-30 18:34:24 +00:00
|
|
|
/// Get JWT Token
|
2023-08-31 21:36:37 +00:00
|
|
|
///
|
2023-07-30 18:34:24 +00:00
|
|
|
/// * `return` : Returns true if token was succesfully retrieved, false otherwise
|
2023-07-30 19:10:36 +00:00
|
|
|
#[warn(unused_results)]
|
2023-08-31 21:36:37 +00:00
|
|
|
pub(crate) async fn login(&mut self) -> Result<Sensitive<String>, Box<dyn Error>> {
|
2023-06-18 22:26:50 +00:00
|
|
|
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,
|
2023-06-18 22:26:50 +00:00
|
|
|
};
|
|
|
|
|
2023-08-03 19:38:22 +00:00
|
|
|
let res = CLIENT
|
2023-06-19 20:10:28 +00:00
|
|
|
.post(self.config.instance.clone() + "/api/v3/user/login")
|
2023-06-18 22:26:50 +00:00
|
|
|
.json(&login_params)
|
2023-08-31 21:36:37 +00:00
|
|
|
.send().await?;
|
2023-06-18 22:26:50 +00:00
|
|
|
|
|
|
|
if res.status() == StatusCode::OK {
|
2023-08-31 21:36:37 +00:00
|
|
|
let data: &LoginResponse = &res.json().await.unwrap();
|
2023-06-18 22:26:50 +00:00
|
|
|
|
|
|
|
let jwt = data.jwt.clone().expect("JWT Token could not be acquired");
|
2023-08-31 21:36:37 +00:00
|
|
|
self.auth = jwt.clone();
|
|
|
|
return Ok(jwt);
|
2023-06-18 22:26:50 +00:00
|
|
|
} else {
|
|
|
|
println!("Error Code: {:?}", res.status());
|
2023-08-14 20:52:34 +00:00
|
|
|
return Err(Box::new(res.error_for_status().unwrap_err()));
|
2023-06-18 22:26:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-30 18:34:24 +00:00
|
|
|
/// Make Post to Lemmy Instance
|
2023-08-31 21:36:37 +00:00
|
|
|
///
|
2023-07-30 18:34:24 +00:00
|
|
|
/// * `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)]
|
2023-08-31 21:36:37 +00:00
|
|
|
pub(crate) async fn post(&mut self, post_data: CreatePost) -> Result<PostView, Box<dyn Error>> {
|
2023-08-03 19:38:22 +00:00
|
|
|
let res = CLIENT
|
2023-06-19 20:10:28 +00:00
|
|
|
.post(self.config.instance.clone() + "/api/v3/post")
|
2023-06-18 22:26:50 +00:00
|
|
|
.json(&post_data)
|
2023-08-31 21:36:37 +00:00
|
|
|
.send().await?;
|
2023-07-30 18:34:24 +00:00
|
|
|
|
2023-08-31 21:36:37 +00:00
|
|
|
// 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().await?.as_str())
|
|
|
|
.unwrap()
|
|
|
|
.remove("post_view")
|
|
|
|
.unwrap();
|
2023-08-03 21:34:23 +00:00
|
|
|
return Ok(ret);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[warn(unused_results)]
|
2023-08-31 21:36:37 +00:00
|
|
|
pub(crate) async fn pin_new(
|
|
|
|
&mut self,
|
|
|
|
old_post: &Option<usize>,
|
|
|
|
new_post: &PostView,
|
|
|
|
) -> Result<(), Box<dyn Error>> {
|
2023-08-03 21:34:23 +00:00
|
|
|
match old_post {
|
|
|
|
Some(id) => {
|
|
|
|
let remove_community_pin = FeaturePost {
|
|
|
|
post_id: self.post_history[*id].post_id,
|
|
|
|
featured: false,
|
|
|
|
feature_type: PostFeatureType::Community,
|
2023-08-31 21:36:37 +00:00
|
|
|
auth: self.auth.clone(),
|
2023-08-03 21:34:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
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)
|
2023-08-31 21:36:37 +00:00
|
|
|
.send().await?
|
|
|
|
.text().await?;
|
2023-08-03 21:34:23 +00:00
|
|
|
|
|
|
|
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,
|
2023-08-31 21:36:37 +00:00
|
|
|
auth: self.auth.clone(),
|
2023-08-03 21:34:23 +00:00
|
|
|
};
|
2023-08-31 21:36:37 +00:00
|
|
|
|
|
|
|
match self.pin(remove_local_pin).await {
|
|
|
|
Ok(_) => {}
|
|
|
|
Err(e) => println!("Error Unpinning Post: {:#?}", e),
|
2023-08-14 20:55:15 +00:00
|
|
|
};
|
2023-08-03 21:34:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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(),
|
|
|
|
};
|
2023-08-31 21:36:37 +00:00
|
|
|
|
|
|
|
match self.pin(pin_new_local).await {
|
|
|
|
Ok(_) => {}
|
|
|
|
Err(e) => println!("Error Pinning Post: {:#?}", e),
|
|
|
|
};
|
2023-07-30 18:34:24 +00:00
|
|
|
|
2023-07-31 17:50:22 +00:00
|
|
|
return Ok(());
|
2023-06-18 22:26:50 +00:00
|
|
|
}
|
2023-06-22 20:08:10 +00:00
|
|
|
|
2023-08-31 21:36:37 +00:00
|
|
|
pub(crate) async fn pin(&mut self, pin_data: FeaturePost) -> Result<bool, Box<dyn Error>> {
|
2023-08-03 21:34:23 +00:00
|
|
|
let res = CLIENT
|
|
|
|
.post(self.config.instance.clone() + "/api/v3/post/feature")
|
|
|
|
.json(&pin_data)
|
2023-08-31 21:36:37 +00:00
|
|
|
.send().await?;
|
2023-08-03 21:34:23 +00:00
|
|
|
|
2023-08-31 21:36:37 +00:00
|
|
|
let ret: PostView = serde_json::from_str::<HashMap<&str, PostView>>(res.text().await?.as_str())
|
|
|
|
.unwrap()
|
|
|
|
.remove("post_view")
|
|
|
|
.unwrap();
|
2023-08-03 21:34:23 +00:00
|
|
|
return Ok(ret.post.featured_local);
|
|
|
|
}
|
|
|
|
|
2023-07-30 19:10:36 +00:00
|
|
|
#[warn(unused_results)]
|
2023-08-31 21:36:37 +00:00
|
|
|
pub(crate) async fn run_once(&mut self, prev_time: &mut NaiveDateTime) -> Result<(), Box<dyn Error>> {
|
2023-07-31 17:22:28 +00:00
|
|
|
println!("{:#<1$}", "", 30);
|
2023-08-03 19:27:39 +00:00
|
|
|
self.start_time = Utc::now().naive_local();
|
2023-06-22 20:08:10 +00:00
|
|
|
|
2023-08-31 21:36:37 +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");
|
2023-08-03 19:27:39 +00:00
|
|
|
*prev_time = self.start_time;
|
2023-06-22 20:08:10 +00:00
|
|
|
self.config.load();
|
2023-08-31 21:36:37 +00:00
|
|
|
self.community_ids.load(&self.auth, &self.config.instance).await?;
|
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");
|
2023-08-31 21:36:37 +00:00
|
|
|
let post_queue: Vec<(CreatePost, (Option<usize>, usize, String))> = self
|
|
|
|
.config
|
|
|
|
.check_feeds(&mut self.post_history, &self.community_ids, &self.auth).await?;
|
2023-07-31 17:22:28 +00:00
|
|
|
println!("Done!");
|
2023-06-22 20:08:10 +00:00
|
|
|
|
2023-08-03 21:34:23 +00:00
|
|
|
|
2023-08-31 21:36:37 +00:00
|
|
|
let mut i = 0;
|
|
|
|
while i < post_queue.len() {
|
|
|
|
let (post, (prev_idx, feed_id, feed_title)) = &post_queue[i];
|
2023-08-03 21:34:23 +00:00
|
|
|
|
2023-08-31 21:36:37 +00:00
|
|
|
println!("Posting: {}", post.name);
|
2023-08-03 21:34:23 +00:00
|
|
|
|
2023-08-31 21:36:37 +00:00
|
|
|
let post_data = self.post(post.clone()).await?;
|
|
|
|
|
|
|
|
self.pin_new(&prev_idx, &post_data).await?;
|
|
|
|
|
2023-08-03 21:34:23 +00:00
|
|
|
// 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;
|
2023-08-31 21:36:37 +00:00
|
|
|
self.post_history[*idx].last_post_url =
|
|
|
|
post.url.clone().unwrap().to_string();
|
2023-08-03 21:34:23 +00:00
|
|
|
}
|
|
|
|
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(),
|
|
|
|
}),
|
|
|
|
}
|
2023-08-31 21:36:37 +00:00
|
|
|
|
|
|
|
i += 1;
|
|
|
|
}
|
2023-07-30 19:10:36 +00:00
|
|
|
|
2023-08-03 21:34:23 +00:00
|
|
|
PrevPost::save(&self.post_history);
|
|
|
|
|
2023-07-31 17:50:22 +00:00
|
|
|
return Ok(());
|
2023-06-26 22:33:00 +00:00
|
|
|
}
|
2023-06-22 20:08:10 +00:00
|
|
|
|
2023-06-26 22:33:00 +00:00
|
|
|
pub(crate) fn idle(&self) {
|
2023-07-31 20:50:48 +00:00
|
|
|
let mut sleep_duration = chrono::Duration::seconds(30);
|
2023-08-03 19:27:39 +00:00
|
|
|
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
|
|
|
|
2023-08-03 19:27:39 +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
|
|
|
|
2023-08-31 21:36:37 +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-30 18:34:24 +00:00
|
|
|
};
|
2023-07-29 00:32:58 +00:00
|
|
|
}
|
2023-06-18 22:26:50 +00:00
|
|
|
}
|
|
|
|
|
2023-08-31 21:36:37 +00:00
|
|
|
async fn list_posts(auth: &Sensitive<String>, base: String) -> GetPostsResponse {
|
2023-06-18 22:26:50 +00:00
|
|
|
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")
|
2023-06-18 22:26:50 +00:00
|
|
|
.query(¶ms)
|
2023-08-31 21:36:37 +00:00
|
|
|
.send().await
|
2023-06-18 22:26:50 +00:00
|
|
|
.unwrap()
|
2023-08-31 21:36:37 +00:00
|
|
|
.text().await
|
2023-06-18 22:26:50 +00:00
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
return serde_json::from_str(&res).unwrap();
|
|
|
|
}
|
|
|
|
|
2023-08-31 21:36:37 +00:00
|
|
|
async fn run_bot(bot: Arc<Mutex<Bot>>) {
|
|
|
|
// TODO this currently does not update the bot Mutex when run
|
2023-07-30 18:34:24 +00:00
|
|
|
// Get all needed auth tokens at the start
|
2023-08-03 19:27:39 +00:00
|
|
|
let mut old = Utc::now().naive_local();
|
2023-08-31 21:36:37 +00:00
|
|
|
|
|
|
|
let mut this = bot.lock().await.clone();
|
|
|
|
match this.login().await {
|
2023-07-31 17:50:22 +00:00
|
|
|
Ok(_) => {
|
2023-08-31 21:36:37 +00:00
|
|
|
println!("Login successful");
|
2023-07-31 17:50:22 +00:00
|
|
|
},
|
|
|
|
Err(e) => {
|
|
|
|
println!("Unable to get initial login:\n {:#?}", e);
|
2023-07-30 18:34:24 +00:00
|
|
|
}
|
2023-08-31 21:36:37 +00:00
|
|
|
};
|
|
|
|
let _ = this.community_ids.load(&this.auth, &this.config.instance).await;
|
|
|
|
|
|
|
|
loop {
|
|
|
|
this.idle();
|
|
|
|
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();
|
2023-07-30 18:34:24 +00:00
|
|
|
}
|
2023-06-21 19:29:14 +00:00
|
|
|
}
|
2023-08-31 21:36:37 +00:00
|
|
|
|
|
|
|
async fn print_info(shutdown: Arc<AtomicBool>, bot: Arc<Mutex<Bot>>) {
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-06-18 22:26:50 +00:00
|
|
|
}
|