Usage of new Modules
This commit is contained in:
parent
6cfdf3e085
commit
7cd56e530d
1 changed files with 59 additions and 479 deletions
538
src/main.rs
538
src/main.rs
|
@ -1,33 +1,25 @@
|
|||
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
|
||||
use config::{CommunitiesVector, Config, LemmyCommunities, PrevPost, Secrets};
|
||||
use lemmy_api_common::{
|
||||
lemmy_db_views::structs::PostView,
|
||||
person::{Login, LoginResponse},
|
||||
post::{CreatePost, FeaturePost, GetPosts, GetPostsResponse},
|
||||
sensitive::Sensitive,
|
||||
};
|
||||
use lemmy_db_schema::newtypes::PostId;
|
||||
use lemmy_db_schema::{newtypes::CommunityId, ListingType, PostFeatureType, SortType};
|
||||
use chrono::{DateTime, Duration, Timelike, Utc};
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
error::Error,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
vec,
|
||||
};
|
||||
use reqwest::{Client};
|
||||
use std::str::FromStr;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use std::{collections::HashMap, error::Error, vec};
|
||||
use std::fmt::Debug;
|
||||
use std::sync::{Arc};
|
||||
use tokio::sync::{RwLock};
|
||||
use std::thread::sleep;
|
||||
use dotenv::dotenv;
|
||||
use strum_macros::Display;
|
||||
use crate::config::Config;
|
||||
use crate::post_history::{SeriesHistory};
|
||||
|
||||
mod config;
|
||||
mod feeds;
|
||||
mod jnovel;
|
||||
mod bot;
|
||||
mod lemmy;
|
||||
mod tui;
|
||||
mod post_history;
|
||||
|
||||
pub static CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||
pub static HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::seconds(30).to_std().unwrap())
|
||||
.connect_timeout(Duration::seconds(30).to_std().unwrap())
|
||||
|
@ -36,480 +28,68 @@ pub static CLIENT: Lazy<Client> = Lazy::new(|| {
|
|||
client
|
||||
});
|
||||
|
||||
struct PostQueueMetadata {
|
||||
id: usize,
|
||||
series: String,
|
||||
part: Option<String>,
|
||||
volume: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Bot {
|
||||
secrets: Secrets,
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct SharedData {
|
||||
messages: Vec<Message>,
|
||||
config: Config,
|
||||
post_history: Vec<PrevPost>,
|
||||
community_ids: CommunitiesVector,
|
||||
auth: Sensitive<String>,
|
||||
login_error: bool,
|
||||
start_time: DateTime<Utc>,
|
||||
message_queue: Vec<String>,
|
||||
error_queue: Vec<String>,
|
||||
post_history: SeriesHistory,
|
||||
start: DateTime<Utc>,
|
||||
}
|
||||
|
||||
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()),
|
||||
login_error: true,
|
||||
start_time: Utc::now(),
|
||||
message_queue: vec![],
|
||||
error_queue: vec![],
|
||||
impl SharedData {
|
||||
pub(crate) fn new() -> Self {
|
||||
SharedData {
|
||||
messages: vec![],
|
||||
config: Config {
|
||||
instance: "".to_string(),
|
||||
status_post_url: None,
|
||||
config_reload_seconds: 0,
|
||||
series: vec![],
|
||||
},
|
||||
post_history: SeriesHistory {
|
||||
series: HashMap::new(),
|
||||
},
|
||||
start: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get JWT Token
|
||||
///
|
||||
/// * `return` : Returns true if token was successfully retrieved, false otherwise
|
||||
#[warn(unused_results)]
|
||||
pub(crate) async fn login(&mut self) -> Result<Sensitive<String>, Box<dyn 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 = CLIENT
|
||||
.post(self.config.instance.clone() + "/api/v3/user/login")
|
||||
.json(&login_params)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
return if res.status() == StatusCode::OK {
|
||||
let data: &LoginResponse = &res.json().await.unwrap();
|
||||
|
||||
let jwt = match data.jwt.clone() {
|
||||
Some(data) => data,
|
||||
None => {
|
||||
self.error_queue.push(format!("Error: Missing JWT Token"));
|
||||
return Err(Box::try_from(format!("Error: Missing JWT Token")).unwrap());
|
||||
}
|
||||
};
|
||||
self.auth = jwt.clone();
|
||||
self.login_error = false;
|
||||
Ok(jwt)
|
||||
} else {
|
||||
self.error_queue
|
||||
.push(format!("Error Code: {:?}", res.status()));
|
||||
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 successful, false otherwise
|
||||
#[warn(unused_results)]
|
||||
pub(crate) async fn post(&mut self, post_data: &CreatePost) -> Result<PostView, Box<dyn Error>> {
|
||||
let res = CLIENT
|
||||
.post(self.config.instance.clone() + "/api/v3/post")
|
||||
.json(post_data)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
// TODO: process res to get info about if post was successfully (mostly if jwt token was valid)
|
||||
let ret: PostView = serde_json::from_str::<HashMap<&str, PostView>>(res.text().await?.as_str())?
|
||||
.remove("post_view")
|
||||
.unwrap();
|
||||
return Ok(ret);
|
||||
}
|
||||
|
||||
#[warn(unused_results)]
|
||||
pub(crate) async fn pin_post(&mut self, new_post_id: &PostId, new_post_community: &CommunityId) -> Result<(bool, bool), Box<dyn Error>> {
|
||||
let mut local_pins = true;
|
||||
let mut community_pins = true;
|
||||
// Unpin Old Posts
|
||||
|
||||
// Get Local Posts & Unpin The Other Post
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
let get_params = GetPosts {
|
||||
auth: Some(self.auth.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
local_pins = self
|
||||
.unpin_old_posts(get_params, PostFeatureType::Local, &meta_community)
|
||||
.await?;
|
||||
|
||||
// Get Community Posts & Unpin The Other Post
|
||||
let get_params = GetPosts {
|
||||
auth: Some(self.auth.clone()),
|
||||
community_id: Some(new_post_community.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
community_pins = self
|
||||
.unpin_old_posts(get_params, PostFeatureType::Community, &meta_community)
|
||||
.await?;
|
||||
|
||||
// Pin New Post
|
||||
let pin_new_community = FeaturePost {
|
||||
post_id: new_post_id.clone(),
|
||||
featured: true,
|
||||
feature_type: PostFeatureType::Community,
|
||||
auth: self.auth.clone(),
|
||||
};
|
||||
|
||||
match self.pin(pin_new_community).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
self.message_queue
|
||||
.push(format!("Error Unpinning Post: {:#?}", e));
|
||||
community_pins = false;
|
||||
}
|
||||
};
|
||||
|
||||
let pin_new_local = FeaturePost {
|
||||
post_id: new_post_id.clone(),
|
||||
featured: true,
|
||||
feature_type: PostFeatureType::Local,
|
||||
auth: self.auth.clone(),
|
||||
};
|
||||
|
||||
match self.pin(pin_new_local).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
self.message_queue
|
||||
.push(format!("Error Unpinning Post: {:#?}", e));
|
||||
local_pins = false;
|
||||
}
|
||||
};
|
||||
|
||||
return Ok((community_pins, local_pins));
|
||||
}
|
||||
|
||||
#[warn(unused_results)]
|
||||
pub(crate) async fn unpin_old_posts(
|
||||
&mut self,
|
||||
get_params: GetPosts,
|
||||
pin_scope: PostFeatureType,
|
||||
meta_community: &CommunityId,
|
||||
) -> Result<bool, Box<dyn Error>> {
|
||||
let post_list_json = CLIENT
|
||||
.get(self.config.instance.clone() + "/api/v3/post/list")
|
||||
.query(&get_params)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
let post_list: GetPostsResponse = serde_json::from_str(post_list_json.as_str()).unwrap();
|
||||
|
||||
for post_view in post_list.posts {
|
||||
if &post_view.community.id != meta_community && ( ( pin_scope == PostFeatureType::Local && post_view.post.featured_local ) || ( pin_scope == PostFeatureType::Community && post_view.post.featured_community ) ) {
|
||||
let remove_local_pin = FeaturePost {
|
||||
post_id: post_view.post.id,
|
||||
featured: false,
|
||||
feature_type: pin_scope,
|
||||
auth: self.auth.clone(),
|
||||
};
|
||||
|
||||
match self.pin(remove_local_pin).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
self.message_queue
|
||||
.push(format!("Error Unpinning Post: {:#?}", e));
|
||||
return Err(Box::from(format!("{}", e)));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) async 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()
|
||||
.await?;
|
||||
|
||||
let ret: PostView = serde_json::from_str::<HashMap<&str, PostView>>(res.text().await?.as_str())?
|
||||
.remove("post_view")
|
||||
.unwrap();
|
||||
return Ok(ret.post.featured_local);
|
||||
}
|
||||
|
||||
pub(crate) async fn idle(&mut self) {
|
||||
let mut sleep_duration = Duration::seconds(30);
|
||||
if Utc::now() - self.start_time > sleep_duration {
|
||||
sleep_duration = Duration::seconds(60);
|
||||
}
|
||||
|
||||
match reqwest::get("https://status.neshweb.net/api/push/7s1CjPPzrV?status=up&msg=OK&ping=").await {
|
||||
Ok(_) => {}
|
||||
Err(err) => self.error_queue.push(format!("{}", err)),
|
||||
};
|
||||
|
||||
while Utc::now() - self.start_time < sleep_duration {
|
||||
sleep(Duration::milliseconds(100).to_std().unwrap()).await;
|
||||
pub(crate) fn get_messages(&self, errors: bool, warnings: bool, infos: bool) -> Vec<Message> {
|
||||
self.messages.iter().filter(|msg| {
|
||||
match msg {
|
||||
Message::Error(_) => true && errors,
|
||||
Message::Warning(_) => true && warnings,
|
||||
Message::Info(_) => true && infos,
|
||||
}
|
||||
}).cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
async 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
|
||||
.get(base + "/api/v3/post/list")
|
||||
.query(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
return serde_json::from_str(&res).unwrap();
|
||||
}
|
||||
|
||||
async fn run_bot(bot: Arc<Mutex<Bot>>) {
|
||||
let mut this = bot.lock().await.clone();
|
||||
let now = Utc::now;
|
||||
|
||||
let mut config_reload_duration = this.config.config_reload.unwrap_or(360);
|
||||
let mut config_reload_time = now() - Duration::minutes(config_reload_duration as i64 + 1); // Setting this to be in the future by default prevents unneeded code duplication
|
||||
|
||||
loop {
|
||||
*bot.lock().await = this.clone();
|
||||
|
||||
this.start_time = now();
|
||||
|
||||
this.idle().await;
|
||||
|
||||
// After configured time passed reload config
|
||||
if now() - config_reload_time >= Duration::minutes(config_reload_duration as i64) {
|
||||
this.config.load();
|
||||
this.secrets.load();
|
||||
|
||||
config_reload_duration = this.config.config_reload.unwrap_or(360);
|
||||
|
||||
let _ = this
|
||||
.community_ids
|
||||
.load(&this.auth, &this.config.instance)
|
||||
.await;
|
||||
|
||||
this.message_queue
|
||||
.push(format!("Config Reloaded at {}", now().naive_local()));
|
||||
config_reload_time = now();
|
||||
}
|
||||
// Check if Login token is valid, if it is not get a new one
|
||||
while this.login_error {
|
||||
let _ = this.login().await;
|
||||
}
|
||||
|
||||
// Perform Run
|
||||
// Start the polling process
|
||||
// Get all feed URLs (use cache)
|
||||
let queue_data = match this
|
||||
.config
|
||||
.check_feeds(&this.post_history, &this.community_ids, &this.auth)
|
||||
.await
|
||||
{
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
this.error_queue.push(format!("{}", e));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
this.message_queue
|
||||
.push(format!("Checked Feeds at {}", now().naive_local()));
|
||||
|
||||
for queued_post in queue_data {
|
||||
let (post, post_metadata) = queued_post;
|
||||
this.message_queue.push(format!("Posting: {}", post.name));
|
||||
|
||||
// Perform Post and Pins
|
||||
let post_data = match this.post(&post).await {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
this.error_queue.push(format!("{}", e));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let _ = this
|
||||
.pin_post(&post_data.post.id, &post_data.community.id)
|
||||
.await;
|
||||
|
||||
// Update Post History
|
||||
let mut index_exists = false;
|
||||
if this.post_history.len() > post_metadata.id {
|
||||
index_exists = true;
|
||||
}
|
||||
|
||||
if let Some(part_slug) = post_metadata.part {
|
||||
match index_exists {
|
||||
true => {
|
||||
this.post_history[post_metadata.id].last_part_slug = Some(part_slug);
|
||||
this.post_history[post_metadata.id].last_part_time = Some(now().to_string());
|
||||
}
|
||||
false => {
|
||||
let new_history = PrevPost {
|
||||
id: post_metadata.id,
|
||||
last_volume_slug: None,
|
||||
last_volume_time: None,
|
||||
last_part_slug: Some(part_slug),
|
||||
last_part_time: Some(now().to_string()),
|
||||
};
|
||||
this.post_history.push(new_history);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(volume_slug) = post_metadata.volume {
|
||||
match index_exists {
|
||||
true => {
|
||||
this.post_history[post_metadata.id].last_volume_slug = Some(volume_slug);
|
||||
this.post_history[post_metadata.id].last_volume_time = Some(now().to_string());
|
||||
}
|
||||
false => {
|
||||
let new_history = PrevPost {
|
||||
id: post_metadata.id,
|
||||
last_volume_slug: Some(volume_slug),
|
||||
last_volume_time: Some(now().to_string()),
|
||||
last_part_slug: None,
|
||||
last_part_time: None,
|
||||
};
|
||||
this.post_history.push(new_history);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
PrevPost::save(&this.post_history);
|
||||
|
||||
// Fix Queue Lengths and Update Mutex Data
|
||||
while this.message_queue.len() > 5 {
|
||||
this.message_queue.remove(0);
|
||||
}
|
||||
while this.error_queue.len() > 10 {
|
||||
this.error_queue.remove(0);
|
||||
}
|
||||
|
||||
this.idle().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn print_info(shutdown: Arc<AtomicBool>, bot: Arc<Mutex<Bot>>) {
|
||||
while !shutdown.load(Ordering::Relaxed) {
|
||||
let snapshot: tokio::sync::MutexGuard<'_, Bot> = bot.lock().await;
|
||||
sleep(Duration::milliseconds(200).to_std().unwrap()).await;
|
||||
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
println!(
|
||||
"##[Ascendance of a Bookworm Bot]## | Time: {}",
|
||||
Utc::now().naive_local().format("%H:%M:%S")
|
||||
);
|
||||
println!("Instance: {}", snapshot.config.instance);
|
||||
println!(
|
||||
"Ran Last: {}",
|
||||
snapshot
|
||||
.start_time
|
||||
.naive_local()
|
||||
.format("%d/%m/%Y %H:%M:%S")
|
||||
);
|
||||
println!("{:#<1$}", "", 175);
|
||||
snapshot.post_history.iter().for_each(|post| {
|
||||
if post.last_part_time.is_some() && post.last_volume_time.is_some() {
|
||||
let part_time = post.last_part_time.clone().unwrap();
|
||||
let volume_time = post.last_volume_time.clone().unwrap();
|
||||
|
||||
let parsed_part_time = DateTime::<Utc>::from_str(&part_time).unwrap_or(DateTime::<Utc>::from(UNIX_EPOCH)).naive_local();
|
||||
|
||||
let parsed_volume_time = DateTime::<Utc>::from_str(&volume_time).unwrap_or(DateTime::<Utc>::from(UNIX_EPOCH)).naive_local();
|
||||
|
||||
let formatted_time;
|
||||
if parsed_part_time > parsed_volume_time {
|
||||
formatted_time = parsed_part_time;
|
||||
}
|
||||
else {
|
||||
formatted_time = parsed_volume_time;
|
||||
}
|
||||
|
||||
print!("| {} |", formatted_time.format("%d/%m/%Y %H:%M:%S"));
|
||||
}
|
||||
else if post.last_part_time.is_some() {
|
||||
let part_time = post.last_part_time.clone().unwrap();
|
||||
let formatted_time: NaiveDateTime = DateTime::<Utc>::from_str(&part_time).unwrap_or(DateTime::<Utc>::from(UNIX_EPOCH)).naive_local();
|
||||
|
||||
print!("| {} |", formatted_time.format("%d/%m/%Y %H:%M:%S"));
|
||||
}
|
||||
else if post.last_volume_time.is_some() {
|
||||
let volume_time = post.last_volume_time.clone().unwrap();
|
||||
let formatted_time = DateTime::<Utc>::from_str(&volume_time).unwrap_or(DateTime::<Utc>::from(UNIX_EPOCH)).naive_local();
|
||||
|
||||
print!("| {} |", formatted_time.format("%d/%m/%Y %H:%M:%S"));
|
||||
}
|
||||
else {
|
||||
print!("| {:<1$} |", "", 19);
|
||||
}
|
||||
|
||||
print!("{:<1$}", "", 2 - post.id.to_string().len());
|
||||
print!("{}| ", post.id);
|
||||
print!("{}", post.last_part_slug.clone().unwrap_or("N/A".to_string()));
|
||||
print!("{:<1$}| ", "", 75 - post.last_part_slug.clone().unwrap_or("N/A".to_string()).len());
|
||||
print!("{}", post.last_volume_slug.clone().unwrap_or("N/A".to_string()));
|
||||
println!("{:<1$}| ", "", 70 - post.last_volume_slug.clone().unwrap_or("N/A".to_string()).len());
|
||||
});
|
||||
println!("{:#<1$}", "", 175);
|
||||
for error in snapshot.error_queue.iter() {
|
||||
println!("{}", error);
|
||||
}
|
||||
println!("{:#<1$}", "", 175);
|
||||
for message in snapshot.message_queue.iter() {
|
||||
println!("{}", message);
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Display)]
|
||||
pub(crate) enum Message {
|
||||
Info(String),
|
||||
Warning(String),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
dotenv().ok();
|
||||
let mut data = SharedData::new();
|
||||
|
||||
|
||||
loop {
|
||||
println!("Starting AoB Bot...");
|
||||
let write_data = Arc::new(RwLock::new(data.clone()));
|
||||
let read_data = write_data.clone();
|
||||
let persistent_data = write_data.clone();
|
||||
|
||||
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 tui_thread = tokio::spawn(async move { tui::run(read_data).await });
|
||||
let bot_thread = tokio::spawn(async move { bot::run(write_data).await });
|
||||
|
||||
let _ = bot_thread.await;
|
||||
|
||||
tui_thread.abort();
|
||||
|
||||
println!("Bot crashed due to unknown Error, restarting thread after wait...");
|
||||
|
||||
sleep(Duration::seconds(10).to_std().unwrap()).await;
|
||||
data = persistent_data.read().await.clone();
|
||||
data.messages.push(Message::Error(format!("Bot crashed due to unknown Error, restarting thread after wait...")));
|
||||
sleep(Duration::seconds(5).to_std().expect("Conversion should always work since static"));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue